import { Primitive, TypedArray, TypedArrayConstructor, HeapBufferView } from './Heap' import * as Assets from './Assets' import * as Native from './NativeTypes' import { Pointer, UserPointer, TextureFormat, TextureSupportFlags } from './NativeTypes' import * as API from './PublicAPI' import * as UmbraMath from './Math' import { NativeModule, EmscriptenHeapName } from './NativeModule' import { AssetType, AssetLoadResult, VertexAttribute, TextureType } from './NativeEnums' import * as Mesh from './Mesh' import { WorkerPool } from './WorkerPool' import { LibraryConfig } from '.' const MAX_LIGHTS = 32 const MAX_NUM_WORKERS = 8 function assertInteger(x) { if (!Number.isInteger(x)) { throw new Error('Value was not integer: ' + x.toString()) } } function isArrayLike(x): boolean { return Array.isArray(x) || (ArrayBuffer.isView(x) && !(x instanceof DataView)) } function hardwareConcurrency() { if ('hardwareConcurrency' in navigator) { return navigator.hardwareConcurrency } return 1 } /** * We need to use a factory here since the native API classes depend on the Emscripten generated wasm code * that can't be loaded as a regular ES6 module. The "Module" object allows access to C++ functions and the * Emscripten heap. */ const create = (Module: NativeModule) => { const primitiveToArrayType = new Map([ [Primitive.Int8, Int8Array], [Primitive.Int16, Int16Array], [Primitive.Int32, Int32Array], [Primitive.Uint8, Uint8Array], [Primitive.Uint16, Uint16Array], [Primitive.Uint32, Uint32Array], [Primitive.Float32, Float32Array], [Primitive.Float64, Float64Array], ]) // gl.texImage2D expects these texture "type" and typed array combinations. // See https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texImage2D const textureFormatToPrimitive = new Map([ [TextureFormat.RGB565, Primitive.Uint16], [TextureFormat.RGBA32, Primitive.Uint8], [TextureFormat.RG8, Primitive.Uint8], [TextureFormat.RG16F, Primitive.Uint16], [TextureFormat.RGB_FLOAT32, Primitive.Float32], [TextureFormat.RGB24, Primitive.Uint8], [TextureFormat.UINT32, Primitive.Uint32], ]) /** * A Buffer is a block of memory in the Emscripten heap. Typed arrays may get detached when Emscripten * memory growth happens, so any JS code that wants to read from the heap needs to create its typed views * right before they are used. * * The views are safe to use during synchronous execution, but for example a 'yield' in a generator may * trigger memory growth before the caller gets the returned value. */ class Buffer { ptr: Pointer size: number constructor(size: number) { if (size === 0) { throw new Error('Buffer size was zero') } this.ptr = Module.umbraAlloc(size) if (this.ptr === 0) { throw new Error(`Allocation of ${size} bytes failed.`) } this.size = size // In bytes } destroy() { Module.umbraFree(this.ptr) this.ptr = 0 this.size = 0 } /** * A destructive resize operation that never shrinks the buffer. * NOTE: Does *not* copy the old data over. */ ensureSize(newSize: number) { if (this.size < newSize) { this.destroy() this.ptr = Module.umbraAlloc(newSize) if (this.ptr === 0) { throw new Error(`Buffer growth to ${newSize} bytes failed.`) } this.size = newSize } } floats() { return new Float32Array(Module.HEAPF32.buffer, this.ptr, this.size / 4) } bytes() { return new Uint8Array(Module.HEAPU8.buffer, this.ptr, this.size) } } /** * A HeapView holds a reference to a block of Emscripten heap memory. * Calling getArray() returns a typed view into the block. This class is needed because * regular TypedArray views into the heap get neutered (become unusable) at memory growth, * so we can't safely pass them to the application. */ class HeapView implements HeapBufferView { ptr: Pointer size: number heapName: EmscriptenHeapName arrayType: TypedArray primitiveToHeap = new Map([ [Primitive.Int8, 'HEAP8'], [Primitive.Int16, 'HEAP16'], [Primitive.Int32, 'HEAP32'], [Primitive.Uint8, 'HEAPU8'], [Primitive.Uint16, 'HEAPU16'], [Primitive.Uint32, 'HEAPU32'], [Primitive.Float32, 'HEAPF32'], [Primitive.Float64, 'HEAPF64'], ]) constructor(ptr: Pointer, size: number, type: Primitive) { this.ptr = ptr this.size = size // Find out which Emscripten heap view we need to use and which TypedArray constructor to call. if (this.primitiveToHeap.has(type) && primitiveToArrayType.has(type)) { this.heapName = this.primitiveToHeap.get(type) this.arrayType = primitiveToArrayType.get(type) } else { throw new TypeError(`BufferView constructor got invalid type '${type}'`) } } getArray(): TypedArray { // eslint-disable-next-line new-cap return new (this.arrayType as any)( Module[this.heapName].buffer, this.ptr, this.size / this.arrayType.BYTES_PER_ELEMENT, ) } } // Struct field offsets are required for zero-copy interop const renderableStructOffsets = Module.getRenderableMemberOffsets() function copyMat4(buf, elements) { for (let i = 0; i < 16; i++) { buf[i] = elements[i] } } function copyVec3(buf, elements) { buf[0] = elements[0] buf[1] = elements[1] buf[2] = elements[2] } class Client { ptr: Pointer configPtr: Pointer constructor() { this.configPtr = Module.umbraAlloc(Module.CONFIG_SIZE) Module.configInit(this.configPtr) this.ptr = Module.clientCreate('UmbraJS', this.configPtr) } destroy() { Module.clientDestroy(this.ptr) Module.umbraFree(this.configPtr) this.ptr = 0 } } class NativeScene implements API.NativeScene { ptr: Pointer private matrixBuffer = new Buffer(16 * 4) private infoBuffer = new Buffer(Module.sceneInfoSize) constructor(ptr) { this.ptr = ptr } destroy() { this.matrixBuffer.destroy() this.infoBuffer.destroy() Module.sceneDestroy(this.ptr) this.ptr = 0 } connectionStatus() { return Module.sceneGetConnectionStatus(this.ptr) } getInfo(): API.SceneInfo { Module.sceneGetInfo(this.ptr, this.infoBuffer.ptr) return Module.deserializeSceneInfo(this.infoBuffer.ptr) } isConnected(): boolean { return this.connectionStatus() === Native.ConnectionStatus.Connected } setTransform(matrix: UmbraMath.Matrix4x4) { if (!isArrayLike(matrix)) { throw new TypeError('Matrix should be an array') } if (matrix.length !== 16) { throw new TypeError('Matrix should be of size 4x4') } copyMat4(this.matrixBuffer.floats(), matrix) Module.sceneSetTransform(this.ptr, this.matrixBuffer.ptr) } } class View implements API.View { ptr: Pointer matrixBuffer: Buffer vectorBuffer: Buffer lightBuffer: Buffer temp: Buffer runtimeAssets: Map constructor(ptr, runtimeAssets) { this.ptr = ptr this.matrixBuffer = new Buffer(16 * 4) this.vectorBuffer = new Buffer(4 * 4) this.lightBuffer = new Buffer(MAX_LIGHTS * 3 * 4) this.temp = undefined this.runtimeAssets = runtimeAssets } destroy() { this.matrixBuffer.destroy() this.vectorBuffer.destroy() this.lightBuffer.destroy() if (this.temp) { this.temp.destroy() } Module.viewDestroy(this.ptr) this.ptr = 0 } setCamera( worldToClip: UmbraMath.Matrix4x4, positionVector: UmbraMath.Vector3, quality: number, lights: UmbraMath.Vector3[] = [], depthRange: Native.DepthRange = Native.DepthRange.MinusOneToOne, matrixFormat: Native.MatrixFormat = Native.MatrixFormat.ColumnMajor, ) { if (!isArrayLike(worldToClip)) { throw new TypeError('Camera matrix should be an array') } if (!isArrayLike(positionVector)) { throw new TypeError('Position should be an array') } if (typeof quality !== 'number') { throw new TypeError('Quality should be a number') } if (worldToClip.length !== 16) { throw new TypeError('WorldToClip matrix should be of size 4x4') } if (positionVector.length !== 3) { throw new TypeError('Position vector should be of length 3') } copyMat4(this.matrixBuffer.floats(), worldToClip) copyVec3(this.vectorBuffer.floats(), positionVector) if (lights) { if (lights.length > MAX_LIGHTS) { throw new Error('Too many lights given') } const buffer = this.lightBuffer.floats() for (let i = 0; i < lights.length; i++) { buffer[3 * i + 0] = lights[i][0] buffer[3 * i + 1] = lights[i][1] buffer[3 * i + 2] = lights[i][2] } } Module.viewUpdateRendering( this.ptr, this.matrixBuffer.ptr, this.vectorBuffer.ptr, depthRange, matrixFormat, quality, this.lightBuffer.ptr, lights.length, ) } getVisible(batchSize: number): API.Renderable[] { assertInteger(batchSize) const bufferSize = batchSize * Module.renderableSize // Check if we should enlarge or allocate the temp buffer if (!this.temp || this.temp.size < bufferSize) { if (this.temp) { this.temp.destroy() } this.temp = new Buffer(bufferSize) } const temp = this.temp const strideInWords = Module.renderableSize / 4 const bufferWordSize = bufferSize / 4 assertInteger(strideInWords) assertInteger(bufferWordSize) const getView = ( arrayType: TypedArrayConstructor, heap: TypedArray, ptr: Pointer, ofs: number, ) => { assertInteger(ofs / 4) // eslint-disable-next-line new-cap return new arrayType(heap.buffer, ptr + ofs, bufferWordSize - ofs / 4) } const meshIDs = getView( Uint32Array, Module.HEAPU32, temp.ptr, renderableStructOffsets['mesh'], ) const lodLevels = getView( Int32Array, Module.HEAP32, temp.ptr, renderableStructOffsets['lodLevel'], ) const masks = getView( Uint32Array, Module.HEAPU32, temp.ptr, renderableStructOffsets['visibilityMask'], ) // const transforms = getView(Float32Array, Module.HEAPF32, temp.ofs, offsets['transform']) const scenePointers = getView( Uint32Array, Module.HEAPU32, temp.ptr, renderableStructOffsets['scene'], ) const count = Module.viewNextRenderables(this.ptr, temp.ptr, batchSize) const output = [] for (let i = 0; i < count; i++) { const id = meshIDs[strideInWords * i] const lod = lodLevels[strideInWords * i] const mask = masks[strideInWords * i] const scenePtr = scenePointers[strideInWords * i] // TODO extract individual transforms too output.push({ id: id, mesh: this.runtimeAssets.get(id), lod: lod, mask: mask, scenePtr: scenePtr, } as API.Renderable) } return output } } class AssetLoad implements Assets.Load { ptr: Pointer assetType: Native.AssetType type: Assets.LoadType data: any // TODO: Replace with a union container data types // We generate user pointers in the library instead of letting the app do // them so that we can call prepare() before async mesh and texture loads. // The pointer is stored here in the load so that it can be recovered for // application handler callback. assetUserPointer: number constructor(ptr: Pointer, assetUserPointer: UserPointer) { this.ptr = ptr this.assetType = Module.assetLoadGetType(this.ptr) this.type = ('Load' + AssetType[this.assetType]) as Assets.LoadType this.data = {} // Type specific asset data set from the outside this.assetUserPointer = assetUserPointer } prepare() { Module.assetLoadPrepare(this.ptr, this.assetUserPointer) } finish(result: AssetLoadResult) { Module.assetLoadFinish(this.ptr, result) } } class AssetUnload implements Assets.Unload { ptr: Pointer assetType: Native.AssetType type: Assets.UnloadType userPointer: UserPointer data constructor(ptr: Pointer) { this.ptr = ptr this.assetType = Module.assetUnloadGetType(this.ptr) this.type = ('Unload' + AssetType[this.assetType]) as Assets.UnloadType this.userPointer = Module.assetUnloadGetUserPointer(this.ptr) } finish() { Module.assetUnloadFinish(this.ptr) } } class Runtime implements API.Runtime { assets: Map = new Map() private client: Client private ptr: Pointer private platformFeatures: API.PlatformFeatures private nextId = 1 private tempTextureBuffer = new Buffer(1024 * 1024) private tempStreamingStateBuffer = new Buffer(Module.streamingStateSize) private tempDispatchBuffer = new Buffer(1) private workerPool: WorkerPool private scratch: Buffer private debug: any private handlers: API.JobHandlers private stats = { numUpdates: 0, meshPipelineMemoryUse: 0, texturePipelineMemoryUse: 0, numWorkerLoadsPending: 0, perFrameLoadedTextures: 0, perFrameLoadedTextureBytes: 0, perFrameLoadedMeshes: 0, perFrameLoadedMeshBytes: 0, numInstantUnloads: 0, assetLoadTimes: new Map(), } private limits = { // A soft upper limit in bytes streamingPipeSizeLimit: 1 << 19, // 512 KiB // A maximum of number loads per worker queue maxWorkQueueLength: 10, } private loadPromises = new Map>() constructor( client: Client, { features = { textureSupportMask: 0, formats: [], srgb: false, halfFloat: false, }, flags = 0, }: API.RuntimeConfig, { workerScriptURL, numWorkers = 0 }: LibraryConfig, ) { /** * If no normal map formats are supported then force support for * BC5 so that textures get transcoded into an uncompressed format. */ const normalFormats = TextureSupportFlags.ETC1 | TextureSupportFlags.ASTC | TextureSupportFlags.PVRTC1 | TextureSupportFlags.BC5 let textureSupportMask = features.textureSupportMask if (!(textureSupportMask & normalFormats)) { textureSupportMask |= TextureSupportFlags.BC5 | TextureSupportFlags.BC4 } this.client = client const infoPtr = Module.serializeEnvironmentInfo( { textureSupportFlags: textureSupportMask, localCacheDirectory: null, localCacheMaximumByteSize: 0, }, 0, ) this.ptr = Module.runtimeCreate(client.ptr, infoPtr, flags) Module.umbraFree(infoPtr) this.platformFeatures = features const scratchSize = Math.max( Module.meshInfoSize, Module.materialInfoSize, Module.textureInfoSize, Module.byteBufferSize, ) this.scratch = new Buffer(scratchSize) this.debug = { textureFormatsInUse: new Set(), platformFeatures: features, trackInstantUnloads: false, } // Pick the number of workers with the same heuristic as the Web Player if (numWorkers == 0) { numWorkers = Math.min(Math.max(1, hardwareConcurrency() - 2), MAX_NUM_WORKERS) } this.workerPool = new WorkerPool(numWorkers, workerScriptURL) } destroy() { this.workerPool.destroy() this.tempTextureBuffer.destroy() this.tempStreamingStateBuffer.destroy() this.tempDispatchBuffer.destroy() this.scratch.destroy() Module.runtimeDestroy(this.ptr) this.client.destroy() this.ptr = 0 } createScenePublic(publicKey: string): NativeScene { const scenePtr = Module.sceneCreatePublic(this.ptr, publicKey) if (!scenePtr) { return undefined } return new NativeScene(scenePtr) } createScene(apiKey: string, locator: string): NativeScene { const scenePtr = Module.sceneCreate(this.ptr, apiKey, locator) if (!scenePtr) { return undefined } return new NativeScene(scenePtr) } createSceneLocal(url: string): NativeScene { const scenePtr = Module.sceneCreateLocal(this.ptr, url) if (!scenePtr) { return undefined } return new NativeScene(scenePtr) } setHandlers(handlers: API.JobHandlers): void { this.handlers = handlers } createView(): View { const viewPtr = Module.viewCreate(this.ptr) return new View(viewPtr, this.assets) } update() { Module.runtimeUpdate(this.ptr) } *getAssetUnloads(): IterableIterator { let p = Module.runtimeNextAssetUnload(this.ptr) while (p !== 0) { const unload = new AssetUnload(p) unload.data = this.assets.get(unload.userPointer) yield unload p = Module.runtimeNextAssetUnload(this.ptr) } } async dispatchTextureLoad(load: Assets.LoadTexture): Promise { const infoPtr = this.scratch.ptr Module.textureLoadGetInfo(load.ptr, infoPtr) const texture = Module.deserializeTextureInfo(infoPtr) as API.Texture const needsTranscoding = this.formatNeedsTranscoding(texture.format) load.prepare() let textureData: Assets.TextureData let uncompressedTexturePtr: Pointer = 0 if (needsTranscoding) { const serSize = Module.textureLoadGetSerializedSize(load.ptr) const headerSize = Module.textureLoadDispatchHeaderSize const textureLoadDispatchSize = serSize + headerSize this.tempDispatchBuffer.ensureSize(textureLoadDispatchSize) const dispatchBuffer = this.tempDispatchBuffer const desc = Module.convertAssetLoadToTextureDispatch( load.ptr, load.assetUserPointer, this.platformFeatures.textureSupportMask, this.platformFeatures.srgb, dispatchBuffer.ptr, textureLoadDispatchSize, ) if (typeof desc === 'undefined') { load.finish(AssetLoadResult.Failure) throw new Error('convertAssetLoadToTextureDispatch failed') } // We can't tell how big the transcoded texture will be but we assume it will be pretty // much the same size as the input. Since both input and output textures can still stay // allocated simultaneously, multiply the size with two two get a peak memory use estimate. const memoryUseEstimate = textureLoadDispatchSize * 2 this.stats.texturePipelineMemoryUse += memoryUseEstimate const best = this.workerPool.findMostAvailableWorker() let buffer // Launch the async worker transcode task try { ;({ buffer } = await this.workerPool.callWorker( best, 'loadTexture', new Uint8Array(dispatchBuffer.bytes()), // Copy the dispatch buffer { load, memoryUse: memoryUseEstimate, }, )) } catch ({ error }) { load.finish(AssetLoadResult.Failure) throw error } this.stats.texturePipelineMemoryUse -= memoryUseEstimate this.stats.perFrameLoadedTextures += 1 this.stats.perFrameLoadedTextureBytes += serSize uncompressedTexturePtr = Module.umbraAlloc(buffer.byteLength) // Int8Array.set(ArrayBuffer, ofs) works but ArrayBuffer is missing .length so we force the type Module.HEAP8.set((buffer as unknown) as ArrayLike, uncompressedTexturePtr) buffer = undefined const result = Module.deserializeTextureLoadResult(uncompressedTexturePtr) const umbraTexture = result.textureInfo let datatype = Primitive.Uint8 if (textureFormatToPrimitive.has(umbraTexture.format)) { datatype = textureFormatToPrimitive.get(umbraTexture.format) } else { console.warn( `Used default datatype for texture format ${TextureFormat[umbraTexture.format]}`, ) } const pixelData = new HeapView( uncompressedTexturePtr + Module.textureLoadResultHeaderSize, umbraTexture.dataByteSize, datatype, ) textureData = { info: umbraTexture, buffer: pixelData } } else { // No transcoding needed const buffer = this.tempTextureBuffer buffer.ensureSize(texture.dataByteSize) Module.serializeByteBuffer( { ptr: buffer.ptr, byteSize: texture.dataByteSize, // The output buffer size must match the texture data size flags: 0, }, this.scratch.ptr, ) if (!Module.textureLoadGetData(load.ptr, this.scratch.ptr)) { load.finish(AssetLoadResult.Failure) throw new Error( `textureLoadGetData failed: ${load.ptr}, ${buffer.ptr}, ${texture.dataByteSize}/${buffer.size}`, ) } const bufferView = new HeapView(buffer.ptr, texture.dataByteSize, Primitive.Uint8) textureData = { info: texture, buffer: bufferView } } try { // The handler must copy texture data if it wants to keep it for later. const result = this.handlers['LoadTexture'](textureData, load.assetUserPointer) load.finish(result) if (result === AssetLoadResult.Success) { this.debug.textureFormatsInUse.add(textureData.info.format) if (this.debug.trackInstantUnloads) { this.stats.assetLoadTimes.set(load.assetUserPointer, this.stats.numUpdates) } } return result } catch (error) { load.finish(AssetLoadResult.Failure) console.error(error) throw error } finally { if (uncompressedTexturePtr > 0) { Module.umbraFree(uncompressedTexturePtr) } } } async dispatchMaterialLoad(load: Assets.LoadMaterial): Promise { load.prepare() Module.materialLoadGetInfo(load.ptr, this.scratch.ptr) const info = Module.deserializeMaterialInfo(this.scratch.ptr) const texturePtrs = info.textures.filter(ptr => ptr !== Module.INVALID_USERPOINTER) // All texture loads have been started by now so the promises should exist. const pending = texturePtrs.map((id: UserPointer) => this.loadPromises.get(id)) // Catches errors and saves the texture load statuses to a list. All texture // promises are waited for before we check for errors. const texturePromises: Promise[] = pending.map(p => p.catch(e => e)) const resultsAndErrors: (AssetLoadResult | ErrorConstructor)[] = await Promise.all( texturePromises, ) // We waited for every dependency to finish so that we can now safely remove them // from "pendingAssetLoads" all at once. This is done even if some of them failed. texturePtrs.forEach(id => this.loadPromises.delete(id)) for (const result of resultsAndErrors) { if (result instanceof Error) { load.finish(AssetLoadResult.Failure) throw result } } // Now we know all dependencies resolved so the type can be updated. const results = resultsAndErrors as AssetLoadResult[] for (const result of results) { if (result !== AssetLoadResult.Success) { load.finish(AssetLoadResult.Aborted) return AssetLoadResult.Aborted } } // Texture loads were successful so we can fetch the objects uploaded by user. const textureObjects = texturePtrs.map((id: UserPointer) => this.assets.get(id)) const materialData = { transparent: info.transparent, textures: textureObjects, } as Assets.MaterialData try { const result = this.handlers['LoadMaterial'](materialData, load.assetUserPointer) load.finish(result) return result // resolves the promise stored in "pendingAssetLoads[load.assetUserPointer]" } catch (error) { load.finish(AssetLoadResult.Failure) console.error(error) throw error } } async dispatchMeshLoad(load: Assets.LoadMesh): Promise { Module.meshLoadGetInfo(load.ptr, this.scratch.ptr) const info = Module.deserializeMeshInfo(this.scratch.ptr) load.prepare() const serializedSize = Module.meshLoadGetSerializedSize(load.ptr) const dispatchSize = Module.meshLoadDispatchHeaderSize + serializedSize this.tempDispatchBuffer.ensureSize(dispatchSize) const buf = this.tempDispatchBuffer if ((buf.ptr & 0x0f) != 0) { console.error(`buf.ptr wasn't aligned: ${buf.ptr}`) } const desc = Module.convertAssetLoadToMeshDispatch( load.ptr, load.assetUserPointer, buf.ptr, dispatchSize, ) const materialUserPointer = info.material const materialPromise = this.loadPromises.get(materialUserPointer) if (typeof materialPromise === 'undefined') { load.finish(AssetLoadResult.Failure) throw new Error(`Material UserPointer ${materialUserPointer} didn't have a promise!`) } // Start async mesh decompression before waiting for "materialPromise" to finish. // This way both texture transcoding and mesh decompression can run in parallel. // Increase tracked memory use here and decrease it in the finish callback. const memoryUse = dispatchSize + desc.byteSize + Module.meshLoadResultHeaderSize this.stats.meshPipelineMemoryUse += memoryUse const workerId = this.workerPool.findMostAvailableWorker() let buffer try { // NOTE: We need to copy "buf" here because if "callWorker()" transfers the ownership // its a must, and even if it doesn't, then it's much faster to transfer a small copy // because otherwise the _whole heap_ would get copied over to a worker. // See https://bugs.chromium.org/p/chromium/issues/detail?id=169705 ;({ buffer } = await this.workerPool.callWorker( workerId, 'loadMesh', new Uint8Array(buf.bytes()), {}, )) } catch ({ error }) { load.finish(Assets.AssetLoadResult.Failure) throw error } finally { this.stats.meshPipelineMemoryUse -= memoryUse this.stats.perFrameLoadedMeshes += 1 this.stats.perFrameLoadedMeshBytes += serializedSize } const bufferByteSize = buffer.length * buffer.BYTES_PER_ELEMENT const ptr = Module.umbraAlloc(bufferByteSize) Module.HEAP8.set(buffer, ptr) buffer = undefined // we don't use "buffer" after we've copied it to the heap const obj = Module.deserializeMeshLoadResult(ptr) let failed = false let failureMessage: string // These errors should never happen if (obj.result !== Assets.AssetLoadResult.Success) { failureMessage = `Asset load failed with ${Assets.AssetLoadResult[obj.result]}` failed = true } if (obj.assetLoad !== load.ptr) { failureMessage = `Asset load ptr didn't match ${obj.assetLoad} vs ${load.ptr}` failed = true } if (failed) { Module.umbraFree(ptr) load.finish(Assets.AssetLoadResult.Failure) throw Error(failureMessage) } const basePtr = ptr + Module.meshLoadResultHeaderSize const bufferDescs = Module.meshLoadResultBuildUmbraBuffers(ptr, basePtr) const attributeArrayType = { position: Primitive.Float32, uv: Primitive.Float32, normal: Primitive.Float32, tangent: Primitive.Float32, index: bufferDescs.index.elementByteSize === 2 ? Primitive.Uint16 : Primitive.Uint32, } const meshAttributes = {} for (const name of Object.keys(bufferDescs)) { const vbuf = {} const elemBuf: Native.ElementBuffer = bufferDescs[name] const attributeArraySize = elemBuf.elementCapacity * elemBuf.elementStride vbuf['data'] = new HeapView(elemBuf.ptr, attributeArraySize, attributeArrayType[name]) vbuf['desc'] = elemBuf meshAttributes[name] = vbuf as Assets.VertexBuffer } // Wait for material load to finish (its textures may take time) and // only then call the handler. let result = AssetLoadResult.Failure try { result = await materialPromise } catch (error) { load.finish(AssetLoadResult.Aborted) Module.umbraFree(ptr) throw error } // Material load failing is normal (e.g. when out of VRAM) so we don't throw any errors // when it happens, we just abort this mesh since it obviously can't be used. if (result !== AssetLoadResult.Success) { load.finish(AssetLoadResult.Aborted) Module.umbraFree(ptr) return AssetLoadResult.Aborted } const materialAsset = this.assets.get(materialUserPointer) if (typeof materialAsset === 'undefined') { load.finish(AssetLoadResult.Aborted) Module.umbraFree(ptr) throw new Error(`Material ${materialUserPointer} had no asset object.`) } const meshData = { buffers: meshAttributes as Assets.MeshAttributes, material: materialAsset, bounds: [info.bounds.mn, info.bounds.mx], } as Assets.MeshData let status = AssetLoadResult.Failure try { status = this.handlers['LoadMesh'](meshData, load.assetUserPointer) if (status === AssetLoadResult.Success) { Module.meshLoadFinishExternalWithLoadResultHeader(load.ptr, ptr, basePtr) } else { load.finish(status) return status } } catch (error) { // Handler threw an exception load.finish(Assets.AssetLoadResult.Failure) throw error } finally { Module.umbraFree(ptr) } return status } loadAssets(timeLimit: number) { const startTime = performance.now() // First go and free up all pending assets for (const unload of this.getAssetUnloads()) { if (this.debug.trackInstantUnloads) { const created = this.stats.assetLoadTimes.get(unload.userPointer) const wasAlive = this.stats.numUpdates - created if (wasAlive < 3) { this.stats.numInstantUnloads += 1 } } // Find the user-supplied asset object const asset = this.assets.get(unload.userPointer) // Delete the promise used to wait for this asset. We can't delete the promise earlier // because multiple meshes may need to wait on a single material, so we must keep its promise alive. this.loadPromises.delete(unload.userPointer) // Call the application handler this.handlers[unload.type](asset, unload.userPointer) unload.finish() if (performance.now() - startTime > timeLimit) { break } } // Process load jobs if there's still time left let jobsLaunched = 0 do { const loadPtr = Module.runtimeNextAssetLoad(this.ptr) // No more jobs to process? if (loadPtr === 0) { break } let promise: Promise // Assign the newly created AssetLoad its own user pointer right away // so that we can call load.prepare() before handing it out to user. const load = new AssetLoad(loadPtr, this.getNextUserPointer()) if (load.type === 'LoadTexture') { promise = this.dispatchTextureLoad(load as Assets.LoadTexture) } else if (load.type === 'LoadMaterial') { promise = this.dispatchMaterialLoad(load as Assets.LoadMaterial) } else if (load.type === 'LoadMesh') { // No-one waits for mesh loads to finish so we don't save its promise this.dispatchMeshLoad(load as Assets.LoadMesh) } if (typeof promise !== 'undefined') { this.loadPromises.set(load.assetUserPointer, promise) } jobsLaunched++ // If every worker is busy then don't exceed the pipeline memory limit. // This causes a bubble in the pipeline if a very large decompression // job consumes all the pipeline memory. On the other hand, this shouldn't // happen as we crash if too much memory is used anyway. const pipelineMemoryUsed = this.stats.meshPipelineMemoryUse + this.stats.texturePipelineMemoryUse const shortest = this.workerPool.shortestQueueLength() if (shortest > 0 && pipelineMemoryUsed >= this.limits.streamingPipeSizeLimit) { break } // Don't queue any new loads if the workers are falling behind. If an async load // is finished too late, it may be get instantly unloaded anyway if the camera moved. if (shortest >= this.limits.maxWorkQueueLength) { break } } while (performance.now() - startTime < timeLimit) this.stats.numUpdates += 1 this.stats.numWorkerLoadsPending = this.workerPool.numLoadsPending() this.stats.perFrameLoadedTextures = 0 this.stats.perFrameLoadedTextureBytes = 0 this.stats.perFrameLoadedMeshes = 0 this.stats.perFrameLoadedMeshBytes = 0 } getNextUserPointer(): UserPointer { // AssetJob IDs are just an increasing 32-bit series const userPtr = this.nextId this.nextId = (this.nextId + 1) | 0 if (this.nextId === 0) { this.nextId = 1 } return userPtr } addAsset(assetID: UserPointer, asset: any) { this.assets.set(assetID, asset) } /** * Removes the asset reference of the stream out job. * Assumes the caller has already freed their own asset resources. */ removeAsset(id: UserPointer): void { if (this.assets.has(id)) { this.assets.delete(id) } } formatNeedsTranscoding(format: number): boolean { const flags = this.platformFeatures.textureSupportMask switch (format) { case TextureFormat.BC1: if (flags & TextureSupportFlags.BC1) { return false } break case TextureFormat.BC3: if (flags & TextureSupportFlags.BC3) { return false } break case TextureFormat.BC4: if (flags & TextureSupportFlags.BC4) { return false } break case TextureFormat.BC5: if (flags & TextureSupportFlags.BC5) { return false } break default: return false } return true } getStreamingState() { Module.runtimeGetStreamingState(this.ptr, this.tempStreamingStateBuffer.ptr) return Module.deserializeStreamingState(this.tempStreamingStateBuffer.ptr) } getStreamingProgress(): number { const state = this.getStreamingState() if (state.numResidentTiles == 0) { return 0.0 } return state.numResidentTilesLoaded / state.numResidentTiles } getNumberOfWorkers(): number { return this.workerPool.numWorkers() } } // Expose only runtime for instantiation return { createRuntime: (config: API.RuntimeConfig) => new Runtime(new Client(), config, Module.umbraLibraryConfig) as API.Runtime, } } export { create }