@lightningjs/renderer
Version:
Lightning 3 Renderer
272 lines • 10.6 kB
JavaScript
/*
* 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 { uploadCompressedTexture } from '../../lib/textureCompression.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 WebGlCoreCtxTexture extends CoreContextTexture {
glw;
_nativeCtxTexture = null;
_w = 0;
_h = 0;
txCoordX1 = 0;
txCoordY1 = 0;
txCoordX2 = 1;
txCoordY2 = 1;
constructor(glw, memManager, textureSource) {
super(memManager, textureSource);
this.glw = glw;
}
/**
* GL error check with direct state marking
* Uses cached error result to minimize function calls
*/
checkGLError() {
// Skip if already failed to prevent double-processing
if (this.state === 'failed') {
return true;
}
const error = this.glw.getError();
if (error !== 0) {
this.state = 'failed';
this.textureSource.setState('failed', new Error(`WebGL Error: ${error}`));
return true;
}
return false;
}
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.
*/
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';
this.textureSource.setState('failed', new Error('WebGL Texture creation failed'));
return;
}
try {
const { width, height } = 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 = 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 });
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;
}
// Ensure texture is marked as failed
this.state = 'failed';
this.textureSource.setState('failed');
}
}
/**
* Called when the texture data needs to be loaded and uploaded to a texture
*/
async onLoadRequest() {
const { glw } = this;
const textureData = this.textureSource.textureData;
// Early return if texture is already failed
if (this.state === 'failed') {
return { width: 0, height: 0 };
}
if (textureData === null || this._nativeCtxTexture === null) {
this.state = 'failed';
this.textureSource.setState('failed', new Error('No texture data available'));
return { width: 0, height: 0 };
}
// 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);
// High-performance error check - single call, direct state marking
if (this.checkGLError() === true) {
return { width: 0, height: 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);
// Check for errors after image upload operations
if (this.checkGLError() === true) {
return { width: 0, height: 0 };
}
this.setTextureMemUse(height * width * formatBytes * memoryPadding);
}
else if (tdata && 'mipmaps' in tdata && tdata.mipmaps) {
const { mipmaps, type, blockInfo } = tdata;
uploadCompressedTexture[type](glw, this._nativeCtxTexture, tdata);
// Check for errors after compressed texture operations
if (this.checkGLError() === true) {
return { width: 0, height: 0 };
}
width = tdata.width;
height = tdata.height;
this.txCoordX2 =
width / (Math.ceil(width / blockInfo.width) * blockInfo.width);
this.txCoordY2 =
height / (Math.ceil(height / blockInfo.height) * blockInfo.height);
this.setTextureMemUse(mipmaps[0]?.byteLength ?? 0);
}
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);
// Check for errors after color texture operations
if (this.checkGLError() === true) {
return { width: 0, height: 0 };
}
this.setTextureMemUse(width * height * formatBytes);
}
else {
console.error(`WebGlCoreCtxTexture.onLoadRequest: Unexpected textureData returned`, textureData);
this.state = 'failed';
this.textureSource.setState('failed', new Error('Unexpected texture data'));
return { width: 0, height: 0 };
}
return {
width,
height,
};
}
/**
* Free the WebGLTexture from the GPU
*
* @returns
*/
free() {
if (this.state === 'freed') {
return;
}
this.state = 'freed';
this.textureSource.setState('freed');
this.release();
}
/**
* Release the WebGLTexture from the GPU without changing state
*/
release() {
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);
const error = glw.getError();
if (error !== 0) {
return null;
}
return nativeTexture;
}
}
//# sourceMappingURL=WebGlCoreCtxTexture.js.map