@lightningtv/renderer
Version:
Lightning 3 Renderer
302 lines (263 loc) • 8.89 kB
text/typescript
/*
* 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 type { Dimensions } from '../../../common/CommonTypes.js';
import { assertTruthy } from '../../../utils.js';
import type { TextureMemoryManager } from '../../TextureMemoryManager.js';
import type { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js';
import type { Texture } from '../../textures/Texture.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 {
protected _nativeCtxTexture: WebGLTexture | null = null;
private _w = 0;
private _h = 0;
constructor(
protected glw: WebGlContextWrapper,
memManager: TextureMemoryManager,
textureSource: Texture,
) {
super(memManager, textureSource);
}
get ctxTexture(): WebGLTexture | null {
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(): Promise<Dimensions> {
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] as unknown as ArrayBufferView);
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
*/
protected 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;
}
}