From 740a499ebabbd201287cce2522106ff7c61e63fa Mon Sep 17 00:00:00 2001 From: Frank Weindel <6070611+frank-weindel@users.noreply.github.com> Date: Tue, 19 Sep 2023 17:06:44 -0400 Subject: [PATCH] Implement automatic `$` prefixed shader props The `u_dimensions` uniform used by both RoundedRectangleShader and DynamicShader are automatically filled in per quad that is rendered. The was it was done originally makes it difficult for the renderer to tell generally if two consecutive quads that use the same shaders can be batched into the same render operation. With `$` prefixed automatic shader props, Shaders opt-into using them and simply set their defaults to a compatible type (for `$dimensions` it is simply a Rect). The renderer, during addQuad() will fill our the correct values for automatic shader props prior to rendering. Shaders now can implement a `canBatchShaderProps()` method to compare two sets of shader props to see if it can be batched into the same render opertion. The default behavior is to return false. Some Shaders like the DynamicShader may opt not to implement this at all if the overhead of comparing the two sets of shader props is too high. --- examples/index.ts | 34 +++++---- src/core/renderers/CoreShader.ts | 7 +- src/core/renderers/webgl/WebGlCoreRenderOp.ts | 3 +- src/core/renderers/webgl/WebGlCoreRenderer.ts | 28 ++++++- src/core/renderers/webgl/WebGlCoreShader.ts | 73 ++++++++++++++++--- .../renderers/webgl/shaders/DynamicShader.ts | 34 +++++---- .../webgl/shaders/RoundedRectangle.ts | 34 +++++---- src/core/renderers/webgl/shaders/SdfShader.ts | 2 +- src/utils.ts | 15 +++- 9 files changed, 166 insertions(+), 64 deletions(-) diff --git a/examples/index.ts b/examples/index.ts index 69b46dd3..69fef015 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -31,9 +31,13 @@ import type { ExampleSettings } from './common/ExampleSettings.js'; (async () => { // URL params + // - driver: main | threadx (default: threadx) + // - test: (default: test) + // - showOverlay: true | false (default: true) const urlParams = new URLSearchParams(window.location.search); let driverName = urlParams.get('driver'); const test = urlParams.get('test') || 'test'; + const showOverlay = urlParams.get('overlay') !== 'false'; if (driverName !== 'main' && driverName !== 'threadx') { driverName = 'threadx'; @@ -72,20 +76,22 @@ import type { ExampleSettings } from './common/ExampleSettings.js'; assertTruthy(canvas instanceof HTMLCanvasElement); - const overlayText = renderer.createTextNode({ - color: 0xff0000ff, - text: `Test: ${test} | Driver: ${driverName}`, - zIndex: 99999, - parent: renderer.root, - fontSize: 50, - }); - overlayText.once( - 'textLoaded', - (target: any, { width, height }: Dimensions) => { - overlayText.x = appDimensions.width - width - 20; - overlayText.y = appDimensions.height - height - 20; - }, - ); + if (showOverlay) { + const overlayText = renderer.createTextNode({ + color: 0xff0000ff, + text: `Test: ${test} | Driver: ${driverName}`, + zIndex: 99999, + parent: renderer.root, + fontSize: 50, + }); + overlayText.once( + 'textLoaded', + (target: any, { width, height }: Dimensions) => { + overlayText.x = appDimensions.width - width - 20; + overlayText.y = appDimensions.height - height - 20; + }, + ); + } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const module = await import(`./tests/${test}.ts`); diff --git a/src/core/renderers/CoreShader.ts b/src/core/renderers/CoreShader.ts index dc16dc5e..e3d12d97 100644 --- a/src/core/renderers/CoreShader.ts +++ b/src/core/renderers/CoreShader.ts @@ -31,8 +31,11 @@ export abstract class CoreShader { return {}; } - abstract bindRenderOp(renderOp: CoreRenderOp): void; - abstract bindProps(props: Record): void; + abstract bindRenderOp( + renderOp: CoreRenderOp, + props: Record | null, + ): void; + protected abstract bindProps(props: Record): void; abstract attach(): void; abstract detach(): void; } diff --git a/src/core/renderers/webgl/WebGlCoreRenderOp.ts b/src/core/renderers/webgl/WebGlCoreRenderOp.ts index c3d80478..b466ba19 100644 --- a/src/core/renderers/webgl/WebGlCoreRenderOp.ts +++ b/src/core/renderers/webgl/WebGlCoreRenderOp.ts @@ -73,8 +73,7 @@ export class WebGlCoreRenderOp extends CoreRenderOp { const { shManager } = options; shManager.useShader(shader); - shader.bindRenderOp(this); - shader.bindProps(shaderProps); + shader.bindRenderOp(this, shaderProps); // TODO: Reduce calculations required const quadIdx = (this.bufferIdx / 24) * 6 * 2; diff --git a/src/core/renderers/webgl/WebGlCoreRenderer.ts b/src/core/renderers/webgl/WebGlCoreRenderer.ts index ed8da838..ffff1070 100644 --- a/src/core/renderers/webgl/WebGlCoreRenderer.ts +++ b/src/core/renderers/webgl/WebGlCoreRenderer.ts @@ -20,6 +20,7 @@ import { assertTruthy, createWebGLContext, + hasOwn, mergeColorAlphaPremultiplied, } from '../../../utils.js'; import { @@ -48,9 +49,10 @@ import type { import { CoreShaderManager } from '../../CoreShaderManager.js'; import type { CoreShader } from '../CoreShader.js'; import { BufferCollection } from './internal/BufferCollection.js'; -import { getNormalizedRgbaComponents } from '../../lib/utils.js'; +import { getNormalizedRgbaComponents, type Rect } from '../../lib/utils.js'; import type { Dimensions } from '../../../common/CommonTypes.js'; import { WebGlCoreShader } from './WebGlCoreShader.js'; +import { RoundedRectangle } from './shaders/RoundedRectangle.js'; const WORDS_PER_QUAD = 24; const BYTES_PER_QUAD = WORDS_PER_QUAD * 4; @@ -94,7 +96,7 @@ export class WebGlCoreRenderer extends CoreRenderer { renderables: Array = []; //// Default Shader - defaultShader: CoreShader; + defaultShader: WebGlCoreShader; quadBufferCollection: BufferCollection; /** @@ -231,6 +233,17 @@ export class WebGlCoreRenderer extends CoreRenderer { } = params; let { texture } = params; + /** + * If the shader props contain any automatic properties, update it with the + * current dimensions that will be used to render the quad. + */ + if (shaderProps && hasOwn(shaderProps, '$dimensions')) { + const dimensions = shaderProps.$dimensions as Dimensions; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + dimensions.width = width; + dimensions.height = height; + } + texture = texture ?? this.defaultTexture; assertTruthy(texture instanceof Texture, 'Invalid texture type'); @@ -241,13 +254,20 @@ export class WebGlCoreRenderer extends CoreRenderer { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment }; const targetShader = shader || this.defaultShader; + assertTruthy(targetShader instanceof WebGlCoreShader); if (curRenderOp) { + // If the current render op is not the same shader, create a new one + // If the current render op's shader props are not compatible with the + // the new shader props, create a new one render op. if (curRenderOp.shader !== targetShader) { curRenderOp = null; } else if ( curRenderOp.shader !== this.defaultShader && - (curRenderOp.shaderProps !== shaderProps || - curRenderOp.dimensions !== targetDims) + (!shaderProps || + !curRenderOp.shader.canBatchShaderProps( + curRenderOp.shaderProps, + shaderProps, + )) ) { curRenderOp = null; } diff --git a/src/core/renderers/webgl/WebGlCoreShader.ts b/src/core/renderers/webgl/WebGlCoreShader.ts index 941af287..16fefda4 100644 --- a/src/core/renderers/webgl/WebGlCoreShader.ts +++ b/src/core/renderers/webgl/WebGlCoreShader.ts @@ -17,7 +17,8 @@ * limitations under the License. */ -import { assertTruthy } from '../../../utils.js'; +import type { Dimensions } from '../../../common/CommonTypes.js'; +import { assertTruthy, hasOwn } from '../../../utils.js'; import { CoreShader } from '../CoreShader.js'; import type { WebGlCoreCtxTexture } from './WebGlCoreCtxTexture.js'; import type { WebGlCoreRenderOp } from './WebGlCoreRenderOp.js'; @@ -34,6 +35,24 @@ import { } from './internal/ShaderUtils.js'; import { isWebGl2 } from './internal/WebGlUtils.js'; +/** + * Automatic shader prop for the dimensions of the Node being rendered + * + * @remarks + * Shader's who's rendering depends on the dimensions of the Node being rendered + * should extend this interface from their Prop interface type. + */ +export interface DimensionsShaderProp { + /** + * Dimensions of the Node being rendered (Auto-set by the renderer) + * + * @remarks + * DO NOT SET THIS. It is set automatically by the renderer. + * Any values set here will be ignored. + */ + $dimensions?: Dimensions; +} + export abstract class WebGlCoreShader extends CoreShader { protected boundBufferCollection: BufferCollection | null = null; protected buffersBound = false; @@ -197,12 +216,52 @@ export abstract class WebGlCoreShader extends CoreShader { this.boundBufferCollection = null; } - bindRenderOp(renderOp: WebGlCoreRenderOp) { + /** + * Given two sets of Shader props destined for this Shader, determine if they can be batched together + * to reduce the number of draw calls. + * + * @remarks + * This is used by the {@link WebGlCoreRenderer} to determine if it can batch multiple consecutive draw + * calls into a single draw call. + * + * By default, this returns false (meaning no batching is allowed), but can be + * overridden by child classes to provide more efficient batching. + * + * @param propsA + * @param propsB + * @returns + */ + canBatchShaderProps( + propsA: Record, + propsB: Record, + ): boolean { + return false; + } + + bindRenderOp( + renderOp: WebGlCoreRenderOp, + props: Record | null, + ) { this.bindBufferCollection(renderOp.buffers); if (renderOp.textures.length > 0) { this.bindTextures(renderOp.textures); } - this.bindUniforms(renderOp); + const { gl } = renderOp; + // Bind standard automatic uniforms + this.setUniform('u_resolution', [gl.canvas.width, gl.canvas.height]); // !!! + this.setUniform('u_pixelRatio', renderOp.options.pixelRatio); + if (props) { + // Bind optional automatic uniforms + // These are only bound if their keys are present in the props. + if (hasOwn(props, '$dimensions')) { + let dimensions = props.$dimensions as Dimensions | null; + if (!dimensions) { + dimensions = renderOp.dimensions; + } + this.setUniform('u_dimensions', [dimensions.width, dimensions.height]); + } + this.bindProps(props); + } } setUniform(name: string, value: any): void { @@ -228,7 +287,7 @@ export abstract class WebGlCoreShader extends CoreShader { this.boundBufferCollection = buffer; } - override bindProps(props: Record) { + protected override bindProps(props: Record) { // Implement in child class } @@ -236,12 +295,6 @@ export abstract class WebGlCoreShader extends CoreShader { // no defaults } - bindUniforms(renderOp: WebGlCoreRenderOp) { - const { gl } = renderOp; - this.setUniform('u_resolution', [gl.canvas.width, gl.canvas.height]); - this.setUniform('u_pixelRatio', renderOp.options.pixelRatio); - } - override attach(): void { this.gl.useProgram(this.program); if (isWebGl2(this.gl) && this.vao) { diff --git a/src/core/renderers/webgl/shaders/DynamicShader.ts b/src/core/renderers/webgl/shaders/DynamicShader.ts index 34bae18f..174a6b38 100644 --- a/src/core/renderers/webgl/shaders/DynamicShader.ts +++ b/src/core/renderers/webgl/shaders/DynamicShader.ts @@ -18,7 +18,10 @@ */ import type { ExtractProps } from '../../../CoreTextureManager.js'; import type { WebGlCoreRenderer } from '../WebGlCoreRenderer.js'; -import { WebGlCoreShader } from '../WebGlCoreShader.js'; +import { + WebGlCoreShader, + type DimensionsShaderProp, +} from '../WebGlCoreShader.js'; import type { UniformInfo } from '../internal/ShaderUtils.js'; import type { WebGlCoreRenderOp } from '../WebGlCoreRenderOp.js'; import type { WebGlCoreCtxTexture } from '../WebGlCoreCtxTexture.js'; @@ -33,7 +36,7 @@ import { BorderBottomEffect } from './effects/BorderBottomEffect.js'; import { BorderLeftEffect } from './effects/BorderLeftEffect.js'; import { GlitchEffect } from './effects/GlitchEffect.js'; -export interface DynamicShaderProps { +export interface DynamicShaderProps extends DimensionsShaderProp { effects?: EffectDesc[]; } @@ -98,13 +101,7 @@ export class DynamicShader extends WebGlCoreShader { gl.bindTexture(gl.TEXTURE_2D, textures[0]!.ctxTexture); } - override bindUniforms(renderOp: WebGlCoreRenderOp) { - super.bindUniforms(renderOp); - const { width = 100, height = 100 } = renderOp.dimensions; - this.setUniform('u_dimensions', [width, height]); - } - - override bindProps(props: DynamicShaderProps): void { + protected override bindProps(props: Required): void { props.effects?.forEach((eff, index) => { const effect = this.effects[index]!; const fxClass = Effects[effect.name as keyof EffectMap]; @@ -313,13 +310,18 @@ export class DynamicShader extends WebGlCoreShader { } static override resolveDefaults( - props: DynamicShaderProps = {}, - ): Record { - props.effects = (props.effects ?? []).map((effect) => ({ - type: effect.type, - props: Effects[effect.type].resolveDefaults(effect.props || {}), - })); - return props as Record; + props: DynamicShaderProps, + ): Required { + return { + effects: (props.effects ?? []).map((effect) => ({ + type: effect.type, + props: Effects[effect.type].resolveDefaults(effect.props || {}), + })), + $dimensions: { + width: 0, + height: 0, + }, + }; } static override makeCacheKey(props: DynamicShaderProps): string { diff --git a/src/core/renderers/webgl/shaders/RoundedRectangle.ts b/src/core/renderers/webgl/shaders/RoundedRectangle.ts index 086278c8..f37f15df 100644 --- a/src/core/renderers/webgl/shaders/RoundedRectangle.ts +++ b/src/core/renderers/webgl/shaders/RoundedRectangle.ts @@ -18,19 +18,21 @@ */ import type { WebGlCoreRenderer } from '../WebGlCoreRenderer.js'; -import { WebGlCoreShader } from '../WebGlCoreShader.js'; +import { + WebGlCoreShader, + type DimensionsShaderProp, +} from '../WebGlCoreShader.js'; import type { WebGlCoreCtxTexture } from '../WebGlCoreCtxTexture.js'; import type { ShaderProgramSources } from '../internal/ShaderUtils.js'; -import type { WebGlCoreRenderOp } from '../WebGlCoreRenderOp.js'; /** * Properties of the {@link RoundedRectangle} shader */ -export interface RoundedRectangleProps { +export interface RoundedRectangleProps extends DimensionsShaderProp { /** * Corner radius, in pixels, to cut out of the corners * - * @default 10 + * @defaultValue 10 */ radius?: number; } @@ -66,6 +68,10 @@ export class RoundedRectangle extends WebGlCoreShader { ): Required { return { radius: props.radius || 10, + $dimensions: { + width: 0, + height: 0, + }, }; } @@ -75,17 +81,19 @@ export class RoundedRectangle extends WebGlCoreShader { gl.bindTexture(gl.TEXTURE_2D, textures[0]!.ctxTexture); } - override bindUniforms(renderOp: WebGlCoreRenderOp) { - super.bindUniforms(renderOp); - const { width = 100, height = 100 } = renderOp.dimensions; - this.setUniform('u_dimensions', [width, height]); + protected override bindProps(props: Required): void { + this.setUniform('u_radius', props.radius); } - override bindProps(props: RoundedRectangleProps): void { - for (const key in props) { - // @ts-expect-error to fancy code - this.setUniform(`u_${key}`, props[key]); - } + override canBatchShaderProps( + propsA: Required, + propsB: Required, + ): boolean { + return ( + propsA.radius === propsB.radius && + propsA.$dimensions.width === propsB.$dimensions.width && + propsA.$dimensions.height === propsB.$dimensions.height + ); } static override shaderSources: ShaderProgramSources = { diff --git a/src/core/renderers/webgl/shaders/SdfShader.ts b/src/core/renderers/webgl/shaders/SdfShader.ts index 9a00cc9f..8101c599 100644 --- a/src/core/renderers/webgl/shaders/SdfShader.ts +++ b/src/core/renderers/webgl/shaders/SdfShader.ts @@ -82,7 +82,7 @@ export class SdfShader extends WebGlCoreShader { gl.bindTexture(gl.TEXTURE_2D, textures[0]!.ctxTexture); } - override bindProps(props: SdfShaderProps): void { + protected override bindProps(props: SdfShaderProps): void { const resolvedProps = SdfShader.resolveDefaults(props); for (const key in resolvedProps) { if (key === 'offset') { diff --git a/src/utils.ts b/src/utils.ts index 960185fd..8ce3223f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -160,10 +160,10 @@ export function mergeColorAlpha(rgba: number, alpha: number): number { * NOTE: This method returns a PREMULTIPLIED alpha color which is generally only useful * in the context of the internal rendering process. Use {@link mergeColorAlpha} if you * need to blend an alpha value into a color in the context of the Renderer's - * public API. + * main API. * * @internalRemarks - * Do not expose this method in the public API because Renderer users should instead use + * Do not expose this method in the main API because Renderer users should instead use * {@link mergeColorAlpha} to manipulate the alpha value of a color. * * @internal @@ -189,3 +189,14 @@ export function mergeColorAlphaPremultiplied( return ((r << 24) | (g << 16) | (b << 8) | a) >>> 0; } + +/** + * Returns true if the given object has the given "own" property. + * + * @param obj + * @param prop + * @returns + */ +export function hasOwn(obj: object, prop: string | number | symbol): boolean { + return Object.prototype.hasOwnProperty.call(obj, prop); +}