// 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(); } } };