Skip to content

Commit

Permalink
New singleton implementation; remove statics
Browse files Browse the repository at this point in the history
  • Loading branch information
alexchuber committed Dec 9, 2024
1 parent 215b2ec commit 0a2680a
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 126 deletions.
96 changes: 33 additions & 63 deletions packages/dev/core/src/Meshes/Compression/dracoCodec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Nullable } from "core/types";
import { Tools } from "../../Misc/tools";
import { AutoReleaseWorkerPool } from "../../Misc/workerPool";
import type { IDisposable } from "../../scene";
import { initializeWebWorker } from "./dracoCompressionWorker";
import { Logger } from "core/Misc";

/**
* Configuration for using a Draco codec.
Expand Down Expand Up @@ -68,39 +68,9 @@ export abstract class DracoCodec<M> implements IDisposable {
protected _modulePromise?: Promise<{ module: M }>;

/**
* The configuration for the Draco codec.
* Subclasses should override this value with a valid configuration.
* The default configuration for the codec.
*/
public static Config: IDracoCodecConfiguration;

/**
* Returns true if the codec's `Configuration` is available.
*/
public static get Available(): boolean {
return !!((this.Config.wasmUrl && this.Config.wasmBinaryUrl && typeof WebAssembly === "object") || this.Config.fallbackUrl);
}

/**
* The default draco compression object
* Subclasses should override this value using the narrowed type of the subclass,
* and define a public `get Default()` that returns the narrowed instance.
*/
protected static _Default: Nullable<DracoCodec<unknown>>;

/**
* Reset the default draco compression object to null and disposing the removed default instance.
* Note that if the workerPool is a member of the static Configuration object it is recommended not to run dispose,
* unless the static worker pool is no longer needed.
* @param skipDispose set to true to not dispose the removed default instance
*/
public static ResetDefault(skipDispose?: boolean): void {
if (this._Default) {
if (!skipDispose) {
this._Default.dispose();
}
this._Default = null;
}
}
protected abstract readonly _defaultConfig: IDracoCodecConfiguration;

