Pārlūkot izejas kodu

Merge pull request #8775 from Popov72/ktx2-light

KTX2 loader reworking
sebavan 5 gadi atpakaļ
vecāks
revīzija
40d4812248
64 mainītis faili ar 1592 papildinājumiem un 107 dzēšanām
  1. 2 1
      .gitignore
  2. 0 1
      Playground/debug.html
  3. 0 1
      Playground/frame.html
  4. 0 1
      Playground/full.html
  5. 0 1
      Playground/index-local.html
  6. 0 1
      Playground/index.html
  7. BIN
      Playground/textures/ktx2/sample_etc1s.ktx2
  8. BIN
      Playground/textures/ktx2/sample_uastc.ktx2
  9. BIN
      Playground/textures/ktx2/sample_uastc_zcmp.ktx2
  10. BIN
      Playground/textures/ktx2/testalpha_etc1s.ktx2
  11. BIN
      Playground/textures/ktx2/testalpha_uastc.ktx2
  12. BIN
      Playground/textures/ktx2/testalpha_uastc_zcmp.ktx2
  13. BIN
      Playground/textures/ktx2/testmipmapcustom_etc1s.ktx2
  14. BIN
      Playground/textures/ktx2/testmipmapcustom_uastc.ktx2
  15. BIN
      Playground/textures/ktx2/testmipmapcustom_uastc_zcmp.ktx2
  16. 0 1
      Playground/zipContent/index.html
  17. 11 1
      Tools/Config/config.json
  18. 0 1
      Viewer/tests/validation/validate.html
  19. 0 22
      dist/ktx2Transcoders/msc_basis_transcoder.js
  20. BIN
      dist/ktx2Transcoders/msc_basis_transcoder.wasm
  21. BIN
      dist/ktx2Transcoders/uastc_astc.wasm
  22. BIN
      dist/ktx2Transcoders/uastc_bc7.wasm
  23. 2 2
      dist/preview release/ktx2Transcoders/msc_basis_transcoder.js
  24. BIN
      dist/preview release/ktx2Transcoders/msc_basis_transcoder.wasm
  25. BIN
      dist/preview release/ktx2Transcoders/uastc_astc.wasm
  26. BIN
      dist/preview release/ktx2Transcoders/uastc_bc7.wasm
  27. 0 22
      dist/preview release/libktx.js
  28. BIN
      dist/preview release/libktx.wasm
  29. BIN
      dist/preview release/zstddec.wasm
  30. BIN
      dist/zstddec.wasm
  31. 1 0
      ktx2Decoder/README-ES6.md
  32. 3 0
      ktx2Decoder/README.md
  33. 131 0
      ktx2Decoder/src/Misc/dataReader.ts
  34. 1 0
      ktx2Decoder/src/Misc/index.ts
  35. 4 0
      ktx2Decoder/src/Transcoders/index.ts
  36. 61 0
      ktx2Decoder/src/Transcoders/liteTranscoder.ts
  37. 20 0
      ktx2Decoder/src/Transcoders/liteTranscoder_UASTC_ASTC.ts
  38. 20 0
      ktx2Decoder/src/Transcoders/liteTranscoder_UASTC_BC7.ts
  39. 103 0
      ktx2Decoder/src/Transcoders/mscTranscoder.ts
  40. 8 0
      ktx2Decoder/src/index.ts
  41. 218 0
      ktx2Decoder/src/ktx2Decoder.ts
  42. 363 0
      ktx2Decoder/src/ktx2FileReader.ts
  43. 8 0
      ktx2Decoder/src/legacy/legacy.ts
  44. 49 0
      ktx2Decoder/src/transcoder.ts
  45. 43 0
      ktx2Decoder/src/transcoderManager.ts
  46. 82 0
      ktx2Decoder/src/wasmMemoryManager.ts
  47. 134 0
      ktx2Decoder/src/zstddec.ts
  48. 9 0
      ktx2Decoder/tsconfig.json
  49. 58 0
      ktx2Decoder/webpack.config.js
  50. 0 1
      loaders/src/glTF/2.0/Extensions/KHR_texture_basisu.ts
  51. 0 1
      localDev/index-views.html
  52. 0 1
      localDev/index.html
  53. 0 1
      sandbox/public/index-local.html
  54. 0 1
      sandbox/public/index.html
  55. 2 0
      src/Engines/engineCapabilities.ts
  56. 1 0
      src/Engines/nativeEngine.ts
  57. 1 0
      src/Engines/nullEngine.ts
  58. 1 0
      src/Engines/thinEngine.ts
  59. 1 1
      src/Materials/Textures/Loaders/ktxTextureLoader.ts
  60. 30 3
      src/Materials/Textures/baseTexture.ts
  61. 3 0
      src/Materials/Textures/internalTexture.ts
  62. 1 0
      src/Misc/index.ts
  63. 221 42
      src/Misc/khronosTextureContainer2.ts
  64. 0 1
      tests/validation/validate.html

+ 2 - 1
.gitignore

@@ -200,4 +200,5 @@ gui/dist/
 # Local Netlify folder
 .netlify
 Playground/dist/
-Sandbox/public/dist/
+Sandbox/public/dist/
+ktx2Decoder/dist/

+ 0 - 1
Playground/debug.html

@@ -34,7 +34,6 @@
         <script src="https://preview.babylonjs.com/recast.js"></script>
         <script src="https://preview.babylonjs.com/cannon.js"></script>
         <script src="https://preview.babylonjs.com/Oimo.js"></script>
-        <script src="https://preview.babylonjs.com/libktx.js"></script>
         <script src="https://preview.babylonjs.com/earcut.min.js"></script>
         
         <!-- Babylon.js -->

+ 0 - 1
Playground/frame.html

@@ -33,7 +33,6 @@
         <script src="https://preview.babylonjs.com/recast.js"></script>
         <script src="https://preview.babylonjs.com/cannon.js"></script>
         <script src="https://preview.babylonjs.com/Oimo.js"></script>
-        <script src="https://preview.babylonjs.com/libktx.js"></script>
         <script src="https://preview.babylonjs.com/earcut.min.js"></script>
         
         <!-- Babylon.js -->

+ 0 - 1
Playground/full.html

@@ -29,7 +29,6 @@
         <script src="https://preview.babylonjs.com/recast.js"></script>
         <script src="https://preview.babylonjs.com/cannon.js"></script>
         <script src="https://preview.babylonjs.com/Oimo.js"></script>
-        <script src="https://preview.babylonjs.com/libktx.js"></script>
         <script src="https://preview.babylonjs.com/earcut.min.js"></script>
 
         <!-- jQuery -->

+ 0 - 1
Playground/index-local.html

@@ -33,7 +33,6 @@
         <script src="/dist/preview%20release/recast.js"></script>
         <script src="/dist/preview%20release/cannon.js"></script>
         <script src="/dist/preview%20release/Oimo.js"></script>
-        <script src="/dist/preview%20release/libktx.js"></script>
         <script src="/dist/preview%20release/earcut.min.js"></script>
         
         <!-- Babylon.js -->

+ 0 - 1
Playground/index.html

@@ -33,7 +33,6 @@
         <script src="https://preview.babylonjs.com/recast.js"></script>
         <script src="https://preview.babylonjs.com/cannon.js"></script>
         <script src="https://preview.babylonjs.com/Oimo.js"></script>
-        <script src="https://preview.babylonjs.com/libktx.js"></script>
         <script src="https://preview.babylonjs.com/earcut.min.js"></script>
         
         <!-- Babylon.js -->

BIN
Playground/textures/ktx2/sample_etc1s.ktx2


BIN
Playground/textures/ktx2/sample_uastc.ktx2


BIN
Playground/textures/ktx2/sample_uastc_zcmp.ktx2


BIN
Playground/textures/ktx2/testalpha_etc1s.ktx2


BIN
Playground/textures/ktx2/testalpha_uastc.ktx2


BIN
Playground/textures/ktx2/testalpha_uastc_zcmp.ktx2


BIN
Playground/textures/ktx2/testmipmapcustom_etc1s.ktx2


BIN
Playground/textures/ktx2/testmipmapcustom_uastc.ktx2


BIN
Playground/textures/ktx2/testmipmapcustom_uastc_zcmp.ktx2


+ 0 - 1
Playground/zipContent/index.html

@@ -11,7 +11,6 @@
         <script src="https://preview.babylonjs.com/ammo.js"></script>
         <script src="https://preview.babylonjs.com/cannon.js"></script>
         <script src="https://preview.babylonjs.com/Oimo.js"></script>
-        <script src="https://preview.babylonjs.com/libktx.js"></script>
         <script src="https://preview.babylonjs.com/earcut.min.js"></script>
         <script src="https://preview.babylonjs.com/babylon.js"></script>
         <script src="https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>

+ 11 - 1
Tools/Config/config.json

@@ -63,7 +63,8 @@
     ],
     "apps": [
         "playground",
-        "sandbox"
+        "sandbox",
+        "ktx2Decoder"
     ],
     "lintModules": [
         "core",
@@ -640,6 +641,15 @@
             }
         }
     },
