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); +}