mirror of
https://github.com/BinomialLLC/basis_universal.git
synced 2026-09-10 04:28:26 +00:00
adding new files
This commit is contained in:
190
webgl/ktx2_studio/encode-worker.js
Normal file
190
webgl/ktx2_studio/encode-worker.js
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
129
webgl/ktx2_studio/encode_dto_apply.js
Normal file
129
webgl/ktx2_studio/encode_dto_apply.js
Normal file
@@ -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 <script src> and in the worker via importScripts(), so it
|
||||
// is the single source of truth for both modes (no duplication / drift).
|
||||
|
||||
function applyEncodeDTO(encoder, dto)
|
||||
{
|
||||
// container / threading
|
||||
encoder.controlThreading(dto.multithreaded, dto.numWorkerThreads);
|
||||
encoder.setCreateKTX2File(dto.createKTX2);
|
||||
encoder.setKTX2UASTCSupercompression(dto.ktx2UASTCSupercompression);
|
||||
|
||||
// color / transfer
|
||||
encoder.setPerceptual(dto.sRGB);
|
||||
if (!dto.isHDRSourceFile)
|
||||
encoder.setKTX2AndBasisSRGBTransferFunc(dto.sRGB);
|
||||
encoder.setMipSRGB(dto.sRGB);
|
||||
|
||||
// source image (the DTO normalized the 3 legacy cases into one shape:
|
||||
// pre-decoded RGBA has real dims; raw file bytes use 0,0 + an img_type)
|
||||
const s = dto.source;
|
||||
if (s.isHDRTarget)
|
||||
encoder.setSliceSourceImageHDR(0, s.bytes, s.width, s.height, s.imgType, s.convertLDRToLinear, s.nitMultiplier);
|
||||
else
|
||||
encoder.setSliceSourceImage(0, s.bytes, s.width, s.height, s.imgType);
|
||||
|
||||
encoder.setFormatMode(dto.formatMode);
|
||||
encoder.setRec2020(dto.rec2020);
|
||||
encoder.setDebug(dto.debug);
|
||||
encoder.setComputeStats(dto.computeStats);
|
||||
encoder.setPrintStats(dto.printStats);
|
||||
encoder.setStatusOutput(true);
|
||||
|
||||
// low-level codec opts -- run when the unified checkbox is OFF (legacy).
|
||||
// NOTE this can run even for XUBC7 (when checkbox is off); applyUnified then
|
||||
// overrides quality/effort afterward, exactly as the legacy path does.
|
||||
if (dto.lowLevelOptsEnabled)
|
||||
{
|
||||
encoder.setUASTCHDRQualityLevel(dto.uastcHDRQuality);
|
||||
encoder.setASTC_HDR_6x6_Level(dto.astcHDR6x6Level);
|
||||
encoder.setLambda(dto.astc6x6Lambda);
|
||||
|
||||
// three-way quality branch (mirrors legacy): XUBC7 defers to the unified
|
||||
// call (no setQualityLevel here); XUASTC uses DCT; everything else ETC1S.
|
||||
if (dto.isXUBC7Target)
|
||||
{
|
||||
// intentionally nothing -- applyUnified handles XUBC7 quality/effort
|
||||
}
|
||||
else if (dto.isXUASTCLDRTarget)
|
||||
{
|
||||
if (dto.xuastcDCTQuality < 100)
|
||||
{
|
||||
encoder.setQualityLevel(dto.xuastcDCTQuality);
|
||||
encoder.setXUASTCLDRUseDCT(true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
encoder.setQualityLevel(dto.etc1sQuality);
|
||||
}
|
||||
|
||||
encoder.setXUASTCLDRUseLossySupercompression(dto.xuastcLossySupercompression);
|
||||
encoder.setASTCOrXUASTCLDREffortLevel(dto.astcXuastcEffortLevel);
|
||||
encoder.setRDOUASTC(dto.rdoUASTC);
|
||||
encoder.setRDOUASTCQualityScalar(dto.rdoUASTCQualityScalar);
|
||||
encoder.setPackUASTCFlags(dto.packUASTCFlags);
|
||||
encoder.setETC1SCompressionLevel(dto.etc1sCompLevel);
|
||||
}
|
||||
|
||||
// always-applied opts (available even when the unified path is used)
|
||||
for (let i = 0; i < 6; i++)
|
||||
encoder.setXUASTCLDRBoundedRDOParam(i, dto.xuastcBoundedRDO[i]);
|
||||
|
||||
encoder.setXUASTCLDRForceDisableRGBDualPlane(dto.xuastcDisableRGBDualPlane);
|
||||
encoder.setXUASTCLDRForceDisableSubsets(dto.xuastcDisableSubsets);
|
||||
encoder.setXUASTCLDRUseBlurring(dto.xuastcUseBlur);
|
||||
encoder.setXUASTCLDRSelectCompressor(dto.xuastcSelectCompressor);
|
||||
encoder.setXUASTCLDRHeavySubsetUsage(dto.xuastcHeavySubsetUsage);
|
||||
encoder.setXUASTCLDRSharpenMode(dto.xuastcSharpenMode);
|
||||
encoder.setXUASTCLDRSharpenAmount(dto.xuastcSharpenAmount);
|
||||
encoder.setXUASTCLDRDeblockingMode(dto.xuastcDeblockingMode);
|
||||
encoder.setXUASTCLDRNumDeblockingPasses(dto.xuastcNumDeblockingPasses);
|
||||
encoder.setASTCOrXUASTCLDRWeights(dto.weights[0], dto.weights[1], dto.weights[2], dto.weights[3]);
|
||||
encoder.setSwizzle(dto.swizzle[0], dto.swizzle[1], dto.swizzle[2], dto.swizzle[3]);
|
||||
encoder.setXUASTCLDRSyntax(dto.xuastcSyntax);
|
||||
encoder.setXUBC7RDOLevel(dto.xubc7RDOLevel);
|
||||
encoder.setXUBC7NumStripes(dto.xubc7NumStripes);
|
||||
encoder.setXUBC7Encoder(dto.xubc7Encoder);
|
||||
encoder.setXUBC7BC7EScalarLevel(dto.xubc7BC7EScalarLevel);
|
||||
|
||||
// mipmaps
|
||||
encoder.setMipGen(dto.mipGen);
|
||||
encoder.setMipFilter(dto.mipFilter);
|
||||
encoder.setMipScale(dto.mipScale);
|
||||
encoder.setMipSmallestDimension(dto.mipSmallestDim);
|
||||
encoder.setMipRenormalize(dto.mipRenormalize);
|
||||
encoder.setMipWrapping(dto.mipWrapping);
|
||||
encoder.setYFlip(dto.yFlip);
|
||||
|
||||
// unified quality/effort LAST -- it intentionally overrides some of the
|
||||
// codec-specific low-level options set above (matches the legacy path)
|
||||
if (dto.applyUnified)
|
||||
encoder.setFormatModeAndQualityEffort(dto.formatMode, dto.unifiedQuality, dto.unifiedEffort, true);
|
||||
}
|
||||
|
||||
// applyDDSEncodeDTO(encoder, dto): applies ONLY the DDS-export-specific options on top of a
|
||||
// BasisEncoder that has ALREADY had applyEncodeDTO() run on it. The caller then calls
|
||||
// encoder.encodeToDDS(buffer). Used by the DDS-export path (worker 'encodeDDS' message).
|
||||
//
|
||||
// PURE: depends only on (encoder, dto) -- no Module, no DOM (same contract as applyEncodeDTO).
|
||||
//
|
||||
// Everything DDS shares with KTX2 (source image, sRGB transfer func, perceptual, channel weights,
|
||||
// mips, etc.) was already set by applyEncodeDTO() and is consumed by encodeToDDS()/build_dds()
|
||||
// unchanged -- so we only set the genuinely NEW things here: the output format + BC7 packer knobs.
|
||||
function applyDDSEncodeDTO(encoder, dto)
|
||||
{
|
||||
encoder.setDDSFormatEnum(dto.ddsFormat);
|
||||
encoder.setDDSBC7Encoder(dto.ddsBC7Encoder);
|
||||
encoder.setDDSBC7FLevel(dto.ddsBC7FLevel);
|
||||
encoder.setDDSBC7EScalarLevel(dto.ddsBC7EScalarLevel);
|
||||
}
|
||||
1
webgl/ktx2_studio/wasm-feature-detect.js
Normal file
1
webgl/ktx2_studio/wasm-feature-detect.js
Normal file
@@ -0,0 +1 @@
|
||||
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e="undefined"!=typeof globalThis?globalThis:e||self).wasmFeatureDetect=n()}(this,(function(){"use strict";return{bigInt:()=>(async e=>{try{return(await WebAssembly.instantiate(e)).instance.exports.b(BigInt(0))===BigInt(0)}catch(e){return!1}})(new Uint8Array([0,97,115,109,1,0,0,0,1,6,1,96,1,126,1,126,3,2,1,0,7,5,1,1,98,0,0,10,6,1,4,0,32,0,11])),bulkMemory:async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,5,3,1,0,1,10,14,1,12,0,65,0,65,0,65,0,252,10,0,0,11])),exceptions:async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,8,1,6,0,6,64,25,11,11])),exceptionsFinal:()=>(async()=>{try{return new WebAssembly.Module(Uint8Array.from(atob("AGFzbQEAAAABBAFgAAADAgEAChABDgACaR9AAQMAAAsACxoL"),(e=>e.codePointAt(0)))),!0}catch(e){return!1}})(),extendedConst:async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,5,3,1,0,1,11,9,1,0,65,1,65,2,106,11,0])),gc:()=>(async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,95,1,120,0])))(),jsStringBuiltins:()=>(async()=>{try{return await WebAssembly.instantiate(Uint8Array.from(atob("AGFzbQEAAAABBgFgAW8BfwIXAQ53YXNtOmpzLXN0cmluZwR0ZXN0AAA="),(e=>e.codePointAt(0))),{},{builtins:["js-string"]}),!0}catch(e){return!1}})(),jspi:()=>(async()=>"Suspending"in WebAssembly)(),memory64:async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,5,3,1,4,1])),multiMemory:()=>(async()=>{try{return new WebAssembly.Module(new Uint8Array([0,97,115,109,1,0,0,0,5,5,2,0,0,0,0])),!0}catch(e){return!1}})(),multiValue:async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,6,1,96,0,2,127,127,3,2,1,0,10,8,1,6,0,65,0,65,0,11])),mutableGlobals:async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,2,8,1,1,97,1,98,3,127,1,6,6,1,127,1,65,0,11,7,5,1,1,97,3,1])),referenceTypes:async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,7,1,5,0,208,112,26,11])),relaxedSimd:async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,15,1,13,0,65,1,253,15,65,2,253,15,253,128,2,11])),saturatedFloatToInt:async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,12,1,10,0,67,0,0,0,0,252,0,26,11])),signExtensions:async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,8,1,6,0,65,0,192,26,11])),simd:async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11])),streamingCompilation:()=>(async()=>"compileStreaming"in WebAssembly)(),tailCall:async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,6,1,4,0,18,0,11])),threads:()=>(async e=>{try{return"undefined"!=typeof MessageChannel&&(new MessageChannel).port1.postMessage(new SharedArrayBuffer(1)),WebAssembly.validate(e)}catch(e){return!1}})(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,5,4,1,3,1,1,10,11,1,9,0,65,0,254,16,2,0,26,11])),typeReflection:()=>(async()=>"Function"in WebAssembly)(),typedFunctionReferences:()=>(async()=>{try{return new WebAssembly.Module(Uint8Array.from(atob("AGFzbQEAAAABEANgAX8Bf2ABZAABf2AAAX8DBAMBAAIJBQEDAAEBChwDCwBBCkEqIAAUAGoLBwAgAEEBagsGANIBEAAL"),(e=>e.codePointAt(0)))),!0}catch(e){return!1}})()}}));
|
||||
Reference in New Issue
Block a user