import UmbraNativeAPI from './native.js' import * as Native from './NativeTypes' import * as wrappers from './wrappers' import { TextureSupportFlags } from './NativeTypes' import { wrapNativeFunctions } from './NativeFunctions' import { NativeModule } from './NativeModule.js' import * as API from './PublicAPI' import './Mesh' import { LibraryInfo } from './NativeEnums.js' type PrivateUmbraInstance = API.UmbraInstance & { nativeModule: NativeModule } type LibraryConfig = { wasmURL?: string workerScriptURL?: string numWorkers?: number } let umbrajsScriptDir = '' if (typeof document !== 'undefined' && document.currentScript) { const dir = (document.currentScript as any).src if (dir !== 'undefined') { umbrajsScriptDir = dir.substr(0, dir.lastIndexOf('/') + 1) } } /** * Returns a library instance that uses the Emscripten resources of "Module". */ function instantiate(Module: NativeModule, config: LibraryConfig): API.UmbraInstance { const lib = {} as PrivateUmbraInstance /** * Queries supported texture formats and also enables the relevant WebGL extensions. */ lib.getPlatformFeatures = (gl: WebGLRenderingContext): API.PlatformFeatures => { let flags = 0 // We accumulate a texture format bitmask here const formats = new Set() // Will hold all supported format names let supportsSRGB = false let supportsHalfFloat = false /** * We fetch all extension strings and strip off possible vendor prefixes of WEBGL * extensions. However, when we enable the extension we need the original full string. * Here 'extStrings' holds a mapping from a shortened to a full extension string. */ const extStrings = new Map([]) // Full strings const extNames = [] // Shortened strings const allExtensions = gl.getSupportedExtensions() for (const full of allExtensions) { const shortened = full.replace(/^(.*)_WEBGL_/, 'WEBGL_') extStrings.set(shortened, full) extNames.push(shortened) } const mapping = new Map([ [ 'WEBGL_compressed_texture_s3tc', { mask: TextureSupportFlags.BC1 | TextureSupportFlags.BC2 | TextureSupportFlags.BC3, names: ['bc1', 'bc2', 'bc3'], }, ], [ 'WEBGL_compressed_texture_s3tc_srgb', { mask: TextureSupportFlags.BC1 | TextureSupportFlags.BC2 | TextureSupportFlags.BC3, names: ['bc1', 'bc2', 'bc3'], }, ], [ 'WEBGL_compressed_texture_rgtc', { mask: TextureSupportFlags.BC4 | TextureSupportFlags.BC5, names: ['bc4', 'bc5'], }, ], [ 'WEBGL_compressed_texture_pvrtc', { mask: TextureSupportFlags.PVRTC1, names: ['pvrtc1_rgb4', 'pvrtc1_rgba4'], }, ], [ 'WEBGL_compressed_texture_etc1', { mask: TextureSupportFlags.ETC1, names: ['etc1_rgb'], }, ], [ 'WEBGL_compressed_texture_astc', { mask: TextureSupportFlags.ASTC, names: ['astc_4x4'], }, ], ]) for (const key of mapping.keys()) { if (extNames.includes(key)) { gl.getExtension(extStrings.get(key)) const value = mapping.get(key) flags |= value.mask value.names.forEach(name => formats.add(name)) } } if (extNames.includes('WEBGL_compressed_texture_s3tc_srgb')) { supportsSRGB = true } if (extNames.includes('EXT_sRGB')) { gl.getExtension(extStrings.get('EXT_sRGB')) supportsSRGB = true } if (extNames.includes('OES_texture_half_float')) { gl.getExtension(extStrings.get('OES_texture_half_float')) supportsHalfFloat = true } return { textureSupportMask: flags, formats: [...formats], srgb: supportsSRGB, halfFloat: supportsHalfFloat, } } // Access to the Emscripten module is needed for deinitialization lib.nativeModule = Module // HACK: Store library config in the "global" nativeModule object so that // Runtime's constructor can fetch web worker file paths. // We take all paths in at the same time because of Emscripten's // init order but now we need to pass them all the way down to Runtime. ;(lib.nativeModule as any)['umbraLibraryConfig'] = config // Allow access to Runtime class lib.createRuntime = wrappers.create(Module).createRuntime lib.getStreamingInfo = () => { return { minBytesDownloaded: lib.nativeModule.minBytesDownloaded, maxBytesDownloaded: lib.nativeModule.maxBytesDownloaded, } } lib.getLibraryInfo = () => { return { version: Module.getLibraryInfo(Native.LibraryInfo.Version), copyright: Module.getLibraryInfo(Native.LibraryInfo.Copyright), buildTime: Module.getLibraryInfo(Native.LibraryInfo.BuildTime), buildID: Module.getLibraryInfo(Native.LibraryInfo.BuildId), } } lib.abortDownloads = () => { Object.values(Module.wgetRequests).forEach((req: XMLHttpRequest) => { req.abort() }) } return lib } /** * Compiles the WebAssembly and code and initializes the Emscripten environment. * Returns a Promise that resolves with the library instance ready for use. */ export const initUmbra = function(config: LibraryConfig) { const defaults: LibraryConfig = { wasmURL: umbrajsScriptDir + 'umbra.wasm', workerScriptURL: umbrajsScriptDir + 'UmbraAssetWorker.js', } config = Object.assign(defaults, config) return new Promise((resolve, reject) => { try { const redirectWasmURL = (path, prefix) => { // The main WASM file's URL must be changed here because it's loaded during startup // using Emscripten's "locateFile()" API using a hardcoded 'umbra.wasm' path. if (path.endsWith('umbra.wasm')) { return config.wasmURL } return prefix + path } UmbraNativeAPI({ locateFile: redirectWasmURL }).then(Module => { // A workaround for https://github.com/emscripten-core/emscripten/issues/5820 delete Module['then'] Module['onAbort'] = what => { reject(what) } wrapNativeFunctions(Module) resolve(instantiate(Module, config)) }) } catch (e) { reject(e) } }) as Promise } /** * Deinitializes the Emscripten environment. * * NOTE: We assume here that async IndexedDB loads and stores have already finished. * This is the case if Umbra.update() was *not* called before deinitUmbra() on the * same frame. */ export const deinitUmbra = function(Umbra: API.UmbraInstance): void { // Pending requests must be aborted so that native handlers won't get called Umbra.abortDownloads() const privateUmbra = Umbra as PrivateUmbraInstance // Emscripten's exit(0) call throws an ExitStatus exception try { privateUmbra.nativeModule._exit(0) } catch (e) { if (e.name !== 'ExitStatus') { throw e } } } export { LibraryConfig, LibraryInfo } export * from './PublicAPI'