| // encode-worker.js | |
| // | |
| // Step 2 of moving encoding off the UI thread: load the (threaded) BasisEncoder | |
| // module in a Web Worker (proven working), then on demand apply a DTO and run a | |
| // real encode -- reporting the resulting size/PSNR/time back to the page. Uses its | |
| // OWN independent module instance; the page keeps its own instance for transcode. | |
| // | |
| // Protocol (page -> worker): | |
| // { type: 'init', scriptSrc } load + initialize the module ONCE | |
| // { type: 'encode', dto } applyEncodeDTO(dto) + encode(); report result (.basis/.KTX2) | |
| // { type: 'encodeDDS', dto } applyEncodeDTO(dto) + applyDDSEncodeDTO(dto) + encodeToDDS(); report .DDS | |
| // Protocol (worker -> page): | |
| // { type: 'log', text } informational | |
| // { type: 'loaded', text } module loaded + initializeBasis() succeeded | |
| // { type: 'encoded', text } encode finished (size / PSNR / time) | |
| // { type: 'encodedDDS', text } DDS encode finished (size / time) | |
| // { type: 'error', text } something failed | |
| importScripts('encode_dto_apply.js'); // defines applyEncodeDTO()/applyDDSEncodeDTO() -- shared with the page | |
| let g_module = null; | |
| function post(type, text) { self.postMessage({ type: type, text: text }); } | |
| self.onmessage = function (e) | |
| { | |
| const msg = e.data || {}; | |
| if (msg.type === 'init') | |
| { | |
| if (g_module) | |
| { | |
| post('loaded', 'worker: module already loaded (reusing instance)'); | |
| return; | |
| } | |
| try | |
| { | |
| post('log', 'worker: importScripts(' + msg.scriptSrc + ')'); | |
| importScripts(msg.scriptSrc); // defines the global BASIS() factory (EXPORT_NAME=BASIS) | |
| if (typeof BASIS !== 'function') | |
| { | |
| post('error', 'worker: BASIS factory not defined after importScripts'); | |
| return; | |
| } | |
| // Absolute URL to the encoder .js (relative to this worker's location). | |
| const absMainUrl = new URL(msg.scriptSrc, self.location.href).href; | |
| post('log', 'worker: mainScriptUrlOrBlob=' + absMainUrl); | |
| BASIS({ | |
| // REQUIRED when loaded via importScripts inside a worker: emscripten | |
| // spawns its pthread sub-workers from this URL. Without it the thread | |
| // pool never comes up and BASIS() hangs silently. | |
| mainScriptUrlOrBlob: absMainUrl, | |
| locateFile: function (path) { return new URL('../encoder/build/' + path, self.location.href).href; }, | |
| // Forward the encoder's stdout/stderr to the page line-by-line. Because | |
| // encode() blocks the WORKER (not the main thread), these stream LIVE to | |
| // the page during the encode -- the modal log updates in real time. | |
| print: function (t) { self.postMessage({ type: 'print', text: t }); }, | |
| printErr: function (t) { self.postMessage({ type: 'print', text: t }); }, | |
| onRuntimeInitialized: function () { post('log', 'worker: onRuntimeInitialized'); } | |
| }) | |
| .then(function (module) | |
| { | |
| g_module = module; | |
| if (module.initializeBasis) | |
| { | |
| module.initializeBasis(); | |
| post('loaded', 'worker: module loaded + initializeBasis() OK (threaded module live in worker)'); | |
| } | |
| else | |
| { | |
| post('error', 'worker: module loaded but initializeBasis() is missing'); | |
| } | |
| }) | |
| .catch(function (err) | |
| { | |
| post('error', 'worker: BASIS() instantiation failed: ' + err); | |
| }); | |
| } | |
| catch (err) | |
| { | |
| post('error', 'worker: init exception: ' + err); | |
| } | |
| } | |
| else if (msg.type === 'encode') | |
| { | |
| if (!g_module) | |
| { | |
| post('error', 'worker: encode requested before module is loaded'); | |
| return; | |
| } | |
| let enc = null; | |
| try | |
| { | |
| enc = new g_module.BasisEncoder(); // embind class lives on the module instance | |
| applyEncodeDTO(enc, msg.dto); // shared with the page; pure (encoder, dto) | |
| // destination buffer -- matches the legacy page (new Uint8Array(1024*1024*24)) | |
| const out = new Uint8Array(1024 * 1024 * 24); | |
| // time JUST the encode() call, like the main-thread path (startTime/elapsed) | |
| const encT0 = performance.now(); | |
| const len = enc.encode(out); | |
| const encodeMs = performance.now() - encT0; | |
| const psnr = (typeof enc.getLastEncodeMip0RGBAPSNR === 'function') ? enc.getLastEncodeMip0RGBAPSNR() : -1; | |
| if (len > 0) | |
| { | |
| // exact-size copy (its own ArrayBuffer) so we can transfer it back zero-copy | |
| const result = out.slice(0, len); | |
| self.postMessage({ | |
| type: 'encoded', | |
| text: 'worker: ENCODE OK -> ' + len + ' bytes, mip0 RGBA PSNR ' + psnr.toFixed(3) + ', ' + encodeMs.toFixed(2) + ' ms', | |
| bytes: result.buffer, | |
| len: len, | |
| psnr: psnr, // number: mip0 RGBA PSNR (-> g_lastEncodeMip0RGBAPSNR) | |
| encodeMs: encodeMs // number: encode() wall time (-> g_lastEncodeTime) | |
| }, [result.buffer]); // transfer the buffer (no copy) | |
| } | |
| else | |
| post('error', 'worker: encode returned 0 bytes (failed)'); | |
| } | |
| catch (err) | |
| { | |
| post('error', 'worker: encode exception: ' + err); | |
| } | |
| finally | |
| { | |
| // Free the WASM encoder even on exception -- the worker is never terminated, so a leak here | |
| // would permanently consume WASM heap. | |
| if (enc) enc.delete(); | |
| } | |
| } | |
| else if (msg.type === 'encodeDDS') | |
| { | |
| if (!g_module) | |
| { | |
| post('error', 'worker: DDS encode requested before module is loaded'); | |
| return; | |
| } | |
| let enc = null; | |
| try | |
| { | |
| enc = new g_module.BasisEncoder(); // embind class lives on the module instance | |
| applyEncodeDTO(enc, msg.dto); // shared source/sRGB/mip/weights setup (same as KTX2) | |
| applyDDSEncodeDTO(enc, msg.dto); // DDS-specific output format + BC7 options | |
| // Destination buffer: sized by the page (worst case 32bpp + full mip chain, bounded by the | |
| // encoder's source-pixel cap). If somehow too small, encodeToDDS() returns 0 (clean failure). | |
| const out = new Uint8Array(msg.dto.ddsBufferSize); | |
| // Time JUST the encode, like the .basis/.KTX2 path. | |
| const encT0 = performance.now(); | |
| const len = enc.encodeToDDS(out); | |
| const encodeMs = performance.now() - encT0; | |
| if (len > 0) | |
| { | |
| // exact-size copy (its own ArrayBuffer) so we can transfer it back zero-copy | |
| const result = out.slice(0, len); | |
| self.postMessage({ | |
| type: 'encodedDDS', | |
| text: 'worker: DDS ENCODE OK -> ' + len + ' bytes, ' + encodeMs.toFixed(2) + ' ms', | |
| bytes: result.buffer, | |
| len: len, | |
| encodeMs: encodeMs // number: encodeToDDS() wall time (-> g_lastEncodeTime) | |
| }, [result.buffer]); // transfer the buffer (no copy) | |
| } | |
| else | |
| post('error', 'worker: encodeToDDS returned 0 bytes (failed)'); | |
| } | |
| catch (err) | |
| { | |
| post('error', 'worker: DDS encode exception: ' + err); | |
| } | |
| finally | |
| { | |
| // Free the WASM encoder even on exception -- the worker is never terminated, so a leak here | |
| // would permanently consume WASM heap. | |
| if (enc) enc.delete(); | |
| } | |
| } | |
| }; |