+    "ktx2Decoder": {
+        "distFile": "/dist/preview release/babylon.ktx2Decoder.js",
+        "build": {
+            "ignoreInWorkerMode": true,
+            "ignoreInTestMode": true,
+            "distOutputDirectory": "../../dist/preview release/",
+            "mainFolder": "./ktx2Decoder/"
+        }
+    },
     "playground": {
         "distFile": "/Playground/dist/babylon.playground.js",
         "build": {

+ 0 - 1
Viewer/tests/validation/validate.html

@@ -6,7 +6,6 @@
 	<script src="https://preview.babylonjs.com/ammo.js"></script>
 	<script src="https://preview.babylonjs.com/cannon.js"></script>
     <script src="https://preview.babylonjs.com/Oimo.js"></script>
-	<script src="https://preview.babylonjs.com/libktx.js"></script>
     <script src="https://preview.babylonjs.com/babylon.js"></script>
     <script src="https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>
 

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 22
dist/ktx2Transcoders/msc_basis_transcoder.js


BIN
dist/ktx2Transcoders/msc_basis_transcoder.wasm


BIN
dist/ktx2Transcoders/uastc_astc.wasm


BIN
dist/ktx2Transcoders/uastc_bc7.wasm


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 2 - 2
dist/preview release/ktx2Transcoders/msc_basis_transcoder.js


BIN
dist/preview release/ktx2Transcoders/msc_basis_transcoder.wasm


BIN
dist/preview release/ktx2Transcoders/uastc_astc.wasm


BIN
dist/preview release/ktx2Transcoders/uastc_bc7.wasm


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 22
dist/preview release/libktx.js


BIN
dist/preview release/libktx.wasm


BIN
dist/preview release/zstddec.wasm


BIN
dist/zstddec.wasm


+ 1 - 0
ktx2Decoder/README-ES6.md

@@ -0,0 +1 @@
+KTX2 Decoder

+ 3 - 0
ktx2Decoder/README.md

@@ -0,0 +1,3 @@
+# Babylon.js KTX2 Decoder
+
+An extension to decode ktx2 files.

+ 131 - 0
ktx2Decoder/src/Misc/dataReader.ts

@@ -0,0 +1,131 @@
+/**
+ * Utility class for reading from a data buffer
+ */
+export class DataReader {
+    /**
+     * The current byte offset from the beginning of the data buffer.
+     */
+    public get byteOffset() {
+        return this._dataByteOffset;
+    }
+
+    private _dataView: DataView;
+    private _dataByteOffset: number;
+
+    /**
+     * Constructor
+     * @param buffer The buffer to set
+     * @param byteOffset The starting offset in the buffer
+     * @param byteLength The byte length of the buffer
+     */
+    constructor(buffer: ArrayBuffer | ArrayBufferView, byteOffset?: number, byteLength?: number) {
+        if ((buffer as  ArrayBufferView).buffer) {
+            this._dataView = new DataView((buffer as ArrayBufferView).buffer, (buffer as ArrayBufferView).byteOffset + (byteOffset ?? 0), byteLength ?? (buffer as ArrayBufferView).byteLength);
+        } else {
+            this._dataView = new DataView(buffer as ArrayBuffer, byteOffset ?? 0, byteLength ?? (buffer as ArrayBuffer).byteLength);
+        }
+
+        this._dataByteOffset = 0;
+    }
+
+    /**
+     * Read a unsigned 8-bit integer from the currently loaded data range.
+     * @returns The 8-bit integer read
+     */
+    public readUint8(): number {
+        const value = this._dataView.getUint8(this._dataByteOffset);
+        this._dataByteOffset += 1;
+        return value;
+    }
+
+    /**
+     * Read a signed 8-bit integer from the currently loaded data range.
+     * @returns The 8-bit integer read
+     */
+    public readInt8(): number {
+        const value = this._dataView.getInt8(this._dataByteOffset);
+        this._dataByteOffset += 1;
+        return value;
+    }
+
+    /**
+     * Read a unsigned 16-bit integer from the currently loaded data range.
+     * @returns The 16-bit integer read
+     */
+    public readUint16(): number {
+        const value = this._dataView.getUint16(this._dataByteOffset, true);
+        this._dataByteOffset += 2;
+        return value;
+    }
+
+    /**
+     * Read a signed 16-bit integer from the currently loaded data range.
+     * @returns The 16-bit integer read
+     */
+    public readInt16(): number {
+        const value = this._dataView.getInt16(this._dataByteOffset, true);
+        this._dataByteOffset += 2;
+        return value;
+    }
+
+    /**
+     * Read a unsigned 32-bit integer from the currently loaded data range.
+     * @returns The 32-bit integer read
+     */
+    public readUint32(): number {
+        const value = this._dataView.getUint32(this._dataByteOffset, true);
+        this._dataByteOffset += 4;
+        return value;
+    }
+
+    /**
+     * Read a signed 32-bit integer from the currently loaded data range.
+     * @returns The 32-bit integer read
+     */
+    public readInt32(): number {
+        const value = this._dataView.getInt32(this._dataByteOffset, true);
+        this._dataByteOffset += 4;
+        return value;
+    }
+
+    /**
+     * Read a unsigned 32-bit integer from the currently loaded data range.
+     * @returns The 32-bit integer read
+     */
+    public readUint64(): number {
+        // split 64-bit number into two 32-bit (4-byte) parts
+        const left = this._dataView.getUint32(this._dataByteOffset, true);
+        const right = this._dataView.getUint32(this._dataByteOffset + 4, true);
+
+        // combine the two 32-bit values
+        const combined = true ? left + (2 ** 32 * right) : (2 ** 32 * left) + right;
+
+        /*if (!Number.isSafeInteger(combined)) {
+            console.warn('DataReader: ' + combined + ' exceeds MAX_SAFE_INTEGER. Precision may be lost.');
+        }*/
+
+        this._dataByteOffset += 8;
+        return combined;
+    }
+
+    /**
+     * Read a byte array from the currently loaded data range.
+     * @param byteLength The byte length to read
+     * @returns The byte array read
+     */
+    public readUint8Array(byteLength: number): Uint8Array {
+        const value = new Uint8Array(this._dataView.buffer, this._dataView.byteOffset + this._dataByteOffset, byteLength);
+        this._dataByteOffset += byteLength;
+        return value;
+    }
+
+    /**
+     * Skips the given byte length the currently loaded data range.
+     * @param byteLength The byte length to skip
+     * @returns This instance
+     */
+    public skipBytes(byteLength: number) {
+        this._dataByteOffset += byteLength;
+        return this;
+    }
+}

+ 1 - 0
ktx2Decoder/src/Misc/index.ts

@@ -0,0 +1 @@
+export * from "./dataReader";

+ 4 - 0
ktx2Decoder/src/Transcoders/index.ts

@@ -0,0 +1,4 @@
+export * from "./liteTranscoder";
+export * from "./liteTranscoder_UASTC_ASTC";
+export * from "./liteTranscoder_UASTC_BC7";
+export * from "./mscTranscoder";

+ 61 - 0
ktx2Decoder/src/Transcoders/liteTranscoder.ts

@@ -0,0 +1,61 @@
+import { Transcoder, sourceTextureFormat, transcodeTarget } from '../transcoder';
+import { WASMMemoryManager } from '../wasmMemoryManager';
+import { KTX2FileReader, IKTX2_ImageDesc } from '../ktx2FileReader';
+
+/**
+ * @hidden
+ */
+export class LiteTranscoder extends Transcoder {
+
+    private _modulePath: string;
+    private _modulePromise: Promise<{ module: any }>;
+    private _memoryManager: WASMMemoryManager;
+
+    protected _loadModule(): Promise<{ module: any }> {
+        if (this._modulePromise) {
+            return this._modulePromise;
+        }
+
+        this._modulePromise = WASMMemoryManager.LoadWASM(this._modulePath).then((wasmBinary) => {
+            return new Promise((resolve) => {
+                WebAssembly.instantiate(wasmBinary as ArrayBuffer, { env: { memory: this._memoryManager.wasmMemory } }).then((moduleWrapper) => {
+                    resolve({ module: moduleWrapper.instance.exports });
+                });
+            });
+        });
+
+        return this._modulePromise;
+    }
+
+    protected get memoryManager(): WASMMemoryManager {
+        return this._memoryManager;
+    }
+
+    protected setModulePath(modulePath: string): void {
+        this._modulePath = modulePath;
+    }
+
+    public needMemoryManager(): boolean {
+        return true;
+    }
+
+    public setMemoryManager(memoryMgr: WASMMemoryManager): void {
+        this._memoryManager = memoryMgr;
+    }
+
+    public transcode(src: sourceTextureFormat, dst: transcodeTarget, level: number, width: number, height: number, uncompressedByteLength: number, ktx2Reader: KTX2FileReader, imageDesc: IKTX2_ImageDesc | null, encodedData: Uint8Array): Promise<Uint8Array | null> {
+        return this._loadModule().then((moduleWrapper: any) => {
+            const transcoder: any = moduleWrapper.module;
+
+            const nBlocks = ((width + 3) >> 2) * ((height + 3) >> 2);
+
+            const texMemoryPages = ((nBlocks * 16 + 65535) >> 16) + 1;
+
+            const textureView = this.memoryManager.getMemoryView(texMemoryPages, 65536, nBlocks * 16);
+
+            textureView.set(encodedData);
+
+            return transcoder.transcode(nBlocks) === 0 ? textureView.slice() : null;
+        });
+    }
+}

+ 20 - 0
ktx2Decoder/src/Transcoders/liteTranscoder_UASTC_ASTC.ts

@@ -0,0 +1,20 @@
+import { sourceTextureFormat, transcodeTarget } from '../transcoder';
+import { LiteTranscoder } from './liteTranscoder';
+
+/**
+ * @hidden
+ */
+export class LiteTranscoder_UASTC_ASTC extends LiteTranscoder {
+    /**
+     * URL to use when loading the wasm module for the transcoder
+     */
+    public static WasmModuleURL = "https://preview.babylonjs.com/ktx2Transcoders/uastc_astc.wasm";
+
+    public static CanTranscode(src: sourceTextureFormat, dst: transcodeTarget): boolean {
+        return src === sourceTextureFormat.UASTC4x4 && dst === transcodeTarget.ASTC_4x4_RGBA;
+    }
+
+    public initialize(): void {
+        this.setModulePath(LiteTranscoder_UASTC_ASTC.WasmModuleURL);
+    }
+}

+ 20 - 0
ktx2Decoder/src/Transcoders/liteTranscoder_UASTC_BC7.ts

@@ -0,0 +1,20 @@
+import { sourceTextureFormat, transcodeTarget } from '../transcoder';
+import { LiteTranscoder } from './liteTranscoder';
+
+/**
+ * @hidden
+ */
+export class LiteTranscoder_UASTC_BC7 extends LiteTranscoder {
+    /**
+     * URL to use when loading the wasm module for the transcoder
+     */
+    public static WasmModuleURL = "https://preview.babylonjs.com/ktx2Transcoders/uastc_bc7.wasm";
+
+    public static CanTranscode(src: sourceTextureFormat, dst: transcodeTarget): boolean {
+        return src === sourceTextureFormat.UASTC4x4 && dst === transcodeTarget.BC7_RGBA;
+    }
+
+    public initialize(): void {
+        this.setModulePath(LiteTranscoder_UASTC_BC7.WasmModuleURL);
+    }
+}

+ 103 - 0
ktx2Decoder/src/Transcoders/mscTranscoder.ts

@@ -0,0 +1,103 @@
+import { Transcoder, sourceTextureFormat, transcodeTarget } from '../transcoder';
+import { KTX2FileReader, IKTX2_ImageDesc } from '../ktx2FileReader';
+import { WASMMemoryManager } from '../wasmMemoryManager';
+
+declare var MSC_TRANSCODER: any;
+
+declare function importScripts(...urls: string[]): void;
+
+/**
+ * @hidden
+ */
+export class MSCTranscoder extends Transcoder {
+    /**
+     * URL to use when loading the MSC transcoder
+     */
+    public static JSModuleURL = "https://preview.babylonjs.com/ktx2Transcoders/msc_basis_transcoder.js";
+    /**
+     * URL to use when loading the wasm module for the transcoder
+     */
+    public static WasmModuleURL = "https://preview.babylonjs.com/ktx2Transcoders/msc_basis_transcoder.wasm";
+
+    public static UseFromWorkerThread = true;
+
+    private _mscBasisTranscoderPromise: Promise<any>;
+    private _mscBasisModule: any;
+
+    private _getMSCBasisTranscoder(): Promise<any> {
+        if (this._mscBasisTranscoderPromise) {
+            return this._mscBasisTranscoderPromise;
+        }
+
+        this._mscBasisTranscoderPromise = WASMMemoryManager.LoadWASM(MSCTranscoder.WasmModuleURL).then((wasmBinary) => {
+            if (MSCTranscoder.UseFromWorkerThread) {
+                importScripts(MSCTranscoder.JSModuleURL);
+            }
+            return new Promise((resolve) => {
+                MSC_TRANSCODER({ wasmBinary }).then((basisModule: any) => {
+                    basisModule.initTranscoders();
+                    this._mscBasisModule = basisModule;
+                    resolve();
+                });
+            });
+        });
+
+        return this._mscBasisTranscoderPromise;
+    }
+
+    public static CanTranscode(src: sourceTextureFormat, dst: transcodeTarget): boolean {
+        return true;
+    }
+
+    public transcode(src: sourceTextureFormat, dst: transcodeTarget, level: number, width: number, height: number, uncompressedByteLength: number, ktx2Reader: KTX2FileReader, imageDesc: IKTX2_ImageDesc | null, encodedData: Uint8Array): Promise<Uint8Array | null> {
+        const isVideo = false;
+
+        return this._getMSCBasisTranscoder().then(() => {
+            const basisModule = this._mscBasisModule;
+
+            const transcoder = src === sourceTextureFormat.UASTC4x4 ? new basisModule.UastcImageTranscoder() : new basisModule.BasisLzEtc1sImageTranscoder();
+            const texFormat = src === sourceTextureFormat.UASTC4x4 ? basisModule.TextureFormat.UASTC4x4 : basisModule.TextureFormat.ETC1S;
+
+            const imageInfo = new basisModule.ImageInfo(texFormat, width, height, level);
+
+            const targetFormat = basisModule.TranscodeTarget[transcodeTarget[dst]]; // works because the labels of the sourceTextureFormat enum are the same than the property names used in TranscodeTarget!
+
+            if (!basisModule.isFormatSupported(targetFormat, texFormat)) {
+                throw new Error(`MSCTranscoder: Transcoding from "${sourceTextureFormat[src]}" to "${transcodeTarget[dst]}" not supported by current transcoder build.`);
+            }
+
+            let result: any;
+
+            if (src === sourceTextureFormat.ETC1S) {
+                const sgd = ktx2Reader.supercompressionGlobalData;
+
+                transcoder.decodePalettes(sgd.endpointCount, sgd.endpointsData, sgd.selectorCount, sgd.selectorsData);
+                transcoder.decodeTables(sgd.tablesData);
+
+                imageInfo.flags = imageDesc!.imageFlags;
+                imageInfo.rgbByteOffset = 0;
+                imageInfo.rgbByteLength = imageDesc!.rgbSliceByteLength;
+                imageInfo.alphaByteOffset = imageDesc!.alphaSliceByteOffset > 0 ? imageDesc!.rgbSliceByteLength : 0;
+                imageInfo.alphaByteLength = imageDesc!.alphaSliceByteLength;
+
+                result = transcoder.transcodeImage(targetFormat, encodedData, imageInfo, 0, isVideo);
+            } else {
+                imageInfo.flags = 0;
+                imageInfo.rgbByteOffset = 0;
+                imageInfo.rgbByteLength = uncompressedByteLength;
+                imageInfo.alphaByteOffset = 0;
+                imageInfo.alphaByteLength = 0;
+
+                result = transcoder.transcodeImage(targetFormat, encodedData, imageInfo, 0, ktx2Reader.hasAlpha, isVideo);
+            }
+
+            if (result && result.transcodedImage !== undefined) {
+                const textureData = result.transcodedImage.get_typed_memory_view().slice();
+                result.transcodedImage.delete();
+                return textureData;
+            }
+
+            return null;
+        });
+    }
+}

+ 8 - 0
ktx2Decoder/src/index.ts

@@ -0,0 +1,8 @@
+export * from "./ktx2Decoder";
+export * from "./ktx2FileReader";
+export * from "./transcoder";
+export * from "./transcoderManager";
+export * from "./wasmMemoryManager";
+export * from "./zstddec";
+export * from "./Misc/index";
+export * from "./Transcoders/index";

+ 218 - 0
ktx2Decoder/src/ktx2Decoder.ts

@@ -0,0 +1,218 @@
+/**
+ * Resources used for the implementation:
+ *  - 3js KTX2 loader: https://github.com/mrdoob/three.js/blob/dfb5c23ce126ec845e4aa240599915fef5375797/examples/jsm/loaders/KTX2Loader.js
+ *  - Universal Texture Transcoders: https://github.com/KhronosGroup/Universal-Texture-Transcoders
+ *  - KTX2 specification: http://github.khronos.org/KTX-Specification/
+ *  - KTX2 binaries to convert files: https://github.com/KhronosGroup/KTX-Software/releases
+ *  - KTX specification: https://www.khronos.org/registry/DataFormat/specs/1.3/dataformat.1.3.html
+ *  - KTX-Software: https://github.com/KhronosGroup/KTX-Software
+ */
+
+import { KTX2FileReader, SupercompressionScheme, IKTX2_ImageDesc } from './ktx2FileReader';
+import { TranscoderManager } from './transcoderManager';
+import { LiteTranscoder_UASTC_ASTC } from './Transcoders/liteTranscoder_UASTC_ASTC';
+import { LiteTranscoder_UASTC_BC7 } from './Transcoders/liteTranscoder_UASTC_BC7';
+import { MSCTranscoder } from './Transcoders/mscTranscoder';
+import { transcodeTarget, sourceTextureFormat } from './transcoder';
+import { ZSTDDecoder } from './zstddec';
+
+const COMPRESSED_RGBA_BPTC_UNORM_EXT = 0x8E8C;
+const COMPRESSED_RGBA_ASTC_4x4_KHR = 0x93B0;
+const COMPRESSED_RGB_S3TC_DXT1_EXT  = 0x83F0;
+const COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3;
+const COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8C02;
+const COMPRESSED_RGB_PVRTC_4BPPV1_IMG = 0x8C00;
+const COMPRESSED_RGBA8_ETC2_EAC = 0x9278;
+const COMPRESSED_RGB8_ETC2 = 0x9274;
+const COMPRESSED_RGB_ETC1_WEBGL = 0x8D64;
+const RGBA8Format = 0x8058;
+
+export interface IDecodedData {
+    width: number;
+    height: number;
+    transcodedFormat: number;
+    mipmaps: Array<IMipmap>;
+    isInGammaSpace: boolean;
+    errors?: string;
+}
+
+export interface IMipmap {
+    data: Uint8Array | null;
+    width: number;
+    height: number;
+}
+
+export interface ICompressedFormatCapabilities {
+    astc?: boolean;
+    bptc?: boolean;
+    s3tc?: boolean;
+    pvrtc?: boolean;
+    etc2?: boolean;
+    etc1?: boolean;
+}
+
+const isPowerOfTwo = (value: number)  => {
+    return (value & (value - 1)) === 0 && value !== 0;
+};
+
+/**
+ * Class for decoding KTX2 files
+ */
+export class KTX2Decoder {
+
+    private _transcoderMgr: TranscoderManager;
+    private _zstdDecoder: ZSTDDecoder;
+
+    constructor() {
+        this._transcoderMgr = new TranscoderManager();
+    }
+
+    public decode(data: Uint8Array, caps: ICompressedFormatCapabilities): Promise<IDecodedData | null> {
+        return Promise.resolve().then(() => {
+            const kfr = new KTX2FileReader(data);
+
+            if (!kfr.isValid()) {
+                throw new Error("Invalid KT2 file: wrong signature");
+            }
+
+            kfr.parse();
+
+            if (kfr.needZSTDDecoder) {
+                if (!this._zstdDecoder) {
+                    this._zstdDecoder = new ZSTDDecoder();
+                }
+
+                return this._zstdDecoder.init().then(() => {
+                    return this._decodeData(kfr, caps);
+                });
+            }
+
+            return this._decodeData(kfr, caps);
+        });
+    }
+
+    private _decodeData(kfr: KTX2FileReader, caps: ICompressedFormatCapabilities): Promise<IDecodedData> {
+        const width = kfr.header.pixelWidth;
+        const height = kfr.header.pixelHeight;
+        const srcTexFormat = kfr.textureFormat;
+
+        // PVRTC1 transcoders (from both ETC1S and UASTC) only support power of 2 dimensions.
+        const pvrtcTranscodable = isPowerOfTwo(width) && isPowerOfTwo(height);
+
+        let targetFormat = -1;
+        let transcodedFormat = -1;
+
+        if (caps.astc) {
+            targetFormat = transcodeTarget.ASTC_4x4_RGBA;
+            transcodedFormat = COMPRESSED_RGBA_ASTC_4x4_KHR;
+        } else if (caps.bptc) {
+            targetFormat = transcodeTarget.BC7_RGBA;
+            transcodedFormat = COMPRESSED_RGBA_BPTC_UNORM_EXT;
+        } else if (caps.s3tc) {
+            targetFormat = kfr.hasAlpha ? transcodeTarget.BC3_RGBA : transcodeTarget.BC1_RGB;
+            transcodedFormat = kfr.hasAlpha ? COMPRESSED_RGBA_S3TC_DXT5_EXT : COMPRESSED_RGB_S3TC_DXT1_EXT;
+        } else if (caps.pvrtc && pvrtcTranscodable) {
+            targetFormat = kfr.hasAlpha ? transcodeTarget.PVRTC1_4_RGBA : transcodeTarget.PVRTC1_4_RGB;
+            transcodedFormat = kfr.hasAlpha ? COMPRESSED_RGBA_PVRTC_4BPPV1_IMG : COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
+        } else if (caps.etc2) {
+            targetFormat = kfr.hasAlpha ? transcodeTarget.ETC2_RGBA : transcodeTarget.ETC1_RGB /* subset of ETC2 */;
+            transcodedFormat = kfr.hasAlpha ? COMPRESSED_RGBA8_ETC2_EAC : COMPRESSED_RGB8_ETC2;
+        } else if (caps.etc1) {
+            targetFormat = transcodeTarget.ETC1_RGB;
+            transcodedFormat = COMPRESSED_RGB_ETC1_WEBGL;
+        } else {
+            targetFormat = transcodeTarget.RGBA32;
+            transcodedFormat = RGBA8Format;
+        }
+
+        const transcoder = this._transcoderMgr.findTranscoder(srcTexFormat, targetFormat);
+
+        if (transcoder === null) {
+            throw new Error(`no transcoder found to transcode source texture format "${sourceTextureFormat[srcTexFormat]}" to format "${transcodeTarget[targetFormat]}"`);
+        }
+
+        const mipmaps: Array<IMipmap> = [];
+        const dataPromises: Array<Promise<Uint8Array | null>> = [];
+        const mipmapBuffers: Array<ArrayBuffer> = [];
+        const decodedData: IDecodedData = { width: 0, height: 0, transcodedFormat, mipmaps, isInGammaSpace: kfr.isInGammaSpace };
+
+        let firstImageDescIndex = 0;
+
+        for (let level = 0; level < kfr.header.levelCount; level ++) {
+            if (level > 0) {
+                firstImageDescIndex += Math.max(kfr.header.layerCount, 1) * kfr.header.faceCount * Math.max(kfr.header.pixelDepth >> (level - 1), 1);
+            }
+
+            const levelWidth = Math.ceil(width / (1 << level));
+            const levelHeight = Math.ceil(height / (1 << level));
+
+            const numImagesInLevel = kfr.header.faceCount; // note that cubemap are not supported yet (see KTX2FileReader), so faceCount == 1
+            const levelImageByteLength = ((levelWidth + 3) >> 2) * ((levelHeight + 3) >> 2) * kfr.dfdBlock.bytesPlane[0];
+
+            const levelUncompressedByteLength = kfr.levels[level].uncompressedByteLength;
+
+            let levelDataBuffer = kfr.data.buffer;
+
+            let levelDataOffset = kfr.levels[level].byteOffset + kfr.data.byteOffset;
+            let imageOffsetInLevel = 0;
+
+            if (kfr.header.supercompressionScheme === SupercompressionScheme.ZStandard) {
+                levelDataBuffer = this._zstdDecoder.decode(new Uint8Array(levelDataBuffer, levelDataOffset, kfr.levels[level].byteLength), levelUncompressedByteLength);
+                levelDataOffset = 0;
+            }
+
+            if (level === 0) {
+                decodedData.width = levelWidth;
+                decodedData.height = levelHeight;
+            }
+
+            for (let imageIndex = 0; imageIndex < numImagesInLevel; imageIndex ++) {
+                let encodedData: Uint8Array;
+                let imageDesc: IKTX2_ImageDesc | null = null;
+
+                if (kfr.header.supercompressionScheme === SupercompressionScheme.BasisLZ) {
+                    imageDesc = kfr.supercompressionGlobalData.imageDescs![firstImageDescIndex + imageIndex];
+
+                    encodedData = new Uint8Array(levelDataBuffer, levelDataOffset + imageDesc.rgbSliceByteOffset, imageDesc.rgbSliceByteLength + imageDesc.alphaSliceByteLength);
+                } else {
+                    encodedData = new Uint8Array(levelDataBuffer, levelDataOffset + imageOffsetInLevel, levelImageByteLength);
+
+                    imageOffsetInLevel += levelImageByteLength;
+                }
+
+                const mipmap: IMipmap = {
+                    data: null,
+                    width: levelWidth,
+                    height: levelHeight,
+                };
+
+                const transcodedData = transcoder.transcode(srcTexFormat, targetFormat, level, levelWidth, levelHeight, levelUncompressedByteLength, kfr, imageDesc, encodedData)
+                    .then((data) => {
+                        mipmap.data = data;
+                        if (data) {
+                            mipmapBuffers.push(data.buffer);
+                        }
+                        return data;
+                    })
+                    .catch((reason) => {
+                        decodedData.errors = decodedData.errors ?? "";
+                        decodedData.errors += reason + "\n";
+                        return null;
+                    });
+
+                dataPromises.push(transcodedData);
+
+                mipmaps.push(mipmap);
+            }
+        }
+
+        return Promise.all(dataPromises).then(() => {
+            return decodedData;
+        });
+    }
+}
+
+// Put in the order you want the transcoders to be used in priority
+TranscoderManager.RegisterTranscoder(LiteTranscoder_UASTC_ASTC);
+TranscoderManager.RegisterTranscoder(LiteTranscoder_UASTC_BC7);
+TranscoderManager.RegisterTranscoder(MSCTranscoder); // catch all transcoder - will throw an error if the format can't be transcoded

+ 363 - 0
ktx2Decoder/src/ktx2FileReader.ts

@@ -0,0 +1,363 @@
+import { DataReader } from './Misc/dataReader';
+import { sourceTextureFormat } from './transcoder';
+
+/** @hidden */
+export enum SupercompressionScheme {
+    None = 0,
+    BasisLZ = 1,
+    ZStandard = 2,
+    ZLib = 3
+}
+
+const enum DFDModel {
+    ETC1S = 163,
+    UASTC = 166
+}
+
+const enum DFDChannel_ETC1S {
+    RGB = 0,
+    RRR = 3,
+    GGG = 4,
+    AAA = 15
+
+}
+
+const enum DFDChannel_UASTC {
+    RGB  = 0,
+    RGBA = 3,
+    RRR  = 4,
+    RRRG = 5
+}
+
+const enum DFDTransferFunction {
+    linear = 1,
+    sRGB = 2
+}
+
+/** @hidden */
+export interface IKTX2_Header {
+    vkFormat: number;
+    typeSize: number;
+    pixelWidth: number;
+    pixelHeight: number;
+    pixelDepth: number;
+    layerCount: number;
+    faceCount: number;
+    levelCount: number;
+    supercompressionScheme: number;
+    dfdByteOffset: number;
+    dfdByteLength: number;
+    kvdByteOffset: number;
+    kvdByteLength: number;
+    sgdByteOffset: number;
+    sgdByteLength: number;
+}
+
+/** @hidden */
+export interface IKTX2_Level {
+    byteOffset: number;
+    byteLength: number;
+    uncompressedByteLength: number;
+}
+
+interface IKTX2_Sample {
+    bitOffset: number;
+    bitLength: number;
+    channelType: number;
+    channelFlags: number;
+    samplePosition: number[];
+    sampleLower: number;
+    sampleUpper: number;
+}
+
+/** @hidden */
+export interface IKTX2_DFD {
+    vendorId: number;
+    descriptorType: number;
+    versionNumber: number;
+    descriptorBlockSize: number;
+    colorModel: number;
+    colorPrimaries: number;
+    transferFunction: number;
+    flags: number;
+    texelBlockDimension: {
+        x: number;
+        y: number;
+        z: number;
+        w: number;
+    };
+    bytesPlane: Array<number>;
+    numSamples: number;
+    samples: Array<IKTX2_Sample>;
+}
+
+/** @hidden */
+export interface IKTX2_ImageDesc {
+    imageFlags: number;
+    rgbSliceByteOffset: number;
+    rgbSliceByteLength: number;
+    alphaSliceByteOffset: number;
+    alphaSliceByteLength: number;
+}
+
+/** @hidden */
+export interface IKTX2_SupercompressionGlobalData {
+    endpointCount?: number;
+    selectorCount?: number;
+    endpointsByteLength?: number;
+    selectorsByteLength?: number;
+    tablesByteLength?: number;
+    extendedByteLength?: number;
+    imageDescs?: Array<IKTX2_ImageDesc>;
+    endpointsData?: Uint8Array;
+    selectorsData?: Uint8Array;
+    tablesData?: Uint8Array;
+    extendedData?: Uint8Array;
+}
+
+export class KTX2FileReader {
+
+    private _data: Uint8Array;
+    private _header: IKTX2_Header;
+    private _levels: Array<IKTX2_Level>;
+    private _dfdBlock: IKTX2_DFD;
+    private _supercompressionGlobalData: IKTX2_SupercompressionGlobalData;
+
+    /**
+     * Will throw an exception if the file can't be parsed
+     */
+    constructor(data: Uint8Array) {
+        this._data = data;
+    }
+
+    public get data(): Uint8Array {
+        return this._data;
+    }
+
+    public get header(): IKTX2_Header {
+        return this._header;
+    }
+
+    public get levels(): Array<IKTX2_Level> {
+        return this._levels;
+    }
+
+    public get dfdBlock(): IKTX2_DFD {
+        return this._dfdBlock;
+    }
+
+    public get supercompressionGlobalData(): IKTX2_SupercompressionGlobalData {
+        return this._supercompressionGlobalData;
+    }
+
+    public isValid() {
+        return KTX2FileReader.IsValid(this._data);
+    }
+
+    public parse() {
+        let offsetInFile = 12; // skip the header
+
+        /**
+         * Get the header
+         */
+        const hdrReader = new DataReader(this._data, offsetInFile, 17 * 4);
+
+        const header = this._header = {
+            vkFormat:               hdrReader.readUint32(),
+            typeSize:               hdrReader.readUint32(),
+            pixelWidth:             hdrReader.readUint32(),
+            pixelHeight:            hdrReader.readUint32(),
+            pixelDepth:             hdrReader.readUint32(),
+            layerCount:             hdrReader.readUint32(),
+            faceCount:              hdrReader.readUint32(),
+            levelCount:             hdrReader.readUint32(),
+            supercompressionScheme: hdrReader.readUint32(),
+
+            dfdByteOffset:          hdrReader.readUint32(),
+            dfdByteLength:          hdrReader.readUint32(),
+            kvdByteOffset:          hdrReader.readUint32(),
+            kvdByteLength:          hdrReader.readUint32(),
+            sgdByteOffset:          hdrReader.readUint64(),
+            sgdByteLength:          hdrReader.readUint64(),
+        };
+
+        if (header.pixelDepth > 0) {
+            throw new Error(`Failed to parse KTX2 file - Only 2D textures are currently supported.`);
+        }
+
+        if (header.layerCount > 1) {
+            throw new Error(`Failed to parse KTX2 file - Array textures are not currently supported.`);
+        }
+
+        if (header.faceCount > 1) {
+            throw new Error(`Failed to parse KTX2 file - Cube textures are not currently supported.`);
+        }
+
+        offsetInFile += hdrReader.byteOffset;
+
+        /**
+         * Get the levels
+         */
+        let levelCount = Math.max(1, header.levelCount);
+
+        const levelReader = new DataReader(this._data, offsetInFile, levelCount * 3 * (2 * 4));
+
+        const levels: Array<IKTX2_Level> = this._levels = [];
+
+        while (levelCount--) {
+            levels.push({
+                byteOffset: levelReader.readUint64(),
+                byteLength: levelReader.readUint64(),
+                uncompressedByteLength: levelReader.readUint64(),
+            });
+        }
+
+        offsetInFile += levelReader.byteOffset;
+
+        /**
+         * Get the data format descriptor (DFD) blocks
+         */
+        const dfdReader = new DataReader(this._data, header.dfdByteOffset, header.dfdByteLength);
+
+        const dfdBlock = this._dfdBlock = {
+            vendorId: dfdReader.skipBytes(4 /* skip totalSize */).readUint16(),
+            descriptorType: dfdReader.readUint16(),
+            versionNumber: dfdReader.readUint16(),
+            descriptorBlockSize: dfdReader.readUint16(),
+            colorModel: dfdReader.readUint8(),
+            colorPrimaries: dfdReader.readUint8(),
+            transferFunction: dfdReader.readUint8(),
+            flags: dfdReader.readUint8(),
+            texelBlockDimension: {
+                x: dfdReader.readUint8() + 1,
+                y: dfdReader.readUint8() + 1,
+                z: dfdReader.readUint8() + 1,
+                w: dfdReader.readUint8() + 1,
+            },
+            bytesPlane: [
+                dfdReader.readUint8(), /* bytesPlane0 */
+                dfdReader.readUint8(), /* bytesPlane1 */
+                dfdReader.readUint8(), /* bytesPlane2 */
+                dfdReader.readUint8(), /* bytesPlane3 */
+                dfdReader.readUint8(), /* bytesPlane4 */
+                dfdReader.readUint8(), /* bytesPlane5 */
+                dfdReader.readUint8(), /* bytesPlane6 */
+                dfdReader.readUint8(), /* bytesPlane7 */
+            ],
+            numSamples: 0,
+            samples: new Array<IKTX2_Sample>(),
+        };
+
+        dfdBlock.numSamples = (dfdBlock.descriptorBlockSize - 24) / 16;
+
+        for (let i = 0; i < dfdBlock.numSamples; i++) {
+            const sample = {
+                bitOffset: dfdReader.readUint16(),
+                bitLength: dfdReader.readUint8() + 1,
+                channelType: dfdReader.readUint8(),
+                channelFlags: 0,
+                samplePosition: [
+                    dfdReader.readUint8(), /* samplePosition0 */
+                    dfdReader.readUint8(), /* samplePosition1 */
+                    dfdReader.readUint8(), /* samplePosition2 */
+                    dfdReader.readUint8(), /* samplePosition3 */
+                ],
+                sampleLower: dfdReader.readUint32(),
+                sampleUpper: dfdReader.readUint32(),
+            };
+
+            sample.channelFlags = (sample.channelType & 0xF0) >> 4;
+            sample.channelType = sample.channelType & 0x0F;
+
+            dfdBlock.samples.push(sample);
+        }
+
+        /**
+         * Get the Supercompression Global Data (sgd)
+         */
+        const sgd: IKTX2_SupercompressionGlobalData = this._supercompressionGlobalData = {};
+
+        if (header.sgdByteLength > 0) {
+            const sgdReader = new DataReader(this._data, header.sgdByteOffset, header.sgdByteLength);
+
+            sgd.endpointCount = sgdReader.readUint16();
+            sgd.selectorCount = sgdReader.readUint16();
+            sgd.endpointsByteLength = sgdReader.readUint32();
+            sgd.selectorsByteLength = sgdReader.readUint32();
+            sgd.tablesByteLength = sgdReader.readUint32();
+            sgd.extendedByteLength = sgdReader.readUint32();
+            sgd.imageDescs = [];
+
+            const imageCount = this._getImageCount();
+
+            for (let i = 0; i < imageCount; i ++) {
+                sgd.imageDescs.push({
+                    imageFlags: sgdReader.readUint32(),
+                    rgbSliceByteOffset: sgdReader.readUint32(),
+                    rgbSliceByteLength: sgdReader.readUint32(),
+                    alphaSliceByteOffset: sgdReader.readUint32(),
+                    alphaSliceByteLength: sgdReader.readUint32(),
+                });
+            }
+
+            const endpointsByteOffset = header.sgdByteOffset + sgdReader.byteOffset;
+            const selectorsByteOffset = endpointsByteOffset + sgd.endpointsByteLength;
+            const tablesByteOffset = selectorsByteOffset + sgd.selectorsByteLength;
+            const extendedByteOffset = tablesByteOffset + sgd.tablesByteLength;
+
+            sgd.endpointsData = new Uint8Array(this._data.buffer, this._data.byteOffset + endpointsByteOffset, sgd.endpointsByteLength);
+            sgd.selectorsData = new Uint8Array(this._data.buffer, this._data.byteOffset + selectorsByteOffset, sgd.selectorsByteLength);
+            sgd.tablesData = new Uint8Array(this._data.buffer, this._data.byteOffset + tablesByteOffset, sgd.tablesByteLength);
+            sgd.extendedData = new Uint8Array(this._data.buffer, this._data.byteOffset + extendedByteOffset, sgd.extendedByteLength);
+        }
+
+    }
+
+    private _getImageCount(): number {
+        let layerPixelDepth = Math.max(this._header.pixelDepth, 1);
+        for (let i = 1; i < this._header.levelCount; i++) {
+            layerPixelDepth += Math.max(this._header.pixelDepth >> i, 1);
+        }
+
+        return Math.max(this._header.layerCount, 1) * this._header.faceCount * layerPixelDepth;
+    }
+
+    public get textureFormat(): sourceTextureFormat {
+        return this._dfdBlock.colorModel === DFDModel.UASTC ? sourceTextureFormat.UASTC4x4 : sourceTextureFormat.ETC1S;
+    }
+
+    public get hasAlpha(): boolean {
+        const tformat = this.textureFormat;
+
+        switch (tformat) {
+            case sourceTextureFormat.ETC1S:
+                return this._dfdBlock.numSamples === 2 && (this._dfdBlock.samples[0].channelType === DFDChannel_ETC1S.AAA || this._dfdBlock.samples[1].channelType === DFDChannel_ETC1S.AAA);
+
+            case sourceTextureFormat.UASTC4x4:
+                return this._dfdBlock.samples[0].channelType === DFDChannel_UASTC.RGBA;
+        }
+
+        return false;
+    }
+
+    public get needZSTDDecoder(): boolean {
+        return this._header.supercompressionScheme === SupercompressionScheme.ZStandard;
+    }
+
+    public get isInGammaSpace(): boolean {
+        return this._dfdBlock.transferFunction === DFDTransferFunction.sRGB;
+    }
+
+    public static IsValid(data: ArrayBufferView): boolean {
+        if (data.byteLength >= 12) {
+            // '«', 'K', 'T', 'X', ' ', '2', '0', '»', '\r', '\n', '\x1A', '\n'
+            const identifier = new Uint8Array(data.buffer, data.byteOffset, 12);
+            if (identifier[0] === 0xAB && identifier[1] === 0x4B && identifier[2] === 0x54 && identifier[3] === 0x58 && identifier[4] === 0x20 && identifier[5] === 0x32 &&
+                identifier[6] === 0x30 && identifier[7] === 0xBB && identifier[8] === 0x0D && identifier[9] === 0x0A && identifier[10] === 0x1A && identifier[11] === 0x0A) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}

+ 8 - 0
ktx2Decoder/src/legacy/legacy.ts

@@ -0,0 +1,8 @@
+import { KTX2Decoder } from "../index";
+
+var globalObject = (typeof global !== 'undefined') ? global : ((typeof window !== 'undefined') ? window : undefined);
+if (typeof globalObject !== "undefined") {
+    (<any>globalObject).KTX2DECODER = KTX2Decoder;
+}
+
+export * from "../index";

+ 49 - 0
ktx2Decoder/src/transcoder.ts

@@ -0,0 +1,49 @@
+import { WASMMemoryManager } from './wasmMemoryManager';
+import { KTX2FileReader, IKTX2_ImageDesc } from './ktx2FileReader';
+
+/**
+ * @hidden
+ */
+export enum sourceTextureFormat {
+    ETC1S,
+    UASTC4x4
+}
+
+/**
+ * @hidden
+ */
+export enum transcodeTarget {
+    ASTC_4x4_RGBA,
+    BC7_RGBA,
+    BC3_RGBA,
+    BC1_RGB,
+    PVRTC1_4_RGBA,
+    PVRTC1_4_RGB,
+    ETC2_RGBA,
+    ETC1_RGB,
+    RGBA32
+}
+
+/**
+ * @hidden
+ */
+export class Transcoder {
+
+    public static CanTranscode(src: sourceTextureFormat, dst: transcodeTarget): boolean {
+        return false;
+    }
+
+    public initialize(): void {
+    }
+
+    public needMemoryManager(): boolean {
+        return false;
+    }
+
+    public setMemoryManager(memoryMgr: WASMMemoryManager): void {
+    }
+
+    public transcode(src: sourceTextureFormat, dst: transcodeTarget, level: number, width: number, height: number, uncompressedByteLength: number, ktx2Reader: KTX2FileReader, imageDesc: IKTX2_ImageDesc | null, encodedData: Uint8Array): Promise<Uint8Array | null> {
+        return Promise.resolve(null);
+    }
+}

+ 43 - 0
ktx2Decoder/src/transcoderManager.ts

@@ -0,0 +1,43 @@
+import { transcodeTarget, sourceTextureFormat, Transcoder } from './transcoder';
+import { WASMMemoryManager } from './wasmMemoryManager';
+
+/**
+ * @hidden
+ */
+export class TranscoderManager {
+
+    public static _Transcoders: Array<typeof Transcoder> = [];
+
+    public static RegisterTranscoder(transcoder: typeof Transcoder) {
+        TranscoderManager._Transcoders.push(transcoder);
+    }
+
+    private static _transcoderInstances: { [key: string]: Transcoder } = {};
+
+    private _wasmMemoryManager: WASMMemoryManager;
+
+    public findTranscoder(src: sourceTextureFormat, dst: transcodeTarget): Transcoder | null {
+        let transcoder: Transcoder | null = null;
+
+        for (let i = 0; i < TranscoderManager._Transcoders.length; ++i) {
+            if (TranscoderManager._Transcoders[i].CanTranscode(src, dst)) {
+                const key = sourceTextureFormat[src] + "_" + transcodeTarget[dst];
+                transcoder = TranscoderManager._transcoderInstances[key];
+                if (!transcoder) {
+                    transcoder = new TranscoderManager._Transcoders[i]();
+                    transcoder!.initialize();
+                    if (transcoder!.needMemoryManager()) {
+                        if (!this._wasmMemoryManager) {
+                            this._wasmMemoryManager = new WASMMemoryManager();
+                        }
+                        transcoder!.setMemoryManager(this._wasmMemoryManager);
+                    }
+                    TranscoderManager._transcoderInstances[key] = transcoder;
+                }
+                break;
+            }
+        }
+
+        return transcoder;
+    }
+}

+ 82 - 0
ktx2Decoder/src/wasmMemoryManager.ts

@@ -0,0 +1,82 @@
+declare function postMessage(message: any, transfer?: any[]): void;
+
+/**
+ * @hidden
+ */
+export class WASMMemoryManager {
+
+    public static LoadBinariesFromCurrentThread = true;
+    public static InitialMemoryPages = (1 * 1024 * 1024) >> 16; // 1 Mbytes
+
+    private static _RequestId = 0;
+
+    public static LoadWASM(path: string): Promise<ArrayBuffer> {
+        if (this.LoadBinariesFromCurrentThread) {
+            return new Promise((resolve, reject) => {
+                fetch(path)
+                .then((response) => {
+                    if (response.ok) {
+                        return response.arrayBuffer();
+                    }
+                    throw new Error(`Could not fetch the wasm component from "${path}": ${response.status} - ${response.statusText}`);
+                })
+                .then((wasmBinary) => resolve(wasmBinary))
+                .catch((reason) => {
+                    reject(reason);
+                });
+            });
+        }
+
+        const id = this._RequestId++;
+
+        return new Promise((resolve) => {
+            const wasmLoadedHandler = (msg: any) => {
+                if (msg.data.action === "wasmLoaded" && msg.data.id === id) {
+                    self.removeEventListener("message", wasmLoadedHandler);
+                    resolve(msg.data.wasmBinary);
+                }
+            };
+
+            self.addEventListener("message", wasmLoadedHandler);
+
+            postMessage({ action: "loadWASM", path: path, id: id });
+        });
+    }
+
+    private _memory: WebAssembly.Memory;
+    private _numPages: number;
+    private _memoryView: Uint8Array;
+    private _memoryViewByteLength: number;
+    private _memoryViewOffset: number;
+
+    constructor(initialMemoryPages: number = WASMMemoryManager.InitialMemoryPages) {
+        this._numPages = initialMemoryPages;
+
+        this._memory = new WebAssembly.Memory({ initial: this._numPages });
+        this._memoryViewByteLength = this._numPages << 16;
+        this._memoryViewOffset = 0;
+        this._memoryView = new Uint8Array(this._memory.buffer, this._memoryViewOffset, this._memoryViewByteLength);
+    }
+
+    public get wasmMemory(): WebAssembly.Memory {
+        return this._memory;
+    }
+
+    public getMemoryView(numPages: number, offset: number = 0, byteLength?: number): Uint8Array {
+        byteLength = byteLength ?? numPages << 16;
+
+        if (this._numPages < numPages) {
+            this._memory.grow(numPages - this._numPages);
+            this._numPages = numPages;
+            this._memoryView = new Uint8Array(this._memory.buffer, offset, byteLength);
+            this._memoryViewByteLength = byteLength;
+            this._memoryViewOffset = offset;
+        } else {
+            this._memoryView = new Uint8Array(this._memory.buffer, offset, byteLength);
+            this._memoryViewByteLength = byteLength;
+            this._memoryViewOffset = offset;
+        }
+
+        return this._memoryView;
+    }
+}

+ 134 - 0
ktx2Decoder/src/zstddec.ts

@@ -0,0 +1,134 @@
+/**
+ * From https://github.com/donmccurdy/zstddec by Don McCurdy
+ */
+interface DecoderExports {
+    memory: Uint8Array;
+
+    ZSTD_findDecompressedSize: (compressedPtr: number, compressedSize: number) => number;
+    ZSTD_decompress: (uncompressedPtr: number, uncompressedSize: number, compressedPtr: number, compressedSize: number) => number;
+    malloc: (ptr: number) => number;
+    free: (ptr: number) => void;
+}
+
+let init: Promise<void>;
+let instance: {exports: DecoderExports};
+let heap: Uint8Array;
+
+const IMPORT_OBJECT = {
+
+    env: {
+
+        emscripten_notify_memory_growth: function (index: number): void {
+
+            heap = new Uint8Array(instance.exports.memory.buffer);
+
+        }
+
+    }
+
+};
+
+/**
+ * ZSTD (Zstandard) decoder.
+ */
+export class ZSTDDecoder {
+
+    public static WasmModuleURL = "https://preview.babylonjs.com/zstddec.wasm";
+
+    init (): Promise<void> {
+
+        if (init) { return init; }
+
+        if (typeof fetch !== 'undefined') {
+
+            // Web.
+
+            init = fetch(ZSTDDecoder.WasmModuleURL)
+                .then((response) => {
+                    if (response.ok) {
+                        return response.arrayBuffer();
+                    }
+                    throw new Error(`Could not fetch the wasm component for the Zstandard decompression lib: ${response.status} - ${response.statusText}`);
+                })
+                .then((arrayBuffer) => WebAssembly.instantiate(arrayBuffer, IMPORT_OBJECT))
+                .then(this._init);
+
+        } else {
+
+            // Node.js.
+
+            init = WebAssembly
+                .instantiateStreaming(fetch(ZSTDDecoder.WasmModuleURL), IMPORT_OBJECT)
+                .then(this._init);
+
+        }
+
+        return init;
+
+    }
+
+    _init (result: WebAssembly.WebAssemblyInstantiatedSource): void {
+
+        instance = result.instance as unknown as { exports: DecoderExports };
+
+        IMPORT_OBJECT.env.emscripten_notify_memory_growth(0); // initialize heap.
+
+    }
+
+    decode (array: Uint8Array, uncompressedSize = 0): Uint8Array {
+
+        if (! instance) { throw new Error(`ZSTDDecoder: Await .init() before decoding.`); }
+
+        // Write compressed data into WASM memory.
+        const compressedSize = array.byteLength;
+        const compressedPtr = instance.exports.malloc(compressedSize);
+        heap.set(array, compressedPtr);
+
+        // Decompress into WASM memory.
+        uncompressedSize = uncompressedSize || Number(instance.exports.ZSTD_findDecompressedSize(compressedPtr, compressedSize));
+        const uncompressedPtr = instance.exports.malloc(uncompressedSize);
+        const actualSize = instance.exports.ZSTD_decompress(uncompressedPtr, uncompressedSize, compressedPtr, compressedSize);
+
+        // Read decompressed data and free WASM memory.
+        const dec = heap.slice(uncompressedPtr, uncompressedPtr + actualSize);
+        instance.exports.free(compressedPtr);
+        instance.exports.free(uncompressedPtr);
+
+        return dec;
+
+    }
+
+}
+
+/**
+ * BSD License
+ *
+ * For Zstandard software
+ *
+ * Copyright (c) 2016-present, Yann Collet, Facebook, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ *  * Neither the name Facebook nor the names of its contributors may be used to
+ *    endorse or promote products derived from this software without specific
+ *    prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */

+ 9 - 0
ktx2Decoder/tsconfig.json

@@ -0,0 +1,9 @@
+{
+    "extends": "../tsconfigRules",
+    "compilerOptions": {
+        "baseUrl": "./src/",
+        "rootDir": "./src/",
+        "paths": {
+        }
+    }
+}

+ 58 - 0
ktx2Decoder/webpack.config.js

@@ -0,0 +1,58 @@
+const path = require("path");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const babylonWebpackConfig = require('../Tools/WebpackPlugins/babylonWebpackConfig');
+
+var config = babylonWebpackConfig({
+    module: "ktx2Decoder",
+    entry: "./legacy/legacy.ts",
+    output: {
+        globalObject: '(typeof self !== "undefined" ? self : typeof global !== "undefined" ? global : this)',
+        filename: "babylon.ktx2Decoder.js",
+        path: path.resolve(__dirname, "../dist/preview release"),
+        publicPath: "/dist/",
+        libraryTarget: 'umd',
+        library: {
+           root: ["KTX2DECODER"],
+        },
+        umdNamedDefine: true
+    },
+    resolve: {
+        extensions: [".js", '.ts', ".tsx"],
+    },
+    moduleRules: [
+        {
+            test: /\.scss$/,
+            use: [
+                // fallback to style-loader in development
+                process.env.NODE_ENV !== 'production' ? 'style-loader' : MiniCssExtractPlugin.loader,
+                "css-loader",
+                "sass-loader"
+            ]
+        }, 
+        {
+            test: /\.css$/,
+            use: ['style-loader', 'css-loader']
+        },
+        {
+            test: /\.svg$/,
+            use: [
+              {
+                loader: 'svg-url-loader',
+                options: {
+                  limit: 10000,
+                },
+              },
+            ],
+          }
+    ],
+    plugins: [
+        new MiniCssExtractPlugin({
+            // Options similar to the same options in webpackOptions.output
+            // both options are optional
+            filename: "[name].css",
+            chunkFilename: "[id].css"
+        })
+    ]
+});
+
+module.exports = config;

+ 0 - 1
loaders/src/glTF/2.0/Extensions/KHR_texture_basisu.ts

@@ -37,7 +37,6 @@ export class KHR_texture_basisu implements IGLTFLoaderExtension {
             const sampler = (texture.sampler == undefined ? GLTFLoader.DefaultSampler : ArrayItem.Get(`${context}/sampler`, this._loader.gltf.samplers, texture.sampler));
             const image = ArrayItem.Get(`${extensionContext}/source`, this._loader.gltf.images, extension.source);
             return this._loader._createTextureAsync(context, sampler, image, (babylonTexture) => {
-                babylonTexture.gammaSpace = false;
                 assign(babylonTexture);
             });
         });

+ 0 - 1
localDev/index-views.html

@@ -10,7 +10,6 @@
     <script src="../dist/preview%20release/Oimo.js"></script>
     <script src="../dist/preview%20release/ammo.js"></script>
     <script src="../dist/preview%20release/recast.js"></script>
-    <script src="../dist/preview%20release/libktx.js"></script>
     <script src="../Tools/DevLoader/BabylonLoader.js"></script>
 
     <style>

+ 0 - 1
localDev/index.html

@@ -10,7 +10,6 @@
     <script src="../dist/preview%20release/Oimo.js"></script>
     <script src="../dist/preview%20release/ammo.js"></script>
     <script src="../dist/preview%20release/recast.js"></script>
-    <script src="../dist/preview%20release/libktx.js"></script>
     <script src="../Tools/DevLoader/BabylonLoader.js"></script>
 
     <style>

+ 0 - 1
sandbox/public/index-local.html

@@ -9,7 +9,6 @@
     <link rel="stylesheet" href="https://use.typekit.net/cta4xsb.css">
     <link rel="shortcut icon" href="https://www.babylonjs.com/favicon.ico">
     <script src="https://code.jquery.com/pep/0.4.2/pep.min.js"></script>
-    <script src="../../dist/preview%20release/libktx.js"></script>
     <script src="../../Tools/DevLoader/BabylonLoader.js"></script>
     <style>
         html,

+ 0 - 1
sandbox/public/index.html

@@ -14,7 +14,6 @@
     <script src="https://preview.babylonjs.com/ammo.js"></script>
     <script src="https://preview.babylonjs.com/cannon.js"></script>
     <script src="https://preview.babylonjs.com/Oimo.js"></script>
-    <script src="https://preview.babylonjs.com/libktx.js"></script>
     <script src="https://preview.babylonjs.com/babylon.js"></script>
 
     <script src="https://preview.babylonjs.com/loaders/babylonjs.loaders.min.js"></script>

+ 2 - 0
src/Engines/engineCapabilities.ts

@@ -36,6 +36,8 @@ export interface EngineCapabilities {
     etc2: any; //WEBGL_compressed_texture_etc;
     /** Defines if astc texture compression is supported */
     astc: any; //WEBGL_compressed_texture_astc;
+    /** Defines if bptc texture compression is supported */
+    bptc: any; //EXT_texture_compression_bptc;
     /** Defines if float textures are supported */
     textureFloat: boolean;
     /** Defines if vertex array objects are supported */

+ 1 - 0
src/Engines/nativeEngine.ts

@@ -235,6 +235,7 @@ export class NativeEngine extends Engine {
             pvrtc: null,
             etc1: null,
             etc2: null,
+            bptc: null,
             maxAnisotropy: 16,  // TODO: Retrieve this smartly. Currently set to D3D11 maximum allowable value.
             uintIndices: true,
             fragmentDepthSupported: false,

+ 1 - 0
src/Engines/nullEngine.ts

@@ -121,6 +121,7 @@ export class NullEngine extends Engine {
             pvrtc: null,
             etc1: null,
             etc2: null,
+            bptc: null,
             maxAnisotropy: 0,
             uintIndices: false,
             fragmentDepthSupported: false,

+ 1 - 0
src/Engines/thinEngine.ts

@@ -777,6 +777,7 @@ export class ThinEngine {
             standardDerivatives: this._webGLVersion > 1 || (this._gl.getExtension('OES_standard_derivatives') !== null),
             maxAnisotropy: 1,
             astc: this._gl.getExtension('WEBGL_compressed_texture_astc') || this._gl.getExtension('WEBKIT_WEBGL_compressed_texture_astc'),
+            bptc: this._gl.getExtension('EXT_texture_compression_bptc') || this._gl.getExtension('WEBKIT_EXT_texture_compression_bptc'),
             s3tc: this._gl.getExtension('WEBGL_compressed_texture_s3tc') || this._gl.getExtension('WEBKIT_WEBGL_compressed_texture_s3tc'),
             pvrtc: this._gl.getExtension('WEBGL_compressed_texture_pvrtc') || this._gl.getExtension('WEBKIT_WEBGL_compressed_texture_pvrtc'),
             etc1: this._gl.getExtension('WEBGL_compressed_texture_etc1') || this._gl.getExtension('WEBKIT_WEBGL_compressed_texture_etc1'),

+ 1 - 1
src/Materials/Textures/Loaders/ktxTextureLoader.ts

@@ -84,7 +84,7 @@ export class _KTXTextureLoader implements IInternalTextureLoader {
         else if (KhronosTextureContainer2.IsValid(data)) {
             const ktx2 = new KhronosTextureContainer2(texture.getEngine());
             ktx2.uploadAsync(data, texture).then(() => {
-                callback(texture.width, texture.height, false, true, () => {}, false);
+                callback(texture.width, texture.height, texture.generateMipMaps, true, () => {}, false);
             }, (error) => {
                 Logger.Warn(`Failed to load KTX2 texture data: ${error.message}`);
                 callback(0, 0, false, false, () => {}, true);

+ 30 - 3
src/Materials/Textures/baseTexture.ts

@@ -1,4 +1,4 @@
-import { serialize, SerializationHelper, serializeAsTexture, expandToProperty } from "../../Misc/decorators";
+import { serialize, SerializationHelper, serializeAsTexture } from "../../Misc/decorators";
 import { Observer, Observable } from "../../Misc/observable";
 import { Nullable } from "../../types";
 import { Scene } from "../../scene";
@@ -233,14 +233,41 @@ export class BaseTexture implements IAnimatable {
         this._texture.is2DArray = value;
     }
 
+    private _gammaSpace = true;
     /**
      * Define if the texture contains data in gamma space (most of the png/jpg aside bump).
      * HDR texture are usually stored in linear space.
      * This only impacts the PBR and Background materials
      */
     @serialize()
-    @expandToProperty("_markAllSubMeshesAsTexturesDirty")
-    public gammaSpace = true;
+    public get gammaSpace(): boolean {
+        if (!this._texture) {
+            return this._gammaSpace;
+        } else {
+            if (this._texture._gammaSpace === null) {
+                this._texture._gammaSpace = this._gammaSpace;
+            }
+        }
+
+        return this._texture._gammaSpace;
+    }
+
+    public set gammaSpace(gamma: boolean) {
+        if (!this._texture) {
+            if (this._gammaSpace === gamma) {
+                return;
+            }
+
+            this._gammaSpace = gamma;
+        } else {
+            if (this._texture._gammaSpace === gamma) {
+                return;
+            }
+            this._texture._gammaSpace = gamma;
+        }
+
+        this._markAllSubMeshesAsTexturesDirty();
+    }
 
     /**
      * Gets or sets whether or not the texture contains RGBD data.

+ 3 - 0
src/Materials/Textures/internalTexture.ts

@@ -253,6 +253,9 @@ export class InternalTexture {
     /** @hidden */
     public _references: number = 1;
 
+    /** @hidden */
+    public _gammaSpace: Nullable<boolean> = null;
+
     private _engine: ThinEngine;
 
     /**

+ 1 - 0
src/Misc/index.ts

@@ -49,3 +49,4 @@ export * from "./minMaxReducer";
 export * from "./depthReducer";
 export * from "./dataStorage";
 export * from "./sceneRecorder";
+export * from "./khronosTextureContainer2";

+ 221 - 42
src/Misc/khronosTextureContainer2.ts

@@ -1,69 +1,216 @@
 import { InternalTexture } from "../Materials/Textures/internalTexture";
 import { ThinEngine } from "../Engines/thinEngine";
-import { EngineCapabilities } from '../Engines/engineCapabilities';
+import { Constants } from '../Engines/constants';
+import { WorkerPool } from './workerPool';
 
-declare var LIBKTX: any;
+declare var KTX2DECODER: any;
 
 /**
  * Class for loading KTX2 files
- * !!! Experimental Extension Subject to Changes !!!
  * @hidden
  */
 export class KhronosTextureContainer2 {
-    private static _ModulePromise: Promise<{ module: any }>;
-    private static _TranscodeFormat: number;
-
-    public constructor(engine: ThinEngine) {
-        if (!KhronosTextureContainer2._ModulePromise) {
-            KhronosTextureContainer2._ModulePromise = new Promise((resolve) => {
-                LIBKTX({preinitializedWebGLContext: engine._gl}).then((module: any) => {
-                    module.GL.makeContextCurrent(module.GL.registerContext(engine._gl, { majorVersion: engine._webGLVersion }));
-                    KhronosTextureContainer2._TranscodeFormat = this._determineTranscodeFormat(module.TranscodeTarget, engine.getCaps());
-                    resolve({ module: module });
+    private static _WorkerPoolPromise?: Promise<WorkerPool>;
+    private static _Initialized: boolean;
+    private static _Ktx2Decoder: any; // used when no worker pool is used
+
+    /**
+     * URL to use when loading the KTX2 decoder module
+     */
+    public static JSModuleURL = "https://preview.babylonjs.com/babylon.ktx2Decoder.js";
+
+    /**
+     * Default number of workers used to handle data decoding
+     */
+    public static DefaultNumWorkers = KhronosTextureContainer2.GetDefaultNumWorkers();
+
+    private static GetDefaultNumWorkers(): number {
+        if (typeof navigator !== "object" || !navigator.hardwareConcurrency) {
+            return 1;
+        }
+
+        // Use 50% of the available logical processors but capped at 4.
+        return Math.min(Math.floor(navigator.hardwareConcurrency * 0.5), 4);
+    }
+
+    private _engine: ThinEngine;
+
+    private static _CreateWorkerPool(numWorkers: number) {
+        this._Initialized = true;
+
+        if (numWorkers && typeof Worker === "function") {
+            KhronosTextureContainer2._WorkerPoolPromise = new Promise((resolve) => {
+                const workerContent = `(${workerFunc})()`;
+                const workerBlobUrl = URL.createObjectURL(new Blob([workerContent], { type: "application/javascript" }));
+                const workerPromises = new Array<Promise<Worker>>(numWorkers);
+                for (let i = 0; i < workerPromises.length; i++) {
+                    workerPromises[i] = new Promise((resolve, reject) => {
+                        const worker = new Worker(workerBlobUrl);
+
+                        const onError = (error: ErrorEvent) => {
+                            worker.removeEventListener("error", onError);
+                            worker.removeEventListener("message", onMessage);
+                            reject(error);
+                        };
+
+                        const onMessage = (message: MessageEvent) => {
+                            if (message.data.action === "init") {
+                                worker.removeEventListener("error", onError);
+                                worker.removeEventListener("message", onMessage);
+                                resolve(worker);
+                            }
+                        };
+
+                        worker.addEventListener("error", onError);
+                        worker.addEventListener("message", onMessage);
+
+                        worker.postMessage({
+                            action: "init",
+                            jsPath: KhronosTextureContainer2.JSModuleURL
+                        });
+                    });
+                }
+
+                Promise.all(workerPromises).then((workers) => {
+                    resolve(new WorkerPool(workers));
                 });
             });
+        } else {
+            KTX2DECODER.MSCTranscoder.UseFromWorkerThread = false;
+            KTX2DECODER.WASMMemoryManager.LoadBinariesFromCurrentThread = true;
+        }
+    }
+
+    /**
+     * Constructor
+     * @param numWorkers The number of workers for async operations. Specify `0` to disable web workers and run synchronously in the current context.
+     */
+    public constructor(engine: ThinEngine, numWorkers = KhronosTextureContainer2.DefaultNumWorkers) {
+        this._engine = engine;
+
+        if (!KhronosTextureContainer2._Initialized) {
+            KhronosTextureContainer2._CreateWorkerPool(numWorkers);
         }
     }
 
     public uploadAsync(data: ArrayBufferView, internalTexture: InternalTexture): Promise<void> {
-        return KhronosTextureContainer2._ModulePromise.then((moduleWrapper: any) => {
-            const module = moduleWrapper.module;
+        const caps = this._engine.getCaps();
 
-            const ktxTexture = new module.ktxTexture(data);
-            try {
-                if (ktxTexture.needsTranscoding) {
-                    ktxTexture.transcodeBasis(KhronosTextureContainer2._TranscodeFormat, 0);
-                }
+        const compressedTexturesCaps = {
+            astc: !!caps.astc,
+            bptc: !!caps.bptc,
+            s3tc: !!caps.s3tc,
+            pvrtc: !!caps.pvrtc,
+            etc2: !!caps.etc2,
+            etc1: !!caps.etc1,
+        };
 
-                internalTexture.width = internalTexture.baseWidth = ktxTexture.baseWidth;
-                internalTexture.height = internalTexture.baseHeight = ktxTexture.baseHeight;
-                internalTexture.generateMipMaps = false;
+        if (KhronosTextureContainer2._WorkerPoolPromise) {
+            return KhronosTextureContainer2._WorkerPoolPromise.then((workerPool) => {
+                return new Promise((resolve, reject) => {
+                    workerPool.push((worker, onComplete) => {
+                        const onError = (error: ErrorEvent) => {
+                            worker.removeEventListener("error", onError);
+                            worker.removeEventListener("message", onMessage);
+                            reject(error);
+                            onComplete();
+                        };
 
-                const result = ktxTexture.glUpload();
-                if (result.error === 0) {
-                    internalTexture._webGLTexture = result.texture;
-                }
-                else {
-                    throw new Error(`Failed to upload: ${result.error}`);
-                }
+                        const onMessage = (message: MessageEvent) => {
+                            if (message.data.action === "decoded") {
+                                worker.removeEventListener("error", onError);
+                                worker.removeEventListener("message", onMessage);
+                                if (!message.data.success) {
+                                    reject({ message: message.data.msg });
+                                } else {
+                                    try {
+                                        this._createTexture(message.data.decodedData, internalTexture);
+                                        resolve();
+                                    } catch (err) {
+                                        reject({ message: err });
+                                    }
+                                }
+                                onComplete();
+                            }
+                        };
 
-                internalTexture.isReady = true;
-            }
-            finally {
-                ktxTexture.delete();
+                        worker.addEventListener("error", onError);
+                        worker.addEventListener("message", onMessage);
+
+                        // note: we can't transfer the ownership of data.buffer because if using a fallback texture the data.buffer buffer will be used by the current thread
+                        worker.postMessage({ action: "decode", data, caps: compressedTexturesCaps }/*, [data.buffer]*/);
+                    });
+                });
+            });
+        }
+
+        return new Promise((resolve, reject) => {
+            if (!KhronosTextureContainer2._Ktx2Decoder) {
+                KhronosTextureContainer2._Ktx2Decoder = new KTX2DECODER.KTX2Decoder();
             }
+
+            KhronosTextureContainer2._Ktx2Decoder.decode(data, caps).then((data: any) => {
+                this._createTexture(data, internalTexture);
+                resolve();
+            }).catch((reason: any) => {
+                reject({ message: reason });
+            });
         });
     }
 
-    private _determineTranscodeFormat(transcodeTarget: any, caps: EngineCapabilities): number {
-        if (caps.s3tc) {
-            return transcodeTarget.BC1_OR_3;
+    /**
+     * Stop all async operations and release resources.
+     */
+    public dispose(): void {
+        if (KhronosTextureContainer2._WorkerPoolPromise) {
+            KhronosTextureContainer2._WorkerPoolPromise.then((workerPool) => {
+                workerPool.dispose();
+            });
+        }
+
+        delete KhronosTextureContainer2._WorkerPoolPromise;
+    }
+
+    protected _createTexture(data: any /* IEncodedData */, internalTexture: InternalTexture) {
+        this._engine._bindTextureDirectly(this._engine._gl.TEXTURE_2D, internalTexture);
+
+        if (data.transcodedFormat === 0x8058 /* RGBA8 */) {
+            internalTexture.type = Constants.TEXTURETYPE_UNSIGNED_BYTE;
+            internalTexture.format = Constants.TEXTUREFORMAT_RGBA;
+        } else {
+            internalTexture.format = data.transcodedFormat;
+        }
+
+        internalTexture._gammaSpace = data.isInGammaSpace;
+
+        if (data.errors) {
+            throw new Error("KTX2 container - could not transcode the data. " + data.errors);
         }
-        else if (caps.etc2) {
-            return transcodeTarget.ETC;
+
+        for (let t = 0; t < data.mipmaps.length; ++t) {
+            let mipmap = data.mipmaps[t];
+
+            if (!mipmap || !mipmap.data) {
+                throw new Error("KTX2 container - could not transcode one of the image");
+            }
+
+            if (data.transcodedFormat === 0x8058 /* RGBA8 */) {
+                // uncompressed RGBA
+                internalTexture.width = mipmap.width; // need to set width/height so that the call to _uploadDataToTextureDirectly uses the right dimensions
+                internalTexture.height = mipmap.height;
+
+                this._engine._uploadDataToTextureDirectly(internalTexture, mipmap.data, 0, t, undefined, true);
+            } else {
+                this._engine._uploadCompressedDataToTextureDirectly(internalTexture, data.transcodedFormat, mipmap.width, mipmap.height, mipmap.data, 0, t);
+            }
         }
 
-        throw new Error("No compatible format available");
+        internalTexture.width = data.mipmaps[0].width;
+        internalTexture.height = data.mipmaps[0].height;
+        internalTexture.generateMipMaps = data.mipmaps.length > 1;
+        internalTexture.isReady = true;
+
+        this._engine._bindTextureDirectly(this._engine._gl.TEXTURE_2D, null);
     }
 
     /**
@@ -72,8 +219,7 @@ export class KhronosTextureContainer2 {
      * @returns true if the data is a KTX2 file or false otherwise
      */
     public static IsValid(data: ArrayBufferView): boolean {
-        if (data.byteLength >= 12)
-        {
+        if (data.byteLength >= 12) {
             // '«', 'K', 'T', 'X', ' ', '2', '0', '»', '\r', '\n', '\x1A', '\n'
             const identifier = new Uint8Array(data.buffer, data.byteOffset, 12);
             if (identifier[0] === 0xAB && identifier[1] === 0x4B && identifier[2] === 0x54 && identifier[3] === 0x58 && identifier[4] === 0x20 && identifier[5] === 0x32 &&
@@ -85,3 +231,36 @@ export class KhronosTextureContainer2 {
         return false;
     }
 }
+
+declare function importScripts(...urls: string[]): void;
+declare function postMessage(message: any, transfer?: any[]): void;
+
+declare var KTX2DECODER: any;
+
+export function workerFunc(): void {
+    let ktx2Decoder: any;
+
+    onmessage = (event) => {
+        switch (event.data.action) {
+            case "init":
+                importScripts(event.data.jsPath);
+                ktx2Decoder = new KTX2DECODER.KTX2Decoder();
+                postMessage({ action: "init" });
+                break;
+            case "decode":
+                ktx2Decoder.decode(event.data.data, event.data.caps).then((data: any) => {
+                    const buffers = [];
+                    for (let mip = 0; mip < data.mipmaps.length; ++mip) {
+                        const mipmap = data.mipmaps[mip];
+                        if (mipmap && mipmap.data) {
+                            buffers.push(mipmap.data.buffer);
+                        }
+                    }
+                    postMessage({ action: "decoded", success: true, decodedData: data }, buffers);
+                }).catch((reason: any) => {
+                    postMessage({ action: "decoded", success: false, msg: reason });
+                });
+                break;
+        }
+    };
+}

+ 0 - 1
tests/validation/validate.html

@@ -9,7 +9,6 @@
 	<script src="https://preview.babylonjs.com/ammo.js"></script>
 	<script src="https://preview.babylonjs.com/cannon.js"></script>
 	<script src="https://preview.babylonjs.com/Oimo.js"></script>
-	<script src="https://preview.babylonjs.com/libktx.js"></script>
 	<script src="https://preview.babylonjs.com/babylon.js"></script>
 	<script src="https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>