diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index b4e1c2e8..8e635363 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -23,6 +23,9 @@ import { mergeColorAlphaPremultiplied, } from '../utils.js'; import type { TextureOptions } from './CoreTextureManager.js'; +import type { WebGlRenderer } from './renderers/webgl/WebGlRenderer.js'; +import type { WebGlCtxTexture } from './renderers/webgl/WebGlCtxTexture.js'; +import type { BufferCollection } from './renderers/webgl/internal/BufferCollection.js'; import type { CoreRenderer } from './renderers/CoreRenderer.js'; import type { Stage } from './Stage.js'; import { @@ -738,6 +741,12 @@ export class CoreNode extends EventEmitter { readonly children: CoreNode[] = []; protected _id: number = getNewId(); readonly props: CoreNodeProps; + public readonly isCoreNode = true as const; + + // WebGL Render Op State + public renderOpBufferIdx: number = 0; + public numQuads: number = 0; + public renderOpTextures: WebGlCtxTexture[] = []; private hasShaderUpdater = false; public hasShaderTimeFn = false; @@ -794,6 +803,10 @@ export class CoreNode extends EventEmitter { constructor(readonly stage: Stage, props: CoreNodeProps) { super(); const p = (this.props = {} as CoreNodeProps); + + // Initialize the renderOpTextures array with a capacity of 16 (typical max textures) + this.renderOpTextures = []; + //inital update type let initialUpdateType = UpdateType.Local | UpdateType.RenderBounds | UpdateType.RenderState; @@ -1800,47 +1813,43 @@ export class CoreNode extends EventEmitter { return; } - const p = this.props; - const t = this.globalTransform!; - const coords = this.renderCoords; - const texture = p.texture || this.stage.defaultTexture; - const textureCoords = - this.textureCoords || this.stage.renderer.defaultTextureCoords; + const texture = this.renderTexture; // There is a race condition where the texture can be null // with RTT nodes. Adding this defensively to avoid errors. - if (texture && texture.state !== 'loaded') { + // Also check if we have a valid texture or default texture to render + if (!texture || texture.state !== 'loaded') { return; } - renderer.addQuad({ - width: p.w, - height: p.h, - colorTl: this.premultipliedColorTl, - colorTr: this.premultipliedColorTr, - colorBl: this.premultipliedColorBl, - colorBr: this.premultipliedColorBr, - texture, - textureOptions: p.textureOptions, - textureCoords: textureCoords, - shader: p.shader as CoreShaderNode, - alpha: this.worldAlpha, - clippingRect: this.clippingRect, - tx: t.tx, - ty: t.ty, - ta: t.ta, - tb: t.tb, - tc: t.tc, - td: t.td, - renderCoords: coords, - rtt: p.rtt, - zIndex: this.calcZIndex, - parentHasRenderTexture: this.parentHasRenderTexture, - framebufferDimensions: this.parentHasRenderTexture - ? this.parentFramebufferDimensions - : null, - time: this.hasShaderTimeFn === true ? this.getTimerValue() : null, - }); + renderer.addQuad(this); + } + + get renderTexture(): Texture | null { + return this.props.texture || this.stage.defaultTexture; + } + + get renderTextureCoords(): TextureCoords | undefined { + return this.textureCoords || this.stage.renderer.defaultTextureCoords; + } + + get quadBufferCollection(): BufferCollection { + return (this.stage.renderer as WebGlRenderer).quadBufferCollection; + } + + get width(): number { + return this.props.w; + } + + get height(): number { + return this.props.h; + } + + get time(): number { + if (this.hasShaderTimeFn === true) { + return this.getTimerValue(); + } + return 0; } getTimerValue(): number { @@ -2621,12 +2630,12 @@ export class CoreNode extends EventEmitter { /** * Returns the framebuffer dimensions of the RTT parent */ - get parentFramebufferDimensions(): Dimensions { + get parentFramebufferDimensions(): Dimensions | null { if (this.rttParent !== null) { - return this.rttParent.framebufferDimensions as Dimensions; + return this.rttParent.framebufferDimensions; } - this.rttParent = this.findParentRTTNode() as CoreNode; - return this.rttParent.framebufferDimensions as Dimensions; + this.rttParent = this.findParentRTTNode(); + return this.rttParent ? this.rttParent.framebufferDimensions : null; } /** @@ -2719,5 +2728,69 @@ export class CoreNode extends EventEmitter { // no-op } + /** + * Add a texture to the current RenderOp. + * + * @param texture + * @returns Assigned Texture Index of the texture in the render op + */ + addTexture(texture: WebGlCtxTexture): number { + const textures = this.renderOpTextures; + const length = textures.length; + + for (let i = 0; i < length; i++) { + if (textures[i] === texture) { + return i; + } + } + + if (length >= 1) { + return 0xffffffff; + } + + textures.push(texture); + return length; + } + + draw(renderer: WebGlRenderer) { + const { glw, options, stage } = renderer; + const shader = this.props.shader as any; + + stage.shManager.useShader(shader.program); + shader.program.bindRenderOp(this); + + // Clipping + if (this.clippingRect.valid === true) { + const pixelRatio = this.parentHasRenderTexture ? 1 : stage.pixelRatio; + + const clipX = Math.round(this.clippingRect.x * pixelRatio); + const clipWidth = Math.round(this.clippingRect.width * pixelRatio); + const clipHeight = Math.round(this.clippingRect.height * pixelRatio); + let clipY = Math.round( + options.canvas.height - clipHeight - this.clippingRect.y * pixelRatio, + ); + // if parent has render texture, we need to adjust the scissor rect + // to be relative to the parent's framebuffer + if (this.parentHasRenderTexture) { + clipY = this.parentFramebufferDimensions + ? this.parentFramebufferDimensions.h - this.props.h + : 0; + } + + glw.setScissorTest(true); + glw.scissor(clipX, clipY, clipWidth, clipHeight); + } else { + glw.setScissorTest(false); + } + + const quadIdx = (this.renderOpBufferIdx / 32) * 6 * 2; + glw.drawElements( + glw.TRIANGLES, + 6 * this.numQuads, + glw.UNSIGNED_SHORT, + quadIdx, + ); + } + //#endregion Properties } diff --git a/src/core/renderers/CoreRenderer.ts b/src/core/renderers/CoreRenderer.ts index 3c5456dc..c5cb6a5d 100644 --- a/src/core/renderers/CoreRenderer.ts +++ b/src/core/renderers/CoreRenderer.ts @@ -84,7 +84,7 @@ export abstract class CoreRenderer { abstract reset(): void; abstract render(surface?: 'screen' | CoreContextTexture): void; - abstract addQuad(quad: QuadOptions): void; + abstract addQuad(node: CoreNode): void; abstract createCtxTexture(textureSource: Texture): CoreContextTexture; abstract createShaderProgram( shaderConfig: Readonly, diff --git a/src/core/renderers/canvas/CanvasRenderer.ts b/src/core/renderers/canvas/CanvasRenderer.ts index 3fa6d5a4..079f359e 100644 --- a/src/core/renderers/canvas/CanvasRenderer.ts +++ b/src/core/renderers/canvas/CanvasRenderer.ts @@ -66,10 +66,11 @@ export class CanvasRenderer extends CoreRenderer { // noop } - addQuad(quad: QuadOptions): void { + addQuad(node: CoreNode): void { const ctx = this.context; - const { tx, ty, ta, tb, tc, td, clippingRect } = quad; - let texture = quad.texture; + const { tx, ty, ta, tb, tc, td } = node.globalTransform!; + const clippingRect = node.clippingRect; + let texture = node.renderTexture; // The Canvas2D renderer only supports image textures, no textures are used for color blocks if (texture !== null) { const textureType = texture.type; @@ -85,12 +86,12 @@ export class CanvasRenderer extends CoreRenderer { const hasTransform = ta !== 1; const hasClipping = clippingRect.width !== 0 && clippingRect.height !== 0; - const hasShader = quad.shader !== null; + const shader = node.props.shader; + const hasShader = shader !== null; let saveAndRestore = hasTransform === true || hasClipping === true; if (hasShader === true) { - saveAndRestore = - saveAndRestore || (quad.shader as CanvasShaderNode).applySNR; + saveAndRestore = saveAndRestore || (shader as CanvasShaderNode).applySNR; } if (saveAndRestore) { @@ -121,12 +122,40 @@ export class CanvasRenderer extends CoreRenderer { if (hasShader === true) { let renderContext: (() => void) | null = () => { - this.renderContext(quad); + this.renderContext(node); }; - (quad.shader as CanvasShaderNode).render(ctx, quad, renderContext); + const quad: QuadOptions = { + width: node.props.w, + height: node.props.h, + colorTl: node.premultipliedColorTl, + colorTr: node.premultipliedColorTr, + colorBl: node.premultipliedColorBl, + colorBr: node.premultipliedColorBr, + texture: texture, + textureOptions: node.props.textureOptions, + textureCoords: node.renderTextureCoords, + zIndex: node.zIndex, // zIndex usage? + shader: shader, + alpha: node.worldAlpha, + clippingRect: clippingRect, + tx, + ty, + ta, + tb, + tc, + td, + renderCoords: node.renderCoords, + rtt: node.props.rtt, + parentHasRenderTexture: node.parentHasRenderTexture, + framebufferDimensions: node.parentHasRenderTexture + ? node.parentFramebufferDimensions + : null, + }; + + (shader as CanvasShaderNode).render(ctx, quad, renderContext); renderContext = null; } else { - this.renderContext(quad); + this.renderContext(node); } if (saveAndRestore) { @@ -134,22 +163,21 @@ export class CanvasRenderer extends CoreRenderer { } } - renderContext(quad: QuadOptions) { - const color = quad.colorTl; - const texture = quad.texture!; + renderContext(node: CoreNode) { + const color = node.premultipliedColorTl; + const texture = node.renderTexture!; const textureType = texture.type; + const tx = node.globalTransform!.tx; + const ty = node.globalTransform!.ty; + const width = node.props.w; + const height = node.props.h; + if (textureType !== TextureType.color) { const tintColor = parseColor(color); if (textureType !== TextureType.subTexture) { const image = (texture.ctxTexture as CanvasTexture).getImage(tintColor); - this.context.globalAlpha = tintColor.a ?? quad.alpha; - this.context.drawImage( - image, - quad.tx, - quad.ty, - quad.width, - quad.height, - ); + this.context.globalAlpha = tintColor.a ?? node.worldAlpha; + this.context.drawImage(image, tx, ty, width, height); this.context.globalAlpha = 1; return; } @@ -158,51 +186,47 @@ export class CanvasRenderer extends CoreRenderer { ).getImage(tintColor); const props = (texture as SubTexture).props; - this.context.globalAlpha = tintColor.a ?? quad.alpha; + this.context.globalAlpha = tintColor.a ?? node.worldAlpha; this.context.drawImage( image, props.x, props.y, props.w, props.h, - quad.tx, - quad.ty, - quad.width, - quad.height, + tx, + ty, + width, + height, ); this.context.globalAlpha = 1; return; } const hasGradient = - quad.colorTl !== quad.colorTr || quad.colorTl !== quad.colorBr; + node.premultipliedColorTl !== node.premultipliedColorTr || + node.premultipliedColorTl !== node.premultipliedColorBr; if (hasGradient === true) { - let endX: number = quad.tx; - let endY: number = quad.ty; + let endX: number = tx; + let endY: number = ty; let endColor: number; - if (quad.colorTl === quad.colorTr) { + if (node.premultipliedColorTl === node.premultipliedColorTr) { // vertical - endX = quad.tx; - endY = quad.ty + quad.height; - endColor = quad.colorBr; + endX = tx; + endY = ty + height; + endColor = node.premultipliedColorBr; } else { // horizontal - endX = quad.tx + quad.width; - endY = quad.ty; - endColor = quad.colorTr; + endX = tx + width; + endY = ty; + endColor = node.premultipliedColorTr; } - const gradient = this.context.createLinearGradient( - quad.tx, - quad.ty, - endX, - endY, - ); + const gradient = this.context.createLinearGradient(tx, ty, endX, endY); gradient.addColorStop(0, normalizeCanvasColor(color)); gradient.addColorStop(1, normalizeCanvasColor(endColor)); this.context.fillStyle = gradient; - this.context.fillRect(quad.tx, quad.ty, quad.width, quad.height); + this.context.fillRect(tx, ty, width, height); } else { this.context.fillStyle = normalizeCanvasColor(color); - this.context.fillRect(quad.tx, quad.ty, quad.width, quad.height); + this.context.fillRect(tx, ty, width, height); } } diff --git a/src/core/renderers/webgl/SdfRenderOp.ts b/src/core/renderers/webgl/SdfRenderOp.ts new file mode 100644 index 00000000..7c17963e --- /dev/null +++ b/src/core/renderers/webgl/SdfRenderOp.ts @@ -0,0 +1,105 @@ +/* + * 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 { CoreRenderOp } from '../CoreRenderOp.js'; +import type { WebGlCtxTexture } from './WebGlCtxTexture.js'; +import type { WebGlRenderer } from './WebGlRenderer.js'; +import type { BufferCollection } from './internal/BufferCollection.js'; +import type { WebGlShaderNode } from './WebGlShaderNode.js'; +import type { RectWithValid } from '../../lib/utils.js'; +import type { Dimensions } from '../../../common/CommonTypes.js'; +import type { Stage } from '../../Stage.js'; + +/** + * Can render multiple quads with multiple textures (up to vertex shader texture limit) + * + */ +export class SdfRenderOp extends CoreRenderOp { + public numQuads = 0; + public readonly isCoreNode = false as const; + public renderOpTextures: WebGlCtxTexture[] = []; + public time: number = 0; + readonly stage: Stage; + + constructor( + readonly renderer: WebGlRenderer, + readonly shader: WebGlShaderNode, + readonly sdfShaderProps: Record, + readonly quadBufferCollection: BufferCollection, + readonly worldAlpha: number, + readonly clippingRect: RectWithValid, + readonly width: number, + readonly height: number, + readonly rtt: boolean, + readonly parentHasRenderTexture: boolean, + readonly framebufferDimensions: Dimensions | null, + ) { + super(); + this.stage = renderer.stage; + } + + addTexture(texture: WebGlCtxTexture): number { + const { renderOpTextures } = this; + const length = renderOpTextures.length; + + // We only support 1 texture (atlas) for SDF for now, but following the pattern + for (let i = 0; i < length; i++) { + if (renderOpTextures[i] === texture) { + return i; + } + } + + renderOpTextures.push(texture); + return length; + } + + draw() { + const { glw, options, stage } = this.renderer; + + stage.shManager.useShader(this.shader.program); + this.shader.program.bindRenderOp(this); + + // Clipping + if (this.clippingRect.valid === true) { + const pixelRatio = this.parentHasRenderTexture ? 1 : stage.pixelRatio; + const clipX = Math.round(this.clippingRect.x * pixelRatio); + const clipWidth = Math.round(this.clippingRect.width * pixelRatio); + const clipHeight = Math.round(this.clippingRect.height * pixelRatio); + let clipY = Math.round( + options.canvas.height - clipHeight - this.clippingRect.y * pixelRatio, + ); + // if parent has render texture, we need to adjust the scissor rect + // to be relative to the parent's framebuffer + if (this.parentHasRenderTexture) { + clipY = this.framebufferDimensions + ? this.framebufferDimensions.h - this.height + : 0; + } + + glw.setScissorTest(true); + glw.scissor(clipX, clipY, clipWidth, clipHeight); + } else { + glw.setScissorTest(false); + } + + // SDF rendering uses drawArrays with explicit triangle vertices (6 vertices per quad) + // Note: buffers should be bound by bindRenderOp -> bindBufferCollection + glw.drawArrays(glw.TRIANGLES, 0, 6 * this.numQuads); + } +} diff --git a/src/core/renderers/webgl/WebGlRenderOp.ts b/src/core/renderers/webgl/WebGlRenderOp.ts deleted file mode 100644 index 876a16c7..00000000 --- a/src/core/renderers/webgl/WebGlRenderOp.ts +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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 { CoreRenderOp } from '../CoreRenderOp.js'; -import type { WebGlCtxTexture } from './WebGlCtxTexture.js'; -import type { WebGlRenderer } from './WebGlRenderer.js'; -import type { BufferCollection } from './internal/BufferCollection.js'; -import type { WebGlShaderNode } from './WebGlShaderNode.js'; -import type { QuadOptions } from '../CoreRenderer.js'; -import type { CoreTextNode } from '../../CoreTextNode.js'; -import type { RectWithValid } from '../../lib/utils.js'; -import type { Dimensions } from '../../../common/CommonTypes.js'; - -type ReqQuad = - | 'alpha' - | 'shader' - | 'parentHasRenderTexture' - | 'rtt' - | 'clippingRect' - | 'height' - | 'width' - | 'time'; -type RenderOpQuadOptions = Pick & - Partial> & { - sdfShaderProps?: Record; - sdfBuffers?: BufferCollection; - }; - -/** - * Can render multiple quads with multiple textures (up to vertex shader texture limit) - * - */ -export class WebGlRenderOp extends CoreRenderOp { - numQuads = 0; - textures: WebGlCtxTexture[] = []; - - /** - * need to improve this when TextRenderers are refactored - */ - readonly sdfShaderProps: Record | undefined; - readonly sdfNode: CoreTextNode | undefined; - readonly maxTextures: number; - readonly buffers: BufferCollection; - readonly shader: WebGlShaderNode; - readonly width: number; - readonly height: number; - readonly clippingRect: RectWithValid; - readonly rtt: boolean; - readonly parentHasRenderTexture: boolean; - readonly framebufferDimensions: Dimensions | null; - readonly alpha: number; - readonly pixelRatio: number; - readonly time?: number | null; - - constructor( - readonly renderer: WebGlRenderer, - quad: RenderOpQuadOptions, - readonly bufferIdx: number, - ) { - super(); - this.buffers = quad.sdfBuffers || renderer.quadBufferCollection; - this.shader = quad.shader as WebGlShaderNode; - this.width = quad.width; - this.height = quad.height; - this.clippingRect = quad.clippingRect; - this.parentHasRenderTexture = quad.parentHasRenderTexture; - this.framebufferDimensions = quad.framebufferDimensions || null; - this.rtt = quad.rtt; - this.alpha = quad.alpha; - this.pixelRatio = - this.parentHasRenderTexture === true ? 1 : renderer.stage.pixelRatio; - this.time = quad.time; - - /** - * related to line 51 - */ - this.sdfShaderProps = quad.sdfShaderProps; - - this.maxTextures = this.shader.program.supportsIndexedTextures - ? (renderer.glw.getParameter( - renderer.glw.MAX_VERTEX_TEXTURE_IMAGE_UNITS, - ) as number) - : 1; - } - - addTexture(texture: WebGlCtxTexture): number { - const { textures, maxTextures } = this; - let existingIdx = -1; - const texturesLength = textures.length; - for (let i = 0; i < texturesLength; i++) { - const t = textures[i]; - if (t === texture) { - existingIdx = i; - break; - } - } - - if (existingIdx !== -1) { - return existingIdx; - } - - if (texturesLength >= maxTextures) { - return 0xffffffff; - } - this.textures.push(texture); - return texturesLength; - } - - draw() { - const { glw, options, stage } = this.renderer; - - stage.shManager.useShader(this.shader.program); - this.shader.program.bindRenderOp(this); - - // Clipping - if (this.clippingRect.valid === true) { - const clipX = Math.round(this.clippingRect.x * this.pixelRatio); - const clipWidth = Math.round(this.clippingRect.width * this.pixelRatio); - const clipHeight = Math.round(this.clippingRect.height * this.pixelRatio); - let clipY = Math.round( - options.canvas.height - - clipHeight - - this.clippingRect.y * this.pixelRatio, - ); - // if parent has render texture, we need to adjust the scissor rect - // to be relative to the parent's framebuffer - if (this.parentHasRenderTexture) { - clipY = this.framebufferDimensions - ? this.framebufferDimensions.h - this.height - : 0; - } - - glw.setScissorTest(true); - glw.scissor(clipX, clipY, clipWidth, clipHeight); - } else { - glw.setScissorTest(false); - } - - // Check if this is SDF rendering (has sdfBuffers) - if (this.sdfShaderProps !== undefined) { - // SDF rendering uses drawArrays with explicit triangle vertices (6 vertices per quad) - glw.drawArrays(glw.TRIANGLES, 0, 6 * this.numQuads); - } else { - // Regular rendering uses drawElements with index buffer (4 vertices per quad) - const quadIdx = (this.bufferIdx / 32) * 6 * 2; - glw.drawElements( - glw.TRIANGLES, - 6 * this.numQuads, - glw.UNSIGNED_SHORT, - quadIdx, - ); - } - } -} diff --git a/src/core/renderers/webgl/WebGlRenderer.ts b/src/core/renderers/webgl/WebGlRenderer.ts index 0ec0f927..2593c709 100644 --- a/src/core/renderers/webgl/WebGlRenderer.ts +++ b/src/core/renderers/webgl/WebGlRenderer.ts @@ -22,9 +22,8 @@ import { CoreRenderer, type BufferInfo, type CoreRendererOptions, - type QuadOptions, } from '../CoreRenderer.js'; -import { WebGlRenderOp } from './WebGlRenderOp.js'; +import type { SdfRenderOp } from './SdfRenderOp.js'; import type { CoreContextTexture } from '../CoreContextTexture.js'; import { createIndexBuffer, @@ -47,7 +46,7 @@ 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 { CoreNodeRenderState, CoreNode } from '../../CoreNode.js'; import { WebGlCtxRenderTexture } from './WebGlCtxRenderTexture.js'; import { Default } from '../../shaders/webgl/Default.js'; import type { WebGlShaderType } from './WebGlShaderNode.js'; @@ -61,6 +60,8 @@ interface CoreWebGlSystem { extensions: CoreWebGlExtensions; } +export type WebGlRenderOp = CoreNode | SdfRenderOp; + export class WebGlRenderer extends CoreRenderer { //// WebGL Native Context and Data glw: WebGlContextWrapper; @@ -253,30 +254,36 @@ export class WebGlRenderer extends CoreRenderer { * 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) { + addQuad(node: CoreNode) { const f = this.fQuadBuffer; const u = this.uiQuadBuffer; let i = this.curBufferIdx; - const reuse = this.reuseRenderOp(params); + const reuse = this.reuseRenderOp(node); if (reuse === false) { - this.newRenderOp(params, i); + this.newRenderOp(node, i); } - let tx = params.texture!; + let tx = node.renderTexture!; if (tx.type === TextureType.subTexture) { tx = (tx as SubTexture).parentTexture; } - const tidx = this.addTexture(tx.ctxTexture as WebGlCtxTexture, i); + const texture = tx.ctxTexture as WebGlCtxTexture; + let tidx = this.curRenderOp!.addTexture(texture); + + if (tidx === 0xffffffff) { + this.newRenderOp(node, i); + tidx = this.curRenderOp!.addTexture(texture); + } - const rc = params.renderCoords!; - const tc = params.textureCoords!; + const rc = node.renderCoords!; + const tc = node.renderTextureCoords!; - const cTl = params.colorTl; - const cTr = params.colorTr; - const cBl = params.colorBl; - const cBr = params.colorBr; + const cTl = node.premultipliedColorTl; + const cTr = node.premultipliedColorTr; + const cBl = node.premultipliedColorBl; + const cBr = node.premultipliedColorBr; // Upper-Left f[i] = rc.x1; @@ -329,104 +336,73 @@ export class WebGlRenderer extends CoreRenderer { * @param shader * @param bufferIdx */ - private newRenderOp(quad: QuadOptions | WebGlRenderOp, bufferIdx: number) { - const curRenderOp = new WebGlRenderOp(this, quad, bufferIdx); + private newRenderOp(node: CoreNode, bufferIdx: number) { + const curRenderOp = node; + curRenderOp.renderOpBufferIdx = bufferIdx; + curRenderOp.numQuads = 0; + curRenderOp.renderOpTextures.length = 0; + 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 { - 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!, 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 { - // Switching shader program will require a new render operation - if ( - this.curRenderOp?.shader.shaderKey !== - (params.shader as WebGlShaderNode).shaderKey - ) { + reuseRenderOp(node: CoreNode): boolean { + const curRenderOp = this.curRenderOp; + if (curRenderOp === null) { + return false; + } + + const shader = node.props.shader as WebGlShaderNode; + const curShader = curRenderOp.shader as WebGlShaderNode; + + if (curShader?.shaderKey !== shader?.shaderKey) { return false; } // Switching clipping rect will require a new render operation - if ( - compareRect(this.curRenderOp.clippingRect, params.clippingRect) === false - ) { + if (compareRect(curRenderOp.clippingRect, node.clippingRect) === false) { return false; } // Force new render operation if rendering to texture is different + const curRtt = curRenderOp.rtt; if ( - this.curRenderOp.parentHasRenderTexture !== - params.parentHasRenderTexture || - this.curRenderOp.rtt !== params.rtt + curRenderOp.parentHasRenderTexture !== node.parentHasRenderTexture || + curRtt !== (node.props.rtt === true) ) { return false; } if ( - params.parentHasRenderTexture === true && - this.curRenderOp.framebufferDimensions !== null && - params.framebufferDimensions !== null + node.parentHasRenderTexture === true && + node.parentFramebufferDimensions !== null ) { + const curFbDims = curRenderOp.isCoreNode + ? curRenderOp.parentFramebufferDimensions + : curRenderOp.framebufferDimensions; if ( - this.curRenderOp.framebufferDimensions.w !== - params.framebufferDimensions.w || - this.curRenderOp.framebufferDimensions.h !== - params.framebufferDimensions.h + curFbDims === null || + curFbDims.w !== node.parentFramebufferDimensions.w || + curFbDims.h !== node.parentFramebufferDimensions.h ) { return false; } } - if ( - this.curRenderOp.shader.shaderKey === 'default' && - params.shader?.shaderKey === 'default' - ) { + if (curShader?.shaderKey === 'default' && shader?.shaderKey === 'default') { return true; } // Check if the shader can batch the shader properties - if ( - this.curRenderOp.shader.program.reuseRenderOp( - params, - this.curRenderOp, - ) === false - ) { + if (curShader?.program.reuseRenderOp(node, curRenderOp) === false) { return false; } - // Render operation can be reused return true; } @@ -454,9 +430,9 @@ export class WebGlRenderer extends CoreRenderer { 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.renderOps[i]!.draw(this); } + 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 diff --git a/src/core/renderers/webgl/WebGlShaderNode.ts b/src/core/renderers/webgl/WebGlShaderNode.ts index a9860785..3d84cc8b 100644 --- a/src/core/renderers/webgl/WebGlShaderNode.ts +++ b/src/core/renderers/webgl/WebGlShaderNode.ts @@ -2,7 +2,6 @@ import type { CoreNode } from '../../CoreNode.js'; import { getNormalizedRgbaComponents } from '../../lib/utils.js'; import type { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js'; import type { Stage } from '../../Stage.js'; -import type { QuadOptions } from '../CoreRenderer.js'; import { CoreShaderNode, type CoreShaderType } from '../CoreShaderNode.js'; import type { UniformCollection, @@ -10,8 +9,7 @@ import type { Vec3, Vec4, } from './internal/ShaderUtils.js'; -import type { WebGlRenderer } from './WebGlRenderer.js'; -import type { WebGlRenderOp } from './WebGlRenderOp.js'; +import type { WebGlRenderer, WebGlRenderOp } from './WebGlRenderer.js'; import type { WebGlShaderProgram } from './WebGlShaderProgram.js'; export type ShaderSource = @@ -47,10 +45,7 @@ export type WebGlShaderType> = * This function is used to check if the shader can be reused based on quad info * @param props */ - canBatch?: ( - incomingQuad: QuadOptions, - currentRenderOp: WebGlRenderOp, - ) => boolean; + canBatch?: (node: CoreNode, currentRenderOp: WebGlRenderOp) => boolean; /** * extensions required for specific shader? */ diff --git a/src/core/renderers/webgl/WebGlShaderProgram.ts b/src/core/renderers/webgl/WebGlShaderProgram.ts index 29409329..f6fb01f4 100644 --- a/src/core/renderers/webgl/WebGlShaderProgram.ts +++ b/src/core/renderers/webgl/WebGlShaderProgram.ts @@ -18,12 +18,11 @@ */ import type { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js'; import { Default } from '../../shaders/webgl/Default.js'; -import type { QuadOptions } from '../CoreRenderer.js'; import type { CoreShaderProgram } from '../CoreShaderProgram.js'; import type { WebGlCtxTexture } from './WebGlCtxTexture.js'; -import type { WebGlRenderOp } from './WebGlRenderOp.js'; -import type { WebGlRenderer } from './WebGlRenderer.js'; +import type { WebGlRenderer, WebGlRenderOp } from './WebGlRenderer.js'; import type { WebGlShaderType } from './WebGlShaderNode.js'; +import { WebGlShaderNode } from './WebGlShaderNode.js'; import type { BufferCollection } from './internal/BufferCollection.js'; import { createProgram, @@ -33,6 +32,7 @@ import { type UniformSet3Params, type UniformSet4Params, } from './internal/ShaderUtils.js'; +import { CoreNode } from '../../CoreNode.js'; export class WebGlShaderProgram implements CoreShaderProgram { protected program: WebGLProgram | null; @@ -146,43 +146,46 @@ export class WebGlShaderProgram implements CoreShaderProgram { } } - reuseRenderOp( - incomingQuad: QuadOptions, - currentRenderOp: WebGlRenderOp, - ): boolean { + reuseRenderOp(node: CoreNode, currentRenderOp: WebGlRenderOp): boolean { if (this.lifecycle.canBatch !== undefined) { - return this.lifecycle.canBatch(incomingQuad, currentRenderOp); + return this.lifecycle.canBatch(node, currentRenderOp); } + const { time, worldAlpha, width, height } = node; + if (this.useTimeValue === true) { - if (incomingQuad.time !== currentRenderOp.time) { + if (time !== currentRenderOp.time) { return false; } } if (this.useSystemAlpha === true) { - if (incomingQuad.alpha !== currentRenderOp.alpha) { + if (worldAlpha !== currentRenderOp.worldAlpha) { return false; } } if (this.useSystemDimensions === true) { if ( - incomingQuad.width !== currentRenderOp.width || - incomingQuad.height !== currentRenderOp.height + width !== currentRenderOp.width || + height !== currentRenderOp.height ) { return false; } } + let shaderPropsA: Record | undefined = undefined; let shaderPropsB: Record | undefined = undefined; - if (incomingQuad.shader !== null) { - shaderPropsA = incomingQuad.shader.resolvedProps; + const shader = node.props.shader; + + if (shader !== null) { + shaderPropsA = (shader as WebGlShaderNode).resolvedProps; } - if (currentRenderOp.shader !== null) { - shaderPropsB = currentRenderOp.shader.resolvedProps; + const opShader = currentRenderOp.shader; + if (opShader !== null) { + shaderPropsB = (opShader as WebGlShaderNode).resolvedProps; } if ( @@ -204,10 +207,15 @@ export class WebGlShaderProgram implements CoreShaderProgram { } bindRenderOp(renderOp: WebGlRenderOp) { - this.bindBufferCollection(renderOp.buffers); - this.bindTextures(renderOp.textures); + const isCoreNode = renderOp.isCoreNode; - const { parentHasRenderTexture } = renderOp; + this.bindTextures(renderOp.renderOpTextures); + this.bindBufferCollection(renderOp.quadBufferCollection); + + const parentHasRenderTexture = renderOp.parentHasRenderTexture; + const framebufferDimensions = isCoreNode + ? renderOp.parentFramebufferDimensions + : renderOp.framebufferDimensions; // Skip if the parent and current operation both have render textures if (renderOp.rtt === true && parentHasRenderTexture === true) { @@ -216,8 +224,8 @@ export class WebGlShaderProgram implements CoreShaderProgram { // Bind render texture framebuffer dimensions as resolution // if the parent has a render texture - if (parentHasRenderTexture === true) { - const { w, h } = renderOp.framebufferDimensions!; + if (parentHasRenderTexture === true && framebufferDimensions) { + const { w, h } = framebufferDimensions; // Force pixel ratio to 1.0 for render textures since they are always 1:1 // the final render texture will be rendered to the screen with the correct pixel ratio this.glw.uniform1f('u_pixelRatio', 1.0); @@ -225,7 +233,8 @@ export class WebGlShaderProgram implements CoreShaderProgram { // Set resolution to the framebuffer dimensions this.glw.uniform2f('u_resolution', w, h); } else { - this.glw.uniform1f('u_pixelRatio', renderOp.renderer.stage.pixelRatio); + this.glw.uniform1f('u_pixelRatio', renderOp.stage.pixelRatio); + this.glw.uniform2f( 'u_resolution', this.glw.canvas.width, @@ -234,11 +243,11 @@ export class WebGlShaderProgram implements CoreShaderProgram { } if (this.useTimeValue === true) { - this.glw.uniform1f('u_time', renderOp.time as number); + this.glw.uniform1f('u_time', renderOp.time); } if (this.useSystemAlpha === true) { - this.glw.uniform1f('u_alpha', renderOp.alpha); + this.glw.uniform1f('u_alpha', renderOp.worldAlpha); } if (this.useSystemDimensions === true) { @@ -246,30 +255,34 @@ export class WebGlShaderProgram implements CoreShaderProgram { } /**temporary fix to make sdf texts work */ - if (renderOp.sdfShaderProps !== undefined) { - (renderOp.shader.shaderType as WebGlShaderType).onSdfBind?.call( + if (isCoreNode === false && renderOp.sdfShaderProps !== undefined) { + const opShader = renderOp.shader; // SdfRenderOp has .shader + (opShader.shaderType as WebGlShaderType).onSdfBind?.call( this.glw, renderOp.sdfShaderProps, ); return; } - if (renderOp.shader.props !== undefined) { + const shader = renderOp.shader as WebGlShaderNode; + if (shader.props !== undefined) { /** * loop over all precalculated uniform types */ - for (const key in renderOp.shader.uniforms.single) { - const { method, value } = renderOp.shader.uniforms.single[key]!; + const uniforms = shader.uniforms; + + for (const key in uniforms.single) { + const { method, value } = uniforms.single[key]!; this.glw[method as keyof UniformSet1Param](key, value as never); } - for (const key in renderOp.shader.uniforms.vec2) { - const { method, value } = renderOp.shader.uniforms.vec2[key]!; + for (const key in uniforms.vec2) { + const { method, value } = uniforms.vec2[key]!; this.glw[method as keyof UniformSet2Params](key, value[0], value[1]); } - for (const key in renderOp.shader.uniforms.vec3) { - const { method, value } = renderOp.shader.uniforms.vec3[key]!; + for (const key in uniforms.vec3) { + const { method, value } = uniforms.vec3[key]!; this.glw[method as keyof UniformSet3Params]( key, value[0], @@ -278,8 +291,8 @@ export class WebGlShaderProgram implements CoreShaderProgram { ); } - for (const key in renderOp.shader.uniforms.vec4) { - const { method, value } = renderOp.shader.uniforms.vec4[key]!; + for (const key in uniforms.vec4) { + const { method, value } = uniforms.vec4[key]!; this.glw[method as keyof UniformSet4Params]( key, value[0], diff --git a/src/core/text-rendering/SdfTextRenderer.ts b/src/core/text-rendering/SdfTextRenderer.ts index 4d401fa3..834118bf 100644 --- a/src/core/text-rendering/SdfTextRenderer.ts +++ b/src/core/text-rendering/SdfTextRenderer.ts @@ -29,7 +29,7 @@ import { hasZeroWidthSpace } from './Utils.js'; import * as SdfFontHandler from './SdfFontHandler.js'; import type { CoreRenderer } from '../renderers/CoreRenderer.js'; import { WebGlRenderer } from '../renderers/webgl/WebGlRenderer.js'; -import { WebGlRenderOp } from '../renderers/webgl/WebGlRenderOp.js'; +import { SdfRenderOp } from '../renderers/webgl/SdfRenderOp.js'; import { Sdf, type SdfShaderProps } from '../shaders/webgl/SdfShader.js'; import { BufferCollection } from '../renderers/webgl/internal/BufferCollection.js'; import type { WebGlCtxTexture } from '../renderers/webgl/WebGlCtxTexture.js'; @@ -238,28 +238,25 @@ const renderQuads = ( glw.arrayBufferData(buffer, vertexBuffer, glw.STATIC_DRAW as number); } - const renderOp = new WebGlRenderOp( + const renderOp = new SdfRenderOp( renderer as WebGlRenderer, + sdfShader!, // Ensure sdfShader is not null { - sdfShaderProps: { - transform: globalTransform, - color: mergeColorAlpha(color, worldAlpha), - size: layout.fontScale, // Use proper font scaling in shader - distanceRange: layout.distanceRange, - } satisfies SdfShaderProps, - sdfBuffers: webGlBuffers, - shader: sdfShader, - alpha: worldAlpha, - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment - clippingRect: renderProps.clippingRect as any, - height: layout.height, - width: layout.width, - rtt: false, - parentHasRenderTexture: renderProps.parentHasRenderTexture, - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment - framebufferDimensions: renderProps.framebufferDimensions as any, - }, - 0, + transform: globalTransform, + color: mergeColorAlpha(color, worldAlpha), + size: layout.fontScale, // Use proper font scaling in shader + distanceRange: layout.distanceRange, + } satisfies SdfShaderProps, + webGlBuffers, + worldAlpha, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + renderProps.clippingRect as any, + layout.width, + layout.height, + false, + renderProps.parentHasRenderTexture, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + renderProps.framebufferDimensions as any, ); // Add atlas texture and set quad count