@lightningtv/renderer
Version:
Lightning 3 Renderer
716 lines (626 loc) • 23.6 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 { assertTruthy, createWebGLContext } from '../../../utils.js';
import {
CoreRenderer,
type BufferInfo,
type CoreRendererOptions,
type QuadOptions,
} from '../CoreRenderer.js';
import { WebGlRenderOp } from './WebGlRenderOp.js';
import type { CoreContextTexture } from '../CoreContextTexture.js';
import {
createIndexBuffer,
type CoreWebGlParameters,
type CoreWebGlExtensions,
getWebGlParameters,
getWebGlExtensions,
type WebGlColor,
} from './internal/RendererUtils.js';
import { WebGlCtxTexture } from './WebGlCtxTexture.js';
import { Texture, TextureType } from '../../textures/Texture.js';
import { SubTexture } from '../../textures/SubTexture.js';
import { WebGlCtxSubTexture } from './WebGlCtxSubTexture.js';
import { BufferCollection } from './internal/BufferCollection.js';
import { compareRect, getNormalizedRgbaComponents } from '../../lib/utils.js';
import { WebGlShaderProgram } from './WebGlShaderProgram.js';
import { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js';
import { RenderTexture } from '../../textures/RenderTexture.js';
import { CoreNodeRenderState, type CoreNode } from '../../CoreNode.js';
import { WebGlCtxRenderTexture } from './WebGlCtxRenderTexture.js';
import { Default } from '../../shaders/webgl/Default.js';
import type { WebGlShaderType } from './WebGlShaderNode.js';
import { WebGlShaderNode } from './WebGlShaderNode.js';
import type { CoreShaderType } from '../CoreShaderNode.js';
const WORDS_PER_QUAD = 24;
// const BYTES_PER_QUAD = WORDS_PER_QUAD * 4;
export type WebGlRendererOptions = CoreRendererOptions;
interface CoreWebGlSystem {
parameters: CoreWebGlParameters;
extensions: CoreWebGlExtensions;
}
export class WebGlRenderer extends CoreRenderer {
//// WebGL Native Context and Data
glw: WebGlContextWrapper;
system: CoreWebGlSystem;
//// Persistent data
quadBuffer: ArrayBuffer;
fQuadBuffer: Float32Array;
uiQuadBuffer: Uint32Array;
renderOps: WebGlRenderOp[] = [];
//// Render Op / Buffer Filling State
curBufferIdx = 0;
curRenderOp: WebGlRenderOp | null = null;
override rttNodes: CoreNode[] = [];
activeRttNode: CoreNode | null = null;
//// Default Shader
defaultShaderNode: WebGlShaderNode | null = null;
quadBufferCollection: BufferCollection;
clearColor: WebGlColor = {
raw: 0x00000000,
normalized: [0, 0, 0, 0],
};
/**
* White pixel texture used by default when no texture is specified.
*/
quadBufferUsage = 0;
numQuadsRendered = 0;
/**
* Whether the renderer is currently rendering to a texture.
*/
public renderToTextureActive = false;
constructor(options: WebGlRendererOptions) {
super(options);
this.quadBuffer = new ArrayBuffer(this.stage.options.quadBufferSize);
this.fQuadBuffer = new Float32Array(this.quadBuffer);
this.uiQuadBuffer = new Uint32Array(this.quadBuffer);
this.mode = 'webgl';
const gl = createWebGLContext(
options.canvas,
options.forceWebGL2,
options.contextSpy,
);
const glw = (this.glw = new WebGlContextWrapper(gl));
glw.viewport(0, 0, options.canvas.width, options.canvas.height);
this.updateClearColor(this.stage.clearColor);
glw.setBlend(true);
glw.blendFunc(glw.ONE, glw.ONE_MINUS_SRC_ALPHA);
createIndexBuffer(glw, this.stage.bufferMemory);
this.system = {
parameters: getWebGlParameters(this.glw),
extensions: getWebGlExtensions(this.glw),
};
const quadBuffer = glw.createBuffer();
assertTruthy(quadBuffer);
const stride = 8 * Float32Array.BYTES_PER_ELEMENT;
this.quadBufferCollection = new BufferCollection([
{
buffer: quadBuffer,
attributes: {
a_position: {
name: 'a_position',
size: 2, // 2 components per iteration
type: glw.FLOAT, // the data is 32bit floats
normalized: false, // don't normalize the data
stride, // 0 = move forward size * sizeof(type) each iteration to get the next position
offset: 0, // start at the beginning of the buffer
},
a_textureCoords: {
name: 'a_textureCoords',
size: 2,
type: glw.FLOAT,
normalized: false,
stride,
offset: 2 * Float32Array.BYTES_PER_ELEMENT,
},
a_color: {
name: 'a_color',
size: 4,
type: glw.UNSIGNED_BYTE,
normalized: true,
stride,
offset: 4 * Float32Array.BYTES_PER_ELEMENT,
},
a_textureIndex: {
name: 'a_textureIndex',
size: 1,
type: glw.FLOAT,
normalized: false,
stride,
offset: 5 * Float32Array.BYTES_PER_ELEMENT,
},
a_nodeCoords: {
name: 'a_nodeCoords',
size: 2,
type: glw.FLOAT,
normalized: false,
stride,
offset: 6 * Float32Array.BYTES_PER_ELEMENT,
},
},
},
]);
}
reset() {
const { glw } = this;
this.curBufferIdx = 0;
this.curRenderOp = null;
this.renderOps.length = 0;
glw.setScissorTest(false);
glw.clear();
}
createShaderProgram(
shaderType: WebGlShaderType,
props: Record<string, unknown>,
): WebGlShaderProgram {
return new WebGlShaderProgram(this, shaderType, props);
}
createShaderNode(
shaderKey: string,
shaderType: WebGlShaderType,
props?: Record<string, unknown>,
program?: WebGlShaderProgram,
) {
return new WebGlShaderNode(
shaderKey,
shaderType,
program!,
this.stage,
props,
);
}
override supportsShaderType(shaderType: Readonly<WebGlShaderType>): boolean {
//if shadertype doesnt have a fragment source we cant use it
return shaderType.fragment !== undefined;
}
createCtxTexture(textureSource: Texture): CoreContextTexture {
if (textureSource instanceof SubTexture) {
return new WebGlCtxSubTexture(
this.glw,
this.stage.txMemManager,
textureSource,
);
} else if (textureSource instanceof RenderTexture) {
return new WebGlCtxRenderTexture(
this.glw,
this.stage.txMemManager,
textureSource,
);
}
return new WebGlCtxTexture(
this.glw,
this.stage.txMemManager,
textureSource,
);
}
/**
* This function adds a quad (a rectangle composed of two triangles) to the WebGL rendering pipeline.
*
* It takes a set of options that define the quad's properties, such as its dimensions, colors, texture, shader, and transformation matrix.
* The function first updates the shader properties with the current dimensions if necessary, then sets the default texture if none is provided.
* It then checks if a new render operation is needed, based on the current shader and clipping rectangle.
* If a new render operation is needed, it creates one and updates the current render operation.
* The function then adjusts the texture coordinates based on the texture options and adds the texture to the texture manager.
*
* Finally, it calculates the vertices for the quad, taking into account any transformations, and adds them to the quad buffer.
* The function updates the length and number of quads in the current render operation, and updates the current buffer index.
*/
addQuad(params: QuadOptions) {
const { fQuadBuffer, uiQuadBuffer } = this;
let texture = params.texture;
assertTruthy(texture !== null, 'Texture is required');
let { curBufferIdx: bufferIdx, curRenderOp } = this;
if (this.reuseRenderOp(params) === false) {
this.newRenderOp(params, bufferIdx);
curRenderOp = this.curRenderOp;
assertTruthy(curRenderOp);
}
let texCoordX1 = 0;
let texCoordY1 = 0;
let texCoordX2 = 1;
let texCoordY2 = 1;
if (texture.type === TextureType.subTexture) {
const {
x: tx,
y: ty,
width: tw,
height: th,
} = (texture as SubTexture).props;
const { width: parentW = 0, height: parentH = 0 } = (
texture as SubTexture
).parentTexture.dimensions || { width: 0, height: 0 };
texCoordX1 = tx / parentW;
texCoordX2 = texCoordX1 + tw / parentW;
texCoordY1 = ty / parentH;
texCoordY2 = texCoordY1 + th / parentH;
texture = (texture as SubTexture).parentTexture;
}
if (
texture.type === TextureType.image &&
params.textureOptions !== null &&
params.textureOptions.resizeMode !== undefined &&
texture.dimensions !== null
) {
const resizeMode = params.textureOptions.resizeMode;
const { width: tw, height: th } = texture.dimensions;
if (resizeMode.type === 'cover') {
const scaleX = params.width / tw;
const scaleY = params.height / th;
const scale = Math.max(scaleX, scaleY);
const precision = 1 / scale;
// Determine based on width
if (scale && scaleX && scaleX < scale) {
const desiredSize = precision * params.width;
texCoordX1 = (1 - desiredSize / tw) * (resizeMode.clipX ?? 0.5);
texCoordX2 = texCoordX1 + desiredSize / tw;
}
// Determine based on height
if (scale && scaleY && scaleY < scale) {
const desiredSize = precision * params.height;
texCoordY1 = (1 - desiredSize / th) * (resizeMode.clipY ?? 0.5);
texCoordY2 = texCoordY1 + desiredSize / th;
}
}
}
// Flip texture coordinates if dictated by texture options
let flipY = 0;
if (params.textureOptions !== null) {
if (params.textureOptions.flipX === true) {
[texCoordX1, texCoordX2] = [texCoordX2, texCoordX1];
}
// convert to integer for bitwise operation below
flipY = +(params.textureOptions.flipY || false);
}
// Eitherone should be true
if (flipY ^ +(texture.type === TextureType.renderToTexture)) {
[texCoordY1, texCoordY2] = [texCoordY2, texCoordY1];
}
const ctxTexture = texture.ctxTexture as WebGlCtxTexture;
assertTruthy(ctxTexture instanceof WebGlCtxTexture);
const textureIdx = this.addTexture(ctxTexture, bufferIdx);
assertTruthy(this.curRenderOp !== null);
assertTruthy(params.renderCoords);
// Upper-Left
fQuadBuffer[bufferIdx++] = params.renderCoords.x1; // vertexX
fQuadBuffer[bufferIdx++] = params.renderCoords.y1; // vertexY
fQuadBuffer[bufferIdx++] = texCoordX1; // texCoordX
fQuadBuffer[bufferIdx++] = texCoordY1; // texCoordY
uiQuadBuffer[bufferIdx++] = params.colorTl; // color
fQuadBuffer[bufferIdx++] = textureIdx; // texIndex
fQuadBuffer[bufferIdx++] = 0; //node X coord
fQuadBuffer[bufferIdx++] = 0; //node y coord
// Upper-Right
fQuadBuffer[bufferIdx++] = params.renderCoords.x2;
fQuadBuffer[bufferIdx++] = params.renderCoords.y2;
fQuadBuffer[bufferIdx++] = texCoordX2;
fQuadBuffer[bufferIdx++] = texCoordY1;
uiQuadBuffer[bufferIdx++] = params.colorTr;
fQuadBuffer[bufferIdx++] = textureIdx;
fQuadBuffer[bufferIdx++] = 1; //node X coord
fQuadBuffer[bufferIdx++] = 0; //node y coord
// Lower-Left
fQuadBuffer[bufferIdx++] = params.renderCoords.x4;
fQuadBuffer[bufferIdx++] = params.renderCoords.y4;
fQuadBuffer[bufferIdx++] = texCoordX1;
fQuadBuffer[bufferIdx++] = texCoordY2;
uiQuadBuffer[bufferIdx++] = params.colorBl;
fQuadBuffer[bufferIdx++] = textureIdx;
fQuadBuffer[bufferIdx++] = 0; //node X coord
fQuadBuffer[bufferIdx++] = 1; //node y coord
// Lower-Right
fQuadBuffer[bufferIdx++] = params.renderCoords.x3;
fQuadBuffer[bufferIdx++] = params.renderCoords.y3;
fQuadBuffer[bufferIdx++] = texCoordX2;
fQuadBuffer[bufferIdx++] = texCoordY2;
uiQuadBuffer[bufferIdx++] = params.colorBr;
fQuadBuffer[bufferIdx++] = textureIdx;
fQuadBuffer[bufferIdx++] = 1; //node X coord
fQuadBuffer[bufferIdx++] = 1; //node y coord
// Update the length of the current render op
this.curRenderOp.length += WORDS_PER_QUAD;
this.curRenderOp.numQuads++;
this.curBufferIdx = bufferIdx;
}
/**
* Replace the existing RenderOp with a new one that uses the specified Shader
* and starts at the specified buffer index.
*
* @param shader
* @param bufferIdx
*/
private newRenderOp(quad: QuadOptions, bufferIdx: number) {
const curRenderOp = new WebGlRenderOp(this, quad, bufferIdx);
this.curRenderOp = curRenderOp;
this.renderOps.push(curRenderOp);
}
/**
* Add a texture to the current RenderOp. If the texture cannot be added to the
* current RenderOp, a new RenderOp will be created and the texture will be added
* to that one.
*
* If the texture cannot be added to the new RenderOp, an error will be thrown.
*
* @param texture
* @param bufferIdx
* @param recursive
* @returns Assigned Texture Index of the texture in the render op
*/
private addTexture(
texture: WebGlCtxTexture,
bufferIdx: number,
recursive?: boolean,
): number {
assertTruthy(this.curRenderOp);
const textureIdx = this.curRenderOp.addTexture(texture);
// TODO: Refactor to be more DRY
if (textureIdx === 0xffffffff) {
if (recursive) {
throw new Error('Unable to add texture to render op');
}
this.newRenderOp(this.curRenderOp.quad as QuadOptions, bufferIdx);
return this.addTexture(texture, bufferIdx, true);
}
return textureIdx;
}
/**
* Test if the current Render operation can be reused for the specified parameters.
* @param params
* @returns
*/
reuseRenderOp(params: QuadOptions): boolean {
const { shader, parentHasRenderTexture, rtt, clippingRect } = params;
// Switching shader program will require a new render operation
if (
this.curRenderOp?.shader.shaderKey !==
(shader as WebGlShaderNode).shaderKey
) {
return false;
}
// Switching clipping rect will require a new render operation
if (
compareRect(this.curRenderOp.quad.clippingRect, clippingRect) === false
) {
return false;
}
// Force new render operation if rendering to texture
// @todo: This needs to be improved, render operations could also be reused
// for rendering to texture
if (parentHasRenderTexture !== undefined || rtt !== undefined) {
return false;
}
// Check if the shader can batch the shader properties
if (
!this.curRenderOp.shader.program.reuseRenderOp(
params,
this.curRenderOp.quad as QuadOptions,
)
) {
return false;
}
// Render operation can be reused
return true;
}
/**
* add RenderOp to the render pipeline
*/
addRenderOp(renderable: WebGlRenderOp) {
this.renderOps.push(renderable);
this.curRenderOp = null;
}
/**
* Render the current set of RenderOps to render to the specified surface.
*
* TODO: 'screen' is the only supported surface at the moment.
*
* @param surface
*/
render(surface: 'screen' | CoreContextTexture = 'screen'): void {
const { glw, quadBuffer } = this;
const arr = new Float32Array(quadBuffer, 0, this.curBufferIdx);
const buffer = this.quadBufferCollection.getBuffer('a_position') || null;
glw.arrayBufferData(buffer, arr, glw.STATIC_DRAW);
for (let i = 0, length = this.renderOps.length; i < length; i++) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.renderOps[i]!.draw();
}
this.quadBufferUsage = this.curBufferIdx * arr.BYTES_PER_ELEMENT;
// Calculate the size of each quad in bytes (4 vertices per quad) times the size of each vertex in bytes
const QUAD_SIZE_IN_BYTES = 4 * (8 * arr.BYTES_PER_ELEMENT); // 8 attributes per vertex
this.numQuadsRendered = this.quadBufferUsage / QUAD_SIZE_IN_BYTES;
}
getQuadCount(): number {
return this.numQuadsRendered;
}
renderToTexture(node: CoreNode) {
for (let i = 0; i < this.rttNodes.length; i++) {
if (this.rttNodes[i] === node) {
return;
}
}
this.insertRTTNodeInOrder(node);
}
/**
* Inserts an RTT node into `this.rttNodes` while maintaining the correct rendering order based on hierarchy.
*
* Rendering order for RTT nodes is critical when nested RTT nodes exist in a parent-child relationship.
* Specifically:
* - Child RTT nodes must be rendered before their RTT-enabled parents to ensure proper texture composition.
* - If an RTT node is added and it has existing RTT children, it should be rendered after those children.
*
* This function addresses both cases by:
* 1. **Checking Upwards**: It traverses the node's hierarchy upwards to identify any RTT parent
* already in `rttNodes`. If an RTT parent is found, the new node is placed before this parent.
* 2. **Checking Downwards**: It traverses the node’s children recursively to find any RTT-enabled
* children that are already in `rttNodes`. If such children are found, the new node is inserted
* after the last (highest index) RTT child node.
*
* The final calculated insertion index ensures the new node is positioned in `rttNodes` to respect
* both parent-before-child and child-before-parent rendering rules, preserving the correct order
* for the WebGL renderer.
*
* @param node - The RTT-enabled CoreNode to be added to `rttNodes` in the appropriate hierarchical position.
*/
private insertRTTNodeInOrder(node: CoreNode) {
let insertIndex = this.rttNodes.length; // Default to the end of the array
// 1. Traverse upwards to ensure the node is placed before its RTT parent (if any).
let currentNode: CoreNode = node;
while (currentNode) {
if (!currentNode.parent) {
break;
}
const parentIndex = this.rttNodes.indexOf(currentNode.parent);
if (parentIndex !== -1) {
// Found an RTT parent in the list; set insertIndex to place node before the parent
insertIndex = parentIndex;
break;
}
currentNode = currentNode.parent;
}
// 2. Traverse downwards to ensure the node is placed after any RTT children.
// Look through each child recursively to see if any are already in rttNodes.
const maxChildIndex = this.findMaxChildRTTIndex(node);
if (maxChildIndex !== -1) {
// Adjust insertIndex to be after the last child RTT node
insertIndex = Math.max(insertIndex, maxChildIndex + 1);
}
// 3. Insert the node at the calculated position
this.rttNodes.splice(insertIndex, 0, node);
}
// Helper function to find the highest index of any RTT children of a node within rttNodes
private findMaxChildRTTIndex(node: CoreNode): number {
let maxIndex = -1;
const traverseChildren = (currentNode: CoreNode) => {
const currentIndex = this.rttNodes.indexOf(currentNode);
if (currentIndex !== -1) {
maxIndex = Math.max(maxIndex, currentIndex);
}
// Recursively check all children of the current node
for (const child of currentNode.children) {
traverseChildren(child);
}
};
// Start traversal directly with the provided node
traverseChildren(node);
return maxIndex;
}
renderRTTNodes() {
const { glw } = this;
// Render all associated RTT nodes to their textures
for (let i = 0; i < this.rttNodes.length; i++) {
const node = this.rttNodes[i];
// Skip nodes that don't have RTT updates
if (node === undefined || node.hasRTTupdates === false) {
continue;
}
// Skip nodes that are not visible
if (
node.worldAlpha === 0 ||
(node.strictBounds === true &&
node.renderState === CoreNodeRenderState.OutOfBounds)
) {
continue;
}
// Skip nodes that do not have a loaded texture
if (node.texture === null || node.texture.state !== 'loaded') {
continue;
}
// Set the active RTT node to the current node
// So we can prevent rendering children of nested RTT nodes
this.activeRttNode = node;
assertTruthy(node.texture !== null, 'RTT node missing texture');
const ctxTexture = node.texture.ctxTexture;
assertTruthy(ctxTexture instanceof WebGlCtxRenderTexture);
this.renderToTextureActive = true;
// Bind the the texture's framebuffer
glw.bindFramebuffer(ctxTexture.framebuffer);
glw.viewport(0, 0, ctxTexture.w, ctxTexture.h);
// Set the clear color to transparent
glw.clearColor(0, 0, 0, 0);
glw.clear();
// Render all associated quads to the texture
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
if (child === undefined) {
continue;
}
this.stage.addQuads(child);
child.hasRTTupdates = false;
}
// Render all associated quads to the texture
this.render();
// Reset render operations
this.renderOps.length = 0;
node.hasRTTupdates = false;
}
const clearColor = this.clearColor.normalized;
// Restore the default clear color
glw.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]);
// Bind the default framebuffer
glw.bindFramebuffer(null);
glw.viewport(0, 0, this.glw.canvas.width, this.glw.canvas.height);
this.renderToTextureActive = false;
}
removeRTTNode(node: CoreNode) {
const index = this.rttNodes.indexOf(node);
if (index === -1) {
return;
}
this.rttNodes.splice(index, 1);
}
getBufferInfo(): BufferInfo | null {
const bufferInfo: BufferInfo = {
totalAvailable: this.stage.options.quadBufferSize,
totalUsed: this.quadBufferUsage,
};
return bufferInfo;
}
getDefaultShaderNode(): WebGlShaderNode {
if (this.defaultShaderNode !== null) {
return this.defaultShaderNode as WebGlShaderNode;
}
this.stage.shManager.registerShaderType('Default', Default);
this.defaultShaderNode = this.stage.shManager.createShader(
'Default',
) as WebGlShaderNode;
return this.defaultShaderNode;
}
/**
* Updates the WebGL context's clear color and clears the color buffer.
*
* @param color - The color to set as the clear color, represented as a 32-bit integer.
*/
updateClearColor(color: number) {
if (this.clearColor.raw === color) {
return;
}
const glw = this.glw;
const normalizedColor = getNormalizedRgbaComponents(color);
glw.clearColor(
normalizedColor[0],
normalizedColor[1],
normalizedColor[2],
normalizedColor[3],
);
this.clearColor = {
raw: color,
normalized: normalizedColor,
};
glw.clear();
}
}