/**
* Checks if the default codec JS module is in scope.
Expand All @@ -118,37 +88,49 @@ export abstract class DracoCodec<M> implements IDisposable {
protected abstract _getWorkerContent(): string;

/**
* Constructor
* @param _config The configuration for the DracoCodec instance.
* Loads the codec module and worker pool if needed.
* @param config An optional configuration for this DracoDecoder. Defaults to the following:
* - `numWorkers`: 50% of the available logical processors, capped to 4. If no logical processors are available, defaults to 1.
* - `wasmUrl`: `"https://cdn.babylonjs.com/draco_wasm_wrapper_gltf.js"` (decoder)
* - `wasmBinaryUrl`: `"https://cdn.babylonjs.com/draco_decoder_gltf.wasm"` (decoder)
* - `fallbackUrl`: `"https://cdn.babylonjs.com/draco_decoder_gltf.js"` (decoder)
* @returns A promise that resolves when the decoder is ready (module loaded and/or worker pool initialized)
*/
constructor(_config: IDracoCodecConfiguration) {
const config = { numWorkers: _GetDefaultNumWorkers(), ..._config };
public async initialize(config?: IDracoCodecConfiguration): Promise<void> {
if (this._workerPoolPromise || this._modulePromise) {
Logger.Warn("Draco codec is already initialized. If a configuration change is needed, call dispose() before re-initializing.");
return;
}

const mergedConfig = { numWorkers: _GetDefaultNumWorkers(), ...this._defaultConfig, ...config };
// check if the decoder binary and worker pool was injected
// Note - it is expected that the developer checked if WebWorker, WebAssembly and the URL object are available
if (config.workerPool) {
if (mergedConfig.workerPool) {
// Set the promise accordingly
this._workerPoolPromise = Promise.resolve(config.workerPool);
this._workerPoolPromise = Promise.resolve(mergedConfig.workerPool);
return;
}

// to avoid making big changes to the code here, if wasmBinary is provided use it in the wasmBinaryPromise
const wasmBinaryProvided = config.wasmBinary;
const numberOfWorkers = config.numWorkers;
const wasmBinaryProvided = mergedConfig.wasmBinary;
const numberOfWorkers = mergedConfig.numWorkers;
const useWorkers = numberOfWorkers && typeof Worker === "function" && typeof URL === "function";
const urlNeeded = useWorkers || (!useWorkers && !config.jsModule);
const urlNeeded = useWorkers || (!useWorkers && !mergedConfig.jsModule);
// code maintained here for back-compat with no changes

const codecInfo: { url: string | undefined; wasmBinaryPromise: Promise<ArrayBuffer | undefined> } =
config.wasmUrl && config.wasmBinaryUrl && typeof WebAssembly === "object"
mergedConfig.wasmUrl && mergedConfig.wasmBinaryUrl && typeof WebAssembly === "object"
? {
url: urlNeeded ? Tools.GetBabylonScriptURL(config.wasmUrl, true) : "",
wasmBinaryPromise: wasmBinaryProvided ? Promise.resolve(wasmBinaryProvided) : Tools.LoadFileAsync(Tools.GetBabylonScriptURL(config.wasmBinaryUrl, true)),
url: urlNeeded ? Tools.GetBabylonScriptURL(mergedConfig.wasmUrl, true) : "",
wasmBinaryPromise: wasmBinaryProvided
? Promise.resolve(wasmBinaryProvided)
: Tools.LoadFileAsync(Tools.GetBabylonScriptURL(mergedConfig.wasmBinaryUrl, true)),
}
: {
url: urlNeeded ? Tools.GetBabylonScriptURL(config.fallbackUrl!) : "",
url: urlNeeded ? Tools.GetBabylonScriptURL(mergedConfig.fallbackUrl!) : "",
wasmBinaryPromise: Promise.resolve(undefined),
};
// If using workers, initialize a worker pool with either the wasm or url?
// If using workers, initialize a worker pool with either the wasm or url
if (useWorkers) {
this._workerPoolPromise = codecInfo.wasmBinaryPromise.then((wasmBinary) => {
const workerContent = this._getWorkerContent();
Expand All @@ -159,32 +141,20 @@ export abstract class DracoCodec<M> implements IDisposable {
return initializeWebWorker(worker, wasmBinary, codecInfo.url);
});
});
await this._workerPoolPromise;
return;
} else {
this._modulePromise = codecInfo.wasmBinaryPromise.then(async (wasmBinary) => {
if (this._isModuleAvailable()) {
if (!config.jsModule) {
if (!mergedConfig.jsModule) {
if (!codecInfo.url) {
throw new Error("Draco codec module is not available");
}
await Tools.LoadBabylonScriptAsync(codecInfo.url);
}
}
return this._createModuleAsync(wasmBinary as ArrayBuffer, config.jsModule);
return this._createModuleAsync(wasmBinary as ArrayBuffer, mergedConfig.jsModule);
});
}
}

/**
* Returns a promise that resolves when ready. Call this manually to ensure draco compression is ready before use.
* @returns a promise that resolves when ready
*/
public async whenReadyAsync(): Promise<void> {
if (this._workerPoolPromise) {
await this._workerPoolPromise;
return;
}

if (this._modulePromise) {
await this._modulePromise;
return;
}
Expand Down
61 changes: 55 additions & 6 deletions packages/dev/core/src/Meshes/Compression/dracoCompression.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { _GetDefaultNumWorkers } from "./dracoCodec";
import type { IDracoCodecConfiguration } from "./dracoCodec";
import { DracoDecoder } from "./dracoDecoder";
import { DefaultDecoderConfig, DracoDecoderClass } from "./dracoDecoder";
import { VertexBuffer } from "../buffer";
import { VertexData } from "../mesh.vertexData";
import type { Nullable } from "core/types";

/**
* Configuration for Draco compression
Expand Down Expand Up @@ -54,36 +55,84 @@ export interface IDracoCompressionOptions extends Pick<IDracoCodecConfiguration,
*
* @see https://playground.babylonjs.com/#DMZIBD#0
*/
export class DracoCompression extends DracoDecoder {
export class DracoCompression extends DracoDecoderClass {
/**
* The configuration. Defaults to the following urls:
* - wasmUrl: "https://cdn.babylonjs.com/draco_wasm_wrapper_gltf.js"
* - wasmBinaryUrl: "https://cdn.babylonjs.com/draco_decoder_gltf.wasm"
* - fallbackUrl: "https://cdn.babylonjs.com/draco_decoder_gltf.js"
*/
public static Configuration: IDracoCompressionConfiguration = {
decoder: DracoDecoder.Config, // TODO: Remove this reference or update the JSDoc with warning.
decoder: { ...DefaultDecoderConfig },
};

/**
* Returns true if the decoder configuration is available.
*/
public static get DecoderAvailable(): boolean {
return DracoDecoder.Available; // TODO: Remove this reference or update the JSDoc with warning.
const decoder = DracoCompression.Configuration.decoder;
return !!((decoder.wasmUrl && decoder.wasmBinaryUrl && typeof WebAssembly === "object") || decoder.fallbackUrl);
}

/**
* Default number of workers to create when creating the draco compression object.
*/
public static DefaultNumWorkers = _GetDefaultNumWorkers();

protected static _Default: Nullable<DracoCompression>;

/**
* Default instance for the draco compression object.
*/
public static get Default(): DracoCompression {
DracoCompression._Default ??= new DracoCompression();
return DracoCompression._Default;
}

/**
* Reset the default draco compression object to null and disposing the removed default instance.
* Note that if the workerPool is a member of the static Configuration object it is recommended not to run dispose,
* unless the static worker pool is no longer needed.
* @param skipDispose set to true to not dispose the removed default instance
*/
public static ResetDefault(skipDispose?: boolean): void {
if (DracoCompression._Default) {
if (!skipDispose) {
DracoCompression._Default.dispose();
}
DracoCompression._Default = null;
}
}

/**
* Constructor
* @param numWorkersOrConfig The number of workers for async operations or a config object. Specify `0` to disable web workers and run synchronously in the current context.
*/
constructor(numWorkersOrConfig: number | IDracoCompressionOptions = DracoCompression.DefaultNumWorkers) {
const config = typeof numWorkersOrConfig === "number" ? { ...DracoDecoder.Config, numWorkers: numWorkersOrConfig } : { ...DracoDecoder.Config, ...numWorkersOrConfig };
super(config);
super();
// Derive config this way to maintain backwards compatibility with "numWorkers"
const mergedConfig = {
...DracoCompression.Configuration.decoder,
...(typeof numWorkersOrConfig === "number" ? { numWorkers: numWorkersOrConfig } : numWorkersOrConfig),
};
// Explicitly initialize here for backwards compatibility
this.initialize(mergedConfig);
}

/**
* Returns a promise that resolves when ready. Call this manually to ensure draco compression is ready before use.
* @returns a promise that resolves when ready
*/
public async whenReadyAsync(): Promise<void> {
if (this._workerPoolPromise) {
await this._workerPoolPromise;
return;
}

if (this._modulePromise) {
await this._modulePromise;
return;
}
}

/**
Expand Down
102 changes: 46 additions & 56 deletions packages/dev/core/src/Meshes/Compression/dracoDecoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,55 +20,20 @@ interface MeshData {
totalVertices: number;
}

/** @internal Used for `DracoCompression` */
export const DefaultDecoderConfig: IDracoCodecConfiguration = {
wasmUrl: `${Tools._DefaultCdnUrl}/draco_wasm_wrapper_gltf.js`,
wasmBinaryUrl: `${Tools._DefaultCdnUrl}/draco_decoder_gltf.wasm`,
fallbackUrl: `${Tools._DefaultCdnUrl}/draco_decoder_gltf.js`,
};

/**
* @experimental This class is an experimental version of `DracoCompression` and is subject to change.
*
* Draco compression (https://google.github.io/draco/)
*
* This class wraps the Draco decoder module.
*
* By default, the configuration points to a copy of the Draco decoder files for glTF from the Babylon.js preview cdn https://preview.babylonjs.com/draco_wasm_wrapper_gltf.js.
*
* To update the configuration, use the following code:
* ```javascript
* DracoDecoder.Config = {
* wasmUrl: "<url to the WebAssembly library>",
* wasmBinaryUrl: "<url to the WebAssembly binary>",
* fallbackUrl: "<url to the fallback JavaScript library>",
* };
* ```
*
* Draco has two versions, one for WebAssembly and one for JavaScript. The decoder configuration can be set to only support WebAssembly or only support the JavaScript version.
* Decoding will automatically fallback to the JavaScript version if WebAssembly version is not configured or if WebAssembly is not supported by the browser.
* Use `DracoDecoder.Available` to determine if the decoder configuration is available for the current context.
*
* To decode Draco compressed data, get the default DracoDecoder object and call decodeMeshToGeometryAsync:
* ```javascript
* var geometry = await DracoDecoder.Default.decodeMeshToGeometryAsync(data);
* ```
* It should not be constructed directly. Use `DracoDecoder` instead.
* @internal
*/
export class DracoDecoder extends DracoCodec<DecoderModule> {
protected static override _Default: Nullable<DracoDecoder> = null;
/**
* Default instance for the DracoDecoder.
*/
public static get Default(): DracoDecoder {
DracoDecoder._Default ??= new DracoDecoder();
return DracoDecoder._Default;
}

/**
* Configuration for the DracoDecoder. Defaults to the following:
* - numWorkers: 50% of the available logical processors, capped to 4. If no logical processors are available, defaults to 1.
* - wasmUrl: `"https://cdn.babylonjs.com/draco_wasm_wrapper_gltf.js"`
* - wasmBinaryUrl: `"https://cdn.babylonjs.com/draco_decoder_gltf.wasm"`
* - fallbackUrl: `"https://cdn.babylonjs.com/draco_decoder_gltf.js"`
*/
public static override Config: IDracoCodecConfiguration = {
wasmUrl: `${Tools._DefaultCdnUrl}/draco_wasm_wrapper_gltf.js`,
wasmBinaryUrl: `${Tools._DefaultCdnUrl}/draco_decoder_gltf.wasm`,
fallbackUrl: `${Tools._DefaultCdnUrl}/draco_decoder_gltf.js`,
};
export class DracoDecoderClass extends DracoCodec<DecoderModule> {
protected override readonly _defaultConfig: IDracoCodecConfiguration = DefaultDecoderConfig;

protected override _isModuleAvailable(): boolean {
return typeof DracoDecoderModule !== "undefined";
Expand All @@ -83,15 +48,6 @@ export class DracoDecoder extends DracoCodec<DecoderModule> {
return `${decodeMesh}(${workerFunction})()`;
}

/**
* Creates a new Draco decoder.
* @param config Optional override of the configuration for the DracoDecoder. If not provided, defaults to `DracoDecoder.Config`.
*/
constructor(config?: IDracoCodecConfiguration) {
// Order of final config will be config > DracoDecoder.Config.
super({ ...DracoDecoder.Config, ...(config ?? {}) });
}

/**
* Decode Draco compressed mesh data to mesh data.
* @param data The ArrayBuffer or ArrayBufferView for the Draco compression data
Expand Down Expand Up @@ -200,7 +156,8 @@ export class DracoDecoder extends DracoCodec<DecoderModule> {
});
}

throw new Error("Draco decoder module is not available");
throw new Error("Draco decoder module is not available. Have you called initialize()?");
// NOTE: Alternatively, we could initialize the module here if the user forgot.
}

/**
Expand Down Expand Up @@ -279,3 +236,36 @@ export class DracoDecoder extends DracoCodec<DecoderModule> {
return geometry;
}
}

/**
* @experimental
* Draco compression (https://google.github.io/draco/)
*
* DracoDecoder is a singleton for decoding Draco-compressed meshes.
* It is automatically constructed when the module is imported.
*
* To decode Draco compressed data, see the following example:
* ```javascript
* DracoDecoder.initialize(//Optional configuration//);
* var geometry = await DracoDecoder.decodeMeshToGeometryAsync(data);
// Perform any additional decoding here
* DracoDecoder.dispose();
* ```
*
* Before using the DracoDecoder, call initialize to ensure the decoder is ready.
* By default, the initialization configuration points to a copy of the Draco decoder files for glTF from the Babylon.js preview cdn https://preview.babylonjs.com/draco_wasm_wrapper_gltf.js.
*
* This configuration can be customized at initialization using the following code:
* ```javascript
* DracoDecoder.initialize({
* wasmUrl: "<url to the WebAssembly library>",
* wasmBinaryUrl: "<url to the WebAssembly binary>",
* fallbackUrl: "<url to the fallback JavaScript library>",
* });
* ```
*
* Draco has two versions, one for WebAssembly and one for JavaScript. The decoder configuration can be set to only support WebAssembly or only support the JavaScript version.
* Decoding will automatically fallback to the JavaScript version if WebAssembly version is not configured or if WebAssembly is not supported by the browser.
* For custom configurations, it is assumed that the developer has verified its availability in the current context (i.e., the WebAssembly version is correctly configured or a valid fallback exists.)
*/
export const DracoDecoder = new DracoDecoderClass();
2 changes: 1 addition & 1 deletion packages/dev/core/src/Meshes/Compression/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from "./dracoCompression";
export * from "./meshoptCompression";
export * from "./dracoDecoder";
export { DracoDecoder as DracoDecoderInstance } from "./dracoDecoder";

0 comments on commit 0a2680a

Please sign in to comment.