Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/core/CoreNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3243,7 +3243,7 @@ export class CoreNode extends EventEmitter {
}

draw(renderer: WebGlRenderer) {
const { glw, options, stage } = renderer;
const { glw, stage } = renderer;
const shader = this.props.shader as any;

stage.shManager.useShader(shader.program);
Expand All @@ -3258,7 +3258,7 @@ export class CoreNode extends EventEmitter {
const clipWidth = Math.round(this.clippingRect.w * pixelRatio);
const clipHeight = Math.round(this.clippingRect.h * pixelRatio);
let clipY = Math.round(
options.canvas.height - clipHeight - this.clippingRect.y * pixelRatio,
glw.canvasH - 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
Expand Down
73 changes: 73 additions & 0 deletions src/core/Stage.renderListUpdate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, it, expect, vi } from 'vitest';
import { Stage } from './Stage.js';

/**
* Tests for the requestRenderListUpdate dedupe.
*
* A full Stage needs a live renderer, so the method is exercised on a minimal
* fake via the prototype. Only the fields requestRenderListUpdate touches are
* provided: renderer.invalidateQuadBuffer, renderListDirty, requestRender.
*/

type FakeStage = {
renderer: { invalidateQuadBuffer?: ReturnType<typeof vi.fn> };
renderListDirty: boolean;
requestRender: ReturnType<typeof vi.fn>;
};

const makeStage = (hasInvalidate = true): FakeStage => ({
renderer: hasInvalidate ? { invalidateQuadBuffer: vi.fn() } : {},
renderListDirty: false,
requestRender: vi.fn(),
});

const requestRenderListUpdate = (stage: FakeStage) =>
Stage.prototype.requestRenderListUpdate.call(stage as unknown as Stage);

describe('Stage.requestRenderListUpdate dedupe', () => {
it('invalidates the quad buffer and marks the list dirty on first call', () => {
const stage = makeStage();

requestRenderListUpdate(stage);

expect(stage.renderer.invalidateQuadBuffer!.mock.calls.length).toBe(1);
expect(stage.renderListDirty).toBe(true);
expect(stage.requestRender.mock.calls.length).toBe(1);
});

it('skips repeat invalidations while a rebuild is already pending', () => {
const stage = makeStage();

requestRenderListUpdate(stage);
requestRenderListUpdate(stage);
requestRenderListUpdate(stage);

// One structural invalidation for the whole burst (e.g. a row of cards
// flipping renderable in the same frame), but a render is requested each
// time so no frame is ever dropped.
expect(stage.renderer.invalidateQuadBuffer!.mock.calls.length).toBe(1);
expect(stage.requestRender.mock.calls.length).toBe(3);
expect(stage.renderListDirty).toBe(true);
});

it('invalidates again after drawFrame clears the dirty flag', () => {
const stage = makeStage();

requestRenderListUpdate(stage);
// drawFrame rebuilds the render list and clears the flag
stage.renderListDirty = false;
requestRenderListUpdate(stage);

expect(stage.renderer.invalidateQuadBuffer!.mock.calls.length).toBe(2);
expect(stage.renderListDirty).toBe(true);
});

it('handles renderers without invalidateQuadBuffer (canvas backend)', () => {
const stage = makeStage(false);

requestRenderListUpdate(stage);

expect(stage.renderListDirty).toBe(true);
expect(stage.requestRender.mock.calls.length).toBe(1);
});
});
12 changes: 11 additions & 1 deletion src/core/Stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -737,14 +737,24 @@ export class Stage {
}

requestRenderListUpdate() {
this.requestRender();

// A rebuild is already pending: the invalidation below ran when the flag
// was first raised, and the render list doesn't change until drawFrame
// rebuilds it. Skipping the repeat matters when many nodes flip renderable
// state in one frame (e.g. a whole row scrolling out of the viewport) —
// invalidateQuadBuffer walks the entire render list on every call.
if (this.renderListDirty === true) {
return;
}

// Notify the renderer that the render list is structurally changing.
// For the WebGL renderer this resets per-node buffer slot assignments
// and schedules a full GPU buffer re-upload on the next frame.
if (this.renderer.invalidateQuadBuffer !== undefined) {
this.renderer.invalidateQuadBuffer();
}
this.renderListDirty = true;
this.requestRender();
}

buildRenderList(node: CoreNode) {
Expand Down
24 changes: 23 additions & 1 deletion src/core/lib/WebGlContextWrapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function mockWebGl1(hasVaoExtension: boolean): WebGLRenderingContext {
deleteVertexArrayOES: (_vao: unknown) => undefined,
};
const target: Record<string, unknown> = {
canvas: {},
canvas: { width: 1920, height: 1080 },
drawingBufferWidth: 100,
drawingBufferHeight: 100,
getParameter: () => 8,
Expand Down Expand Up @@ -56,3 +56,25 @@ describe('WebGlContextWrapper VAO support', () => {
expect(glw.isWebGl2).toBe(false);
});
});

describe('WebGlContextWrapper canvas dimension cache', () => {
it('seeds canvasW/canvasH from the canvas at construction', () => {
const glw = new WebGlContextWrapper(mockWebGl1(true));
expect(glw.canvasW).toBe(1920);
expect(glw.canvasH).toBe(1080);
});

it('does not track canvas resizes until updateCanvasDimensions is called', () => {
const glw = new WebGlContextWrapper(mockWebGl1(true));
const canvas = glw.canvas as { width: number; height: number };

canvas.width = 1280;
canvas.height = 720;
expect(glw.canvasW).toBe(1920);
expect(glw.canvasH).toBe(1080);

glw.updateCanvasDimensions();
expect(glw.canvasW).toBe(1280);
expect(glw.canvasH).toBe(720);
});
});
24 changes: 23 additions & 1 deletion src/core/lib/WebGlContextWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,18 @@ export class WebGlContextWrapper {
//#endregion Vertex Array Objects

//#region Canvas
public readonly canvas;
public readonly canvas: HTMLCanvasElement | OffscreenCanvas;
/**
* Cached copies of `canvas.width`/`canvas.height`.
*
* @remarks
* On HTMLCanvasElement these are DOM getters that cross into Blink — too
* slow to read per render op per frame on embedded targets. The canvas is
* only resized via Renderer.updateAppDimensions, which refreshes this cache
* through {@link updateCanvasDimensions}.
*/
public canvasW: number;
public canvasH: number;
//#endregion Canvas

//#region WebGL Enums
Expand Down Expand Up @@ -170,6 +181,8 @@ export class WebGlContextWrapper {
this.isWebGl2 = this.gl2 !== null;

this.canvas = gl.canvas;
this.canvasW = gl.canvas.width;
this.canvasH = gl.canvas.height;

// Extract GLenums
this.MAX_RENDERBUFFER_SIZE = gl.MAX_RENDERBUFFER_SIZE;
Expand Down Expand Up @@ -447,6 +460,15 @@ export class WebGlContextWrapper {
this.gl.viewport(x, y, width, height);
}

/**
* Refresh {@link canvasW}/{@link canvasH} from the canvas element.
* Must be called after the canvas is resized.
*/
updateCanvasDimensions() {
this.canvasW = this.canvas.width;
this.canvasH = this.canvas.height;
}

/**
* ```
* gl.clearColor(red, green, blue, alpha);
Expand Down
4 changes: 2 additions & 2 deletions src/core/renderers/webgl/SdfRenderOp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class SdfRenderOp extends CoreRenderOp {
}

draw() {
const { glw, options, stage } = this.renderer;
const { glw, stage } = this.renderer;

stage.shManager.useShader(this.shader.program);
this.shader.program.bindRenderOp(this);
Expand All @@ -73,7 +73,7 @@ export class SdfRenderOp extends CoreRenderOp {
const clipWidth = Math.round(this.clippingRect.w * pixelRatio);
const clipHeight = Math.round(this.clippingRect.h * pixelRatio);
let clipY = Math.round(
options.canvas.height - clipHeight - this.clippingRect.y * pixelRatio,
glw.canvasH - 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
Expand Down
9 changes: 6 additions & 3 deletions src/core/renderers/webgl/WebGlRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ export class WebGlRenderer extends CoreRenderer {
gl,
options.disableVertexArrayObject,
));
glw.viewport(0, 0, options.canvas.width, options.canvas.height);
glw.viewport(0, 0, glw.canvasW, glw.canvasH);

this.attachContextLossListeners(options.canvas);

Expand Down Expand Up @@ -1264,7 +1264,7 @@ export class WebGlRenderer extends CoreRenderer {
// Bind the default framebuffer
glw.bindFramebuffer(null);

glw.viewport(0, 0, this.glw.canvas.width, this.glw.canvas.height);
glw.viewport(0, 0, glw.canvasW, glw.canvasH);
this.renderToTextureActive = false;
}

Expand Down Expand Up @@ -1331,7 +1331,10 @@ export class WebGlRenderer extends CoreRenderer {
}

updateViewport(): void {
this.glw.viewport(0, 0, this.glw.canvas.width, this.glw.canvas.height);
// Called after the canvas is resized (Renderer.updateAppDimensions) —
// refresh the wrapper's cached dimensions before they're used again.
this.glw.updateCanvasDimensions();
this.glw.viewport(0, 0, this.glw.canvasW, this.glw.canvasH);
}

removeRTTNode(node: CoreNode) {
Expand Down
4 changes: 2 additions & 2 deletions src/core/renderers/webgl/WebGlShaderProgram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,8 @@ export class WebGlShaderProgram implements CoreShaderProgram {
resolutionH = framebufferDimensions.h;
} else {
pixelRatio = renderOp.stage.pixelRatio;
resolutionW = this.glw.canvas.width;
resolutionH = this.glw.canvas.height;
resolutionW = this.glw.canvasW;
resolutionH = this.glw.canvasH;
}

if (pixelRatio !== this.lastPixelRatio) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ type FakeGlw = {
uniform1f: ReturnType<typeof vi.fn>;
uniform2f: ReturnType<typeof vi.fn>;
uniform4f: ReturnType<typeof vi.fn>;
canvas: { width: number; height: number };
canvasW: number;
canvasH: number;
};

const makeProgram = (): { program: WebGlShaderProgram; glw: FakeGlw } => {
const glw: FakeGlw = {
uniform1f: vi.fn(),
uniform2f: vi.fn(),
uniform4f: vi.fn(),
canvas: { width: 1920, height: 1080 },
canvasW: 1920,
canvasH: 1080,
};

const program = Object.create(
Expand Down
Loading