playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
253 lines (251 loc) • 7.32 kB
JavaScript
import { WasmModule } from "../../core/wasm-module.js";
import { Debug } from "../../core/debug.js";
import { PIXELFORMAT_RGB565, PIXELFORMAT_RGBA4 } from "../../platform/graphics/constants.js";
import { BasisWorker } from "./basis-worker.js";
import { http } from "../../platform/net/http.js";
const getCompressionFormats = (device) => {
return {
astc: !!device.extCompressedTextureASTC,
atc: !!device.extCompressedTextureATC,
dxt: !!device.extCompressedTextureS3TC,
etc1: !!device.extCompressedTextureETC1,
etc2: !!device.extCompressedTextureETC,
pvr: !!device.extCompressedTexturePVRTC
};
};
const prepareWorkerModules = (config, callback) => {
const getWorkerBlob = (basisCode) => {
const code = [
"/* basis */",
basisCode,
"",
`(${BasisWorker.toString()})()
`
].join("\n");
return new Blob([code], { type: "application/javascript" });
};
const wasmSupported = () => {
try {
if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") {
const module = new WebAssembly.Module(Uint8Array.of(0, 97, 115, 109, 1, 0, 0, 0));
if (module instanceof WebAssembly.Module) {
return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
}
}
} catch (e) {
}
return false;
};
const sendResponse = (basisCode, module) => {
callback(null, {
workerUrl: URL.createObjectURL(getWorkerBlob(basisCode)),
module,
rgbPriority: config.rgbPriority,
rgbaPriority: config.rgbaPriority
});
};
const options = {
cache: true,
responseType: "text",
retry: config.maxRetries > 0,
maxRetries: config.maxRetries
};
if (config.glueUrl && config.wasmUrl && wasmSupported()) {
let basisCode = null;
let module = null;
http.get(config.glueUrl, options, (err, response) => {
if (err) {
callback(err);
} else {
if (module) {
sendResponse(response, module);
} else {
basisCode = response;
}
}
});
const fetchPromise = fetch(config.wasmUrl);
const compileManual = () => {
fetchPromise.then((result) => result.arrayBuffer()).then((buffer) => WebAssembly.compile(buffer)).then((module_) => {
if (basisCode) {
sendResponse(basisCode, module_);
} else {
module = module_;
}
}).catch((err) => {
callback(err, null);
});
};
if (WebAssembly.compileStreaming) {
WebAssembly.compileStreaming(fetchPromise).then((module_) => {
if (basisCode) {
sendResponse(basisCode, module_);
} else {
module = module_;
}
}).catch((err) => {
Debug.warn(`compileStreaming() failed for ${config.wasmUrl} (${err}), falling back to arraybuffer download.`);
compileManual();
});
} else {
compileManual();
}
} else {
http.get(config.fallbackUrl, options, (err, response) => {
if (err) {
callback(err, null);
} else {
sendResponse(response, null);
}
});
}
};
class BasisQueue {
constructor() {
this.callbacks = {};
this.queue = [];
this.clients = [];
}
enqueueJob(url, data, callback, options) {
if (this.callbacks.hasOwnProperty(url)) {
this.callbacks[url].push(callback);
} else {
this.callbacks[url] = [callback];
const job = {
url,
data,
options
};
if (this.clients.length > 0) {
this.clients.shift().run(job);
} else {
this.queue.push(job);
}
}
}
enqueueClient(client) {
if (this.queue.length > 0) {
client.run(this.queue.shift());
} else {
this.clients.push(client);
}
}
handleResponse(url, err, data) {
const callback = this.callbacks[url];
if (err) {
for (let i = 0; i < callback.length; ++i) {
callback[i](err);
}
} else {
if (data.format === PIXELFORMAT_RGB565 || data.format === PIXELFORMAT_RGBA4) {
data.levels = data.levels.map((v) => {
return new Uint16Array(v);
});
} else {
data.levels = data.levels.map((v) => {
return new Uint8Array(v);
});
}
for (let i = 0; i < callback.length; ++i) {
callback[i](null, data);
}
}
delete this.callbacks[url];
}
}
class BasisClient {
constructor(queue2, config, eager) {
this.queue = queue2;
this.worker = new Worker(config.workerUrl);
this.worker.addEventListener("message", (message) => {
const data = message.data;
this.queue.handleResponse(data.url, data.err, data.data);
if (!this.eager) {
this.queue.enqueueClient(this);
}
});
this.worker.postMessage({ type: "init", config });
this.eager = eager;
}
run(job) {
const transfer = [];
if (job.data instanceof ArrayBuffer) {
transfer.push(job.data);
}
this.worker.postMessage({
type: "transcode",
url: job.url,
format: job.format,
data: job.data,
options: job.options
}, transfer);
if (this.eager) {
this.queue.enqueueClient(this);
}
}
}
const defaultNumWorkers = 1;
const defaultRgbPriority = ["etc2", "etc1", "astc", "dxt", "pvr", "atc"];
const defaultRgbaPriority = ["astc", "dxt", "etc2", "pvr", "atc"];
const defaultMaxRetries = 5;
const queue = new BasisQueue();
let lazyConfig = null;
let initializing = false;
function basisInitialize(config) {
if (initializing) {
return;
}
if (!config) {
config = lazyConfig || {};
} else if (config.lazyInit) {
lazyConfig = config;
return;
}
if (!config.glueUrl || !config.wasmUrl || !config.fallbackUrl) {
const moduleConfig = WasmModule.getConfig("BASIS");
if (moduleConfig) {
config = {
glueUrl: moduleConfig.glueUrl,
wasmUrl: moduleConfig.wasmUrl,
fallbackUrl: moduleConfig.fallbackUrl,
numWorkers: moduleConfig.numWorkers
};
}
}
if (config.glueUrl || config.wasmUrl || config.fallbackUrl) {
initializing = true;
const numWorkers = Math.max(1, Math.min(16, config.numWorkers || defaultNumWorkers));
const eagerWorkers = config.numWorkers === 1 || (config.hasOwnProperty("eagerWorkers") ? config.eagerWorkers : true);
config.rgbPriority = config.rgbPriority || defaultRgbPriority;
config.rgbaPriority = config.rgbaPriority || defaultRgbaPriority;
config.maxRetries = config.hasOwnProperty("maxRetries") ? config.maxRetries : defaultMaxRetries;
prepareWorkerModules(config, (err, clientConfig) => {
if (err) {
console.error(`failed to initialize basis worker: ${err}`);
} else {
for (let i = 0; i < numWorkers; ++i) {
queue.enqueueClient(new BasisClient(queue, clientConfig, eagerWorkers));
}
}
});
}
}
let deviceDetails = null;
function basisTranscode(device, url, data, callback, options) {
basisInitialize();
if (!deviceDetails) {
deviceDetails = {
formats: getCompressionFormats(device)
};
}
queue.enqueueJob(url, data, callback, {
deviceDetails,
isGGGR: !!options?.isGGGR,
isKTX2: !!options?.isKTX2
});
return initializing;
}
export {
basisInitialize,
basisTranscode
};