UNPKG

@lightningjs/renderer

Version:
236 lines 9.72 kB
/* * If not stated otherwise in this file or this component's LICENSE file the * following copyright and licenses apply: * * Copyright 2023 Comcast Cable Communications Management, LLC. * * Licensed under the Apache License, Version 2.0 (the License); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { CoreContextTexture } from '../CoreContextTexture.js'; import { isHTMLImageElement } from './internal/RendererUtils.js'; const TRANSPARENT_TEXTURE_DATA = new Uint8Array([0, 0, 0, 0]); /** * A wrapper around a WebGLTexture that handles loading the texture data * from a Texture source and uploading it to the GPU as well as freeing * the uploaded texture. * * @remarks * When accessing the ctxTexture property, the texture will be loaded if * it hasn't been already. ctxTexture will always return a valid WebGLTexture * and trigger the loading/uploading of the texture's data if it hasn't been * loaded yet. */ export class WebGlCtxTexture extends CoreContextTexture { glw; _nativeCtxTexture = null; _w = 0; _h = 0; constructor(glw, memManager, textureSource) { super(memManager, textureSource); this.glw = glw; } get ctxTexture() { if (this.state === 'freed') { this.load(); return null; } return this._nativeCtxTexture; } get w() { return this._w; } get h() { return this._h; } /** * Load the texture data from the Texture source and upload it to the GPU * * @remarks * This method is called automatically when accessing the ctxTexture property * if the texture hasn't been loaded yet. But it can also be called manually * to force the texture to be pre-loaded prior to accessing the ctxTexture * property. */ async load() { // If the texture is already loading or loaded, return resolved promise if (this.state === 'loading' || this.state === 'loaded') { return Promise.resolve(); } this.state = 'loading'; this.textureSource.setState('loading'); // Await the native texture creation to ensure GPU buffer is fully allocated this._nativeCtxTexture = this.createNativeCtxTexture(); if (this._nativeCtxTexture === null) { this.state = 'failed'; const error = new Error('Could not create WebGL Texture'); this.textureSource.setState('failed', error); console.error('Could not create WebGL Texture'); throw error; } try { const { w, h } = await this.onLoadRequest(); // If the texture has been freed while loading, return early. // Type assertion needed because state could change during async operations if (this.state === 'freed') { return; } this.state = 'loaded'; this._w = w; this._h = h; // Update the texture source's width and height so that it can be used // for rendering. this.textureSource.setState('loaded', { w, h }); // cleanup source texture data next tick // This is done using queueMicrotask to ensure it runs after the current // event loop tick, allowing the texture to be fully loaded and bound // to the GL context before freeing the source data. // This is important to avoid issues with the texture data being // freed while the texture is still being loaded or used. queueMicrotask(() => { this.textureSource.freeTextureData(); }); } catch (err) { // If the texture has been freed while loading, return early. // Type assertion needed because state could change during async operations if (this.state === 'freed') { return; } this.state = 'failed'; const error = err instanceof Error ? err : new Error(String(err)); this.textureSource.setState('failed', error); this.textureSource.freeTextureData(); console.error(err); throw error; // Re-throw to propagate the error } } /** * Called when the texture data needs to be loaded and uploaded to a texture */ async onLoadRequest() { const { glw } = this; const textureData = this.textureSource.textureData; if (textureData === null || this._nativeCtxTexture === null) { throw new Error('Texture data or native texture is null ' + this.textureSource.type); } // Set to a 1x1 transparent texture glw.texImage2D(0, glw.RGBA, 1, 1, 0, glw.RGBA, glw.UNSIGNED_BYTE, null); this.setTextureMemUse(TRANSPARENT_TEXTURE_DATA.byteLength); let w = 0; let h = 0; glw.activeTexture(0); const tdata = textureData.data; const format = glw.RGBA; const formatBytes = 4; const memoryPadding = 1.1; // Add padding to account for GPU Padding // If textureData is null, the texture is empty (0, 0) and we don't need to // upload any data to the GPU. if ((typeof ImageBitmap !== 'undefined' && tdata instanceof ImageBitmap) || tdata instanceof ImageData || // not using typeof HTMLI mageElement due to web worker isHTMLImageElement(tdata) === true) { w = tdata.width; h = tdata.height; glw.bindTexture(this._nativeCtxTexture); glw.pixelStorei(glw.UNPACK_PREMULTIPLY_ALPHA_WEBGL, !!textureData.premultiplyAlpha); glw.texImage2D(0, format, format, glw.UNSIGNED_BYTE, tdata); this.setTextureMemUse(h * w * formatBytes * memoryPadding); } else if (tdata === null) { w = 0; h = 0; // Reset to a 1x1 transparent texture glw.bindTexture(this._nativeCtxTexture); glw.texImage2D(0, format, 1, 1, 0, format, glw.UNSIGNED_BYTE, TRANSPARENT_TEXTURE_DATA); this.setTextureMemUse(TRANSPARENT_TEXTURE_DATA.byteLength); } else if ('mipmaps' in tdata && tdata.mipmaps) { const { mipmaps, w = 0, h = 0, type, glInternalFormat } = tdata; const view = type === 'ktx' ? new DataView(mipmaps[0] ?? new ArrayBuffer(0)) : mipmaps[0]; glw.bindTexture(this._nativeCtxTexture); glw.compressedTexImage2D(0, glInternalFormat, w, h, 0, view); glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE); glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE); glw.texParameteri(glw.TEXTURE_MAG_FILTER, glw.LINEAR); glw.texParameteri(glw.TEXTURE_MIN_FILTER, glw.LINEAR); this.setTextureMemUse(view.byteLength); } else if (tdata && tdata instanceof Uint8Array) { // Color Texture w = 1; h = 1; glw.bindTexture(this._nativeCtxTexture); glw.pixelStorei(glw.UNPACK_PREMULTIPLY_ALPHA_WEBGL, !!textureData.premultiplyAlpha); glw.texImage2D(0, format, w, h, 0, format, glw.UNSIGNED_BYTE, tdata); this.setTextureMemUse(w * h * formatBytes); } else { console.error(`WebGlCoreCtxTexture.onLoadRequest: Unexpected textureData returned`, textureData); } return { w, h, }; } /** * Free the WebGLTexture from the GPU * * @returns */ free() { if (this.state === 'freed') { return; } this.state = 'freed'; this.textureSource.setState('freed'); this._w = 0; this._h = 0; if (this._nativeCtxTexture !== null) { this.glw.deleteTexture(this._nativeCtxTexture); this.setTextureMemUse(0); this._nativeCtxTexture = null; } // if the texture still has source data, free it this.textureSource.freeTextureData(); } /** * Create native context texture asynchronously * * @remarks * When this method resolves, the returned texture will be bound to the GL context state * and fully ready for use. This ensures proper GPU resource allocation timing. * * @returns Promise that resolves to the native WebGL texture or null on failure */ createNativeCtxTexture() { const { glw } = this; const nativeTexture = glw.createTexture(); if (!nativeTexture) { return null; } // On initial load request, create a 1x1 transparent texture to use until // the texture data is finally loaded. glw.activeTexture(0); glw.bindTexture(nativeTexture); // linear texture filtering glw.texParameteri(glw.TEXTURE_MAG_FILTER, glw.LINEAR); glw.texParameteri(glw.TEXTURE_MIN_FILTER, glw.LINEAR); // texture wrapping method glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE); glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE); return nativeTexture; } } //# sourceMappingURL=WebGlCtxTexture.js.map