UNPKG

@lightningtv/renderer

Version:
227 lines 8.94 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 { assertTruthy } from '../../../utils.js'; import { isPowerOfTwo } from '../../utils.js'; 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; } assertTruthy(this._nativeCtxTexture); 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. */ load() { // If the texture is already loading or loaded, don't load it again. if (this.state === 'loading' || this.state === 'loaded') { return; } this.state = 'loading'; this.textureSource.setState('loading'); this._nativeCtxTexture = this.createNativeCtxTexture(); if (this._nativeCtxTexture === null) { this.state = 'failed'; this.textureSource.setState('failed', new Error('Could not create WebGL Texture')); console.error('Could not create WebGL Texture'); return; } this.onLoadRequest() .then(({ width, height }) => { // If the texture has been freed while loading, return early. if (this.state === 'freed') { return; } this.state = 'loaded'; this._w = width; this._h = height; // Update the texture source's width and height so that it can be used // for rendering. this.textureSource.setState('loaded', { width, height }); // cleanup source texture data this.textureSource.freeTextureData(); }) .catch((err) => { // If the texture has been freed while loading, return early. if (this.state === 'freed') { return; } this.state = 'failed'; this.textureSource.setState('failed', err); this.textureSource.freeTextureData(); console.error(err); }); } /** * 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 width = 0; let height = 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 HTMLImageElement due to web worker isHTMLImageElement(tdata)) { width = tdata.width; height = 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(height * width * formatBytes * memoryPadding); } else if (tdata === null) { width = 0; height = 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, width = 0, height = 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, width, height, 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 width = 1; height = 1; glw.bindTexture(this._nativeCtxTexture); glw.pixelStorei(glw.UNPACK_PREMULTIPLY_ALPHA_WEBGL, !!textureData.premultiplyAlpha); glw.texImage2D(0, format, width, height, 0, format, glw.UNSIGNED_BYTE, tdata); this.setTextureMemUse(width * height * formatBytes); } else { console.error(`WebGlCoreCtxTexture.onLoadRequest: Unexpected textureData returned`, textureData); } return { width, height, }; } /** * 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 * * @remarks * When this method returns the returned texture will be bound to the GL context state. * * @param width * @param height * @returns */ 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