Skip to content

Commit

Permalink
Static draco worker (#7473)
Browse files Browse the repository at this point in the history
* Avoid using new Blob(...) in DRACOLoader.ts and make worker static
* Add google-analytics to img-src
* Ignore draco worker during linting
* Merge branch 'master' into static-draco-worker
  • Loading branch information
daniel-wer authored and philippotto committed Dec 5, 2023
1 parent c2d030b commit 68842e8
Show file tree
Hide file tree
Showing 5 changed files with 309 additions and 325 deletions.
2 changes: 1 addition & 1 deletion conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ play {
connect-src = "'self' ws://localhost:9002 app.olvy.co api.github.com gist.github.com *.google-analytics.com api.airbrake.io"
frame-src = "'self' webknossos.olvy.co"
# *.voxelytics.com is needed for images embedded in Voxelytics reports.
img-src = "'self' data: *.googletagmanager.com *.voxelytics.com"
img-src = "'self' data: *.googletagmanager.com *.google-analytics.com *.voxelytics.com"
media-src = "*.webknossos.org"
font-src = "'self' data:"
}
Expand Down
213 changes: 7 additions & 206 deletions frontend/javascripts/libs/DRACOLoader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// @ts-nocheck
/* eslint-disable */
// Copied from https://github.com/mrdoob/three.js/pull/25475 / DRACOLoader.js to fix ERR_REQUIRE_ESM error.
// Adapted to avoid using `new Blob(...)` to create the worker but instead use a static worker file.
// This removes support for devices that don't support WebAssembly, but enables a more secure Content Security Policy.
import {
BufferAttribute,
BufferGeometry,
Expand All @@ -25,7 +27,6 @@ class DRACOLoader extends Loader {
this.workerLimit = 4;
this.workerPool = [];
this.workerNextTaskID = 1;
this.workerSourceURL = "";

this.defaultAttributeIDs = {
position: "POSITION",
Expand Down Expand Up @@ -224,34 +225,10 @@ class DRACOLoader extends Loader {
_initDecoder() {
if (this.decoderPending) return this.decoderPending;

const useJS = typeof WebAssembly !== "object" || this.decoderConfig.type === "js";
const librariesPending = [];
const wasmLibraryPromise = this._loadLibrary("draco_decoder.wasm", "arraybuffer");

if (useJS) {
librariesPending.push(this._loadLibrary("draco_decoder.js", "text"));
} else {
librariesPending.push(this._loadLibrary("draco_wasm_wrapper.js", "text"));
librariesPending.push(this._loadLibrary("draco_decoder.wasm", "arraybuffer"));
}

this.decoderPending = Promise.all(librariesPending).then((libraries) => {
const jsContent = libraries[0];

if (!useJS) {
this.decoderConfig.wasmBinary = libraries[1];
}

const fn = DRACOWorker.toString();

const body = [
"/* draco decoder */",
jsContent,
"",
"/* worker */",
fn.substring(fn.indexOf("{") + 1, fn.lastIndexOf("}")),
].join("\n");

this.workerSourceURL = URL.createObjectURL(new Blob([body]));
this.decoderPending = wasmLibraryPromise.then((library) => {
this.decoderConfig.wasmBinary = library;
});

return this.decoderPending;
Expand All @@ -260,7 +237,8 @@ class DRACOLoader extends Loader {
_getWorker(taskID, taskCost) {
return this._initDecoder().then(() => {
if (this.workerPool.length < this.workerLimit) {
const worker = new Worker(this.workerSourceURL);
// See https://webpack.js.org/guides/web-workers/
const worker = new Worker(new URL("./DRACOWorker.worker.js", import.meta.url));

worker._callbacks = {};
worker._taskCosts = {};
Expand Down Expand Up @@ -319,185 +297,8 @@ class DRACOLoader extends Loader {

this.workerPool.length = 0;

if (this.workerSourceURL !== "") {
URL.revokeObjectURL(this.workerSourceURL);
}

return this;
}
}

/* WEB WORKER */

function DRACOWorker() {
let decoderConfig;
let decoderPending;

onmessage = function (e) {
const message = e.data;

switch (message.type) {
case "init":
decoderConfig = message.decoderConfig;
decoderPending = new Promise(function (resolve /*, reject*/) {
decoderConfig.onModuleLoaded = function (draco) {
// Module is Promise-like. Wrap before resolving to avoid loop.
resolve({ draco: draco });
};

DracoDecoderModule(decoderConfig); // eslint-disable-line no-undef
});
break;

case "decode":
const buffer = message.buffer;
const taskConfig = message.taskConfig;
decoderPending.then((module) => {
const draco = module.draco;
const decoder = new draco.Decoder();

try {
const geometry = decodeGeometry(draco, decoder, new Int8Array(buffer), taskConfig);

const buffers = geometry.attributes.map((attr) => attr.array.buffer);

if (geometry.index) buffers.push(geometry.index.array.buffer);

self.postMessage({ type: "decode", id: message.id, geometry }, buffers);
} catch (error) {
console.error(error);

self.postMessage({ type: "error", id: message.id, error: error.message });
} finally {
draco.destroy(decoder);
}
});
break;
}
};

function decodeGeometry(draco, decoder, array, taskConfig) {
const attributeIDs = taskConfig.attributeIDs;
const attributeTypes = taskConfig.attributeTypes;

let dracoGeometry;
let decodingStatus;
const geometryType = decoder.GetEncodedGeometryType(array);

if (geometryType === draco.TRIANGULAR_MESH) {
dracoGeometry = new draco.Mesh();
decodingStatus = decoder.DecodeArrayToMesh(array, array.byteLength, dracoGeometry);
} else if (geometryType === draco.POINT_CLOUD) {
dracoGeometry = new draco.PointCloud();
decodingStatus = decoder.DecodeArrayToPointCloud(array, array.byteLength, dracoGeometry);
} else {
throw new Error("THREE.DRACOLoader: Unexpected geometry type.");
}

if (!decodingStatus.ok() || dracoGeometry.ptr === 0) {
throw new Error("THREE.DRACOLoader: Decoding failed: " + decodingStatus.error_msg());
}

const geometry = { index: null, attributes: [] };

// Gather all vertex attributes.
for (const attributeName in attributeIDs) {
const attributeType = self[attributeTypes[attributeName]];

let attribute;
let attributeID;

// A Draco file may be created with default vertex attributes, whose attribute IDs
// are mapped 1:1 from their semantic name (POSITION, NORMAL, ...). Alternatively,
// a Draco file may contain a custom set of attributes, identified by known unique
// IDs. glTF files always do the latter, and `.drc` files typically do the former.
if (taskConfig.useUniqueIDs) {
attributeID = attributeIDs[attributeName];
attribute = decoder.GetAttributeByUniqueId(dracoGeometry, attributeID);
} else {
attributeID = decoder.GetAttributeId(dracoGeometry, draco[attributeIDs[attributeName]]);

if (attributeID === -1) continue;

attribute = decoder.GetAttribute(dracoGeometry, attributeID);
}

const attributeResult = decodeAttribute(
draco,
decoder,
dracoGeometry,
attributeName,
attributeType,
attribute,
);

if (attributeName === "color") {
attributeResult.vertexColorSpace = taskConfig.vertexColorSpace;
}

geometry.attributes.push(attributeResult);
}

// Add index.
if (geometryType === draco.TRIANGULAR_MESH) {
geometry.index = decodeIndex(draco, decoder, dracoGeometry);
}

draco.destroy(dracoGeometry);

return geometry;
}

function decodeIndex(draco, decoder, dracoGeometry) {
const numFaces = dracoGeometry.num_faces();
const numIndices = numFaces * 3;
const byteLength = numIndices * 4;

const ptr = draco._malloc(byteLength);
decoder.GetTrianglesUInt32Array(dracoGeometry, byteLength, ptr);
const index = new Uint32Array(draco.HEAPF32.buffer, ptr, numIndices).slice();
draco._free(ptr);

return { array: index, itemSize: 1 };
}

function decodeAttribute(draco, decoder, dracoGeometry, attributeName, attributeType, attribute) {
const numComponents = attribute.num_components();
const numPoints = dracoGeometry.num_points();
const numValues = numPoints * numComponents;
const byteLength = numValues * attributeType.BYTES_PER_ELEMENT;
const dataType = getDracoDataType(draco, attributeType);

const ptr = draco._malloc(byteLength);
decoder.GetAttributeDataArrayForAllPoints(dracoGeometry, attribute, dataType, byteLength, ptr);
const array = new attributeType(draco.HEAPF32.buffer, ptr, numValues).slice();
draco._free(ptr);

return {
name: attributeName,
array: array,
itemSize: numComponents,
};
}

function getDracoDataType(draco, attributeType) {
switch (attributeType) {
case Float32Array:
return draco.DT_FLOAT32;
case Int8Array:
return draco.DT_INT8;
case Int16Array:
return draco.DT_INT16;
case Int32Array:
return draco.DT_INT32;
case Uint8Array:
return draco.DT_UINT8;
case Uint16Array:
return draco.DT_UINT16;
case Uint32Array:
return draco.DT_UINT32;
}
}
}

export { DRACOLoader };
Loading

0 comments on commit 68842e8

Please sign in to comment.