khronosTextureContainer2.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. import { InternalTexture } from "../Materials/Textures/internalTexture";
  2. import { ThinEngine } from "../Engines/thinEngine";
  3. import { Constants } from '../Engines/constants';
  4. import { WorkerPool } from './workerPool';
  5. declare var KTX2DECODER: any;
  6. /**
  7. * Class for loading KTX2 files
  8. */
  9. export class KhronosTextureContainer2 {
  10. private static _WorkerPoolPromise?: Promise<WorkerPool>;
  11. private static _Initialized: boolean;
  12. private static _Ktx2Decoder: any; // used when no worker pool is used
  13. /**
  14. * URLs to use when loading the KTX2 decoder module as well as its dependencies
  15. * If a url is null, the default url is used (pointing to https://preview.babylonjs.com)
  16. * Note that jsDecoderModule can't be null and that the other dependencies will only be loaded if necessary
  17. * Urls you can change:
  18. * URLConfig.jsDecoderModule
  19. * URLConfig.wasmUASTCToASTC
  20. * URLConfig.wasmUASTCToBC7
  21. * URLConfig.jsMSCTranscoder
  22. * URLConfig.wasmMSCTranscoder
  23. * You can see their default values in this PG: https://playground.babylonjs.com/#EIJH8L#9
  24. */
  25. public static URLConfig = {
  26. jsDecoderModule: "https://preview.babylonjs.com/babylon.ktx2Decoder.js",
  27. wasmUASTCToASTC: null,
  28. wasmUASTCToBC7: null,
  29. jsMSCTranscoder: null,
  30. wasmMSCTranscoder: null
  31. };
  32. /**
  33. * Default number of workers used to handle data decoding
  34. */
  35. public static DefaultNumWorkers = KhronosTextureContainer2.GetDefaultNumWorkers();
  36. private static GetDefaultNumWorkers(): number {
  37. if (typeof navigator !== "object" || !navigator.hardwareConcurrency) {
  38. return 1;
  39. }
  40. // Use 50% of the available logical processors but capped at 4.
  41. return Math.min(Math.floor(navigator.hardwareConcurrency * 0.5), 4);
  42. }
  43. private _engine: ThinEngine;
  44. private static _CreateWorkerPool(numWorkers: number) {
  45. this._Initialized = true;
  46. if (numWorkers && typeof Worker === "function") {
  47. KhronosTextureContainer2._WorkerPoolPromise = new Promise((resolve) => {
  48. const workerContent = `(${workerFunc})()`;
  49. const workerBlobUrl = URL.createObjectURL(new Blob([workerContent], { type: "application/javascript" }));
  50. const workerPromises = new Array<Promise<Worker>>(numWorkers);
  51. for (let i = 0; i < workerPromises.length; i++) {
  52. workerPromises[i] = new Promise((resolve, reject) => {
  53. const worker = new Worker(workerBlobUrl);
  54. const onError = (error: ErrorEvent) => {
  55. worker.removeEventListener("error", onError);
  56. worker.removeEventListener("message", onMessage);
  57. reject(error);
  58. };
  59. const onMessage = (message: MessageEvent) => {
  60. if (message.data.action === "init") {
  61. worker.removeEventListener("error", onError);
  62. worker.removeEventListener("message", onMessage);
  63. resolve(worker);
  64. }
  65. };
  66. worker.addEventListener("error", onError);
  67. worker.addEventListener("message", onMessage);
  68. worker.postMessage({
  69. action: "init",
  70. urls: KhronosTextureContainer2.URLConfig
  71. });
  72. });
  73. }
  74. Promise.all(workerPromises).then((workers) => {
  75. resolve(new WorkerPool(workers));
  76. });
  77. });
  78. } else {
  79. KTX2DECODER.MSCTranscoder.UseFromWorkerThread = false;
  80. KTX2DECODER.WASMMemoryManager.LoadBinariesFromCurrentThread = true;
  81. }
  82. }
  83. /**
  84. * Constructor
  85. * @param engine The engine to use
  86. * @param numWorkers The number of workers for async operations. Specify `0` to disable web workers and run synchronously in the current context.
  87. */
  88. public constructor(engine: ThinEngine, numWorkers = KhronosTextureContainer2.DefaultNumWorkers) {
  89. this._engine = engine;
  90. if (!KhronosTextureContainer2._Initialized) {
  91. KhronosTextureContainer2._CreateWorkerPool(numWorkers);
  92. }
  93. }
  94. /** @hidden */
  95. public uploadAsync(data: ArrayBufferView, internalTexture: InternalTexture): Promise<void> {
  96. const caps = this._engine.getCaps();
  97. const compressedTexturesCaps = {
  98. astc: !!caps.astc,
  99. bptc: !!caps.bptc,
  100. s3tc: !!caps.s3tc,
  101. pvrtc: !!caps.pvrtc,
  102. etc2: !!caps.etc2,
  103. etc1: !!caps.etc1,
  104. };
  105. if (KhronosTextureContainer2._WorkerPoolPromise) {
  106. return KhronosTextureContainer2._WorkerPoolPromise.then((workerPool) => {
  107. return new Promise((resolve, reject) => {
  108. workerPool.push((worker, onComplete) => {
  109. const onError = (error: ErrorEvent) => {
  110. worker.removeEventListener("error", onError);
  111. worker.removeEventListener("message", onMessage);
  112. reject(error);
  113. onComplete();
  114. };
  115. const onMessage = (message: MessageEvent) => {
  116. if (message.data.action === "decoded") {
  117. worker.removeEventListener("error", onError);
  118. worker.removeEventListener("message", onMessage);
  119. if (!message.data.success) {
  120. reject({ message: message.data.msg });
  121. } else {
  122. try {
  123. this._createTexture(message.data.decodedData, internalTexture);
  124. resolve();
  125. } catch (err) {
  126. reject({ message: err });
  127. }
  128. }
  129. onComplete();
  130. }
  131. };
  132. worker.addEventListener("error", onError);
  133. worker.addEventListener("message", onMessage);
  134. // 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
  135. worker.postMessage({ action: "decode", data, caps: compressedTexturesCaps }/*, [data.buffer]*/);
  136. });
  137. });
  138. });
  139. }
  140. return new Promise((resolve, reject) => {
  141. if (!KhronosTextureContainer2._Ktx2Decoder) {
  142. KhronosTextureContainer2._Ktx2Decoder = new KTX2DECODER.KTX2Decoder();
  143. }
  144. KhronosTextureContainer2._Ktx2Decoder.decode(data, caps).then((data: any) => {
  145. this._createTexture(data, internalTexture);
  146. resolve();
  147. }).catch((reason: any) => {
  148. reject({ message: reason });
  149. });
  150. });
  151. }
  152. /**
  153. * Stop all async operations and release resources.
  154. */
  155. public dispose(): void {
  156. if (KhronosTextureContainer2._WorkerPoolPromise) {
  157. KhronosTextureContainer2._WorkerPoolPromise.then((workerPool) => {
  158. workerPool.dispose();
  159. });
  160. }
  161. delete KhronosTextureContainer2._WorkerPoolPromise;
  162. }
  163. protected _createTexture(data: any /* IEncodedData */, internalTexture: InternalTexture) {
  164. const oglTexture2D = 3553;
  165. this._engine._bindTextureDirectly(oglTexture2D, internalTexture);
  166. if (data.transcodedFormat === 0x8058 /* RGBA8 */) {
  167. internalTexture.type = Constants.TEXTURETYPE_UNSIGNED_BYTE;
  168. internalTexture.format = Constants.TEXTUREFORMAT_RGBA;
  169. } else {
  170. internalTexture.format = data.transcodedFormat;
  171. }
  172. internalTexture._gammaSpace = data.isInGammaSpace;
  173. internalTexture.generateMipMaps = data.mipmaps.length > 1;
  174. if (data.errors) {
  175. throw new Error("KTX2 container - could not transcode the data. " + data.errors);
  176. }
  177. for (let t = 0; t < data.mipmaps.length; ++t) {
  178. let mipmap = data.mipmaps[t];
  179. if (!mipmap || !mipmap.data) {
  180. throw new Error("KTX2 container - could not transcode one of the image");
  181. }
  182. if (data.transcodedFormat === 0x8058 /* RGBA8 */) {
  183. // uncompressed RGBA
  184. internalTexture.width = mipmap.width; // need to set width/height so that the call to _uploadDataToTextureDirectly uses the right dimensions
  185. internalTexture.height = mipmap.height;
  186. this._engine._uploadDataToTextureDirectly(internalTexture, mipmap.data, 0, t, undefined, true);
  187. } else {
  188. this._engine._uploadCompressedDataToTextureDirectly(internalTexture, data.transcodedFormat, mipmap.width, mipmap.height, mipmap.data, 0, t);
  189. }
  190. }
  191. internalTexture.width = data.mipmaps[0].width;
  192. internalTexture.height = data.mipmaps[0].height;
  193. internalTexture.isReady = true;
  194. this._engine._bindTextureDirectly(oglTexture2D, null);
  195. }
  196. /**
  197. * Checks if the given data starts with a KTX2 file identifier.
  198. * @param data the data to check
  199. * @returns true if the data is a KTX2 file or false otherwise
  200. */
  201. public static IsValid(data: ArrayBufferView): boolean {
  202. if (data.byteLength >= 12) {
  203. // '«', 'K', 'T', 'X', ' ', '2', '0', '»', '\r', '\n', '\x1A', '\n'
  204. const identifier = new Uint8Array(data.buffer, data.byteOffset, 12);
  205. if (identifier[0] === 0xAB && identifier[1] === 0x4B && identifier[2] === 0x54 && identifier[3] === 0x58 && identifier[4] === 0x20 && identifier[5] === 0x32 &&
  206. identifier[6] === 0x30 && identifier[7] === 0xBB && identifier[8] === 0x0D && identifier[9] === 0x0A && identifier[10] === 0x1A && identifier[11] === 0x0A) {
  207. return true;
  208. }
  209. }
  210. return false;
  211. }
  212. }
  213. declare function importScripts(...urls: string[]): void;
  214. declare function postMessage(message: any, transfer?: any[]): void;
  215. declare var KTX2DECODER: any;
  216. function workerFunc(): void {
  217. let ktx2Decoder: any;
  218. onmessage = (event) => {
  219. switch (event.data.action) {
  220. case "init":
  221. const urls = event.data.urls;
  222. importScripts(urls.jsDecoderModule);
  223. if (urls.wasmUASTCToASTC !== null) {
  224. KTX2DECODER.LiteTranscoder_UASTC_ASTC.WasmModuleURL = urls.wasmUASTCToASTC;
  225. }
  226. if (urls.wasmUASTCToBC7 !== null) {
  227. KTX2DECODER.LiteTranscoder_UASTC_BC7.WasmModuleURL = urls.wasmUASTCToBC7;
  228. }
  229. if (urls.jsMSCTranscoder !== null) {
  230. KTX2DECODER.MSCTranscoder.JSModuleURL = urls.jsMSCTranscoder;
  231. }
  232. if (urls.wasmMSCTranscoder !== null) {
  233. KTX2DECODER.MSCTranscoder.WasmModuleURL = urls.wasmMSCTranscoder;
  234. }
  235. ktx2Decoder = new KTX2DECODER.KTX2Decoder();
  236. postMessage({ action: "init" });
  237. break;
  238. case "decode":
  239. ktx2Decoder.decode(event.data.data, event.data.caps).then((data: any) => {
  240. const buffers = [];
  241. for (let mip = 0; mip < data.mipmaps.length; ++mip) {
  242. const mipmap = data.mipmaps[mip];
  243. if (mipmap && mipmap.data) {
  244. buffers.push(mipmap.data.buffer);
  245. }
  246. }
  247. postMessage({ action: "decoded", success: true, decodedData: data }, buffers);
  248. }).catch((reason: any) => {
  249. postMessage({ action: "decoded", success: false, msg: reason });
  250. });
  251. break;
  252. }
  253. };
  254. }