From 60c9e474ad2007b4aff85cf141f23c73c2cdace5 Mon Sep 17 00:00:00 2001 From: Richard Geldreich Date: Wed, 1 Jul 2026 12:59:16 -0400 Subject: [PATCH] adding new files --- webgl/ktx2_studio/encode-worker.js | 190 +++++++++++++++++++++++ webgl/ktx2_studio/encode_dto_apply.js | 129 +++++++++++++++ webgl/ktx2_studio/wasm-feature-detect.js | 1 + 3 files changed, 320 insertions(+) create mode 100644 webgl/ktx2_studio/encode-worker.js create mode 100644 webgl/ktx2_studio/encode_dto_apply.js create mode 100644 webgl/ktx2_studio/wasm-feature-detect.js diff --git a/webgl/ktx2_studio/encode-worker.js b/webgl/ktx2_studio/encode-worker.js new file mode 100644 index 0000000..07a8c3e --- /dev/null +++ b/webgl/ktx2_studio/encode-worker.js @@ -0,0 +1,190 @@ +// 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(); + } + } +}; diff --git a/webgl/ktx2_studio/encode_dto_apply.js b/webgl/ktx2_studio/encode_dto_apply.js new file mode 100644 index 0000000..9307e93 --- /dev/null +++ b/webgl/ktx2_studio/encode_dto_apply.js @@ -0,0 +1,129 @@ +// encode_dto_apply.js +// +// applyEncodeDTO(encoder, dto): applies an encode DTO (built by buildEncodeDTO in +// index.html) to a BasisEncoder instance, in the SAME ORDER as the legacy inline +// encode path; the caller then calls encoder.encode(). +// +// PURE: depends only on (encoder, dto) -- no Module, no DOM. That's why it can run +// unchanged on the main thread (non-worker DTO path) AND inside the encode worker +// (which importScripts() this file). All enum values in dto are already ints. +// +// Loaded on the page via