From d5c6a5eb87ad22166690dd1bda42702ced45ebcc Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Tue, 9 Jun 2026 22:47:39 -0400 Subject: [PATCH] perf(core): only fire RecalcUniforms on dimension changes, not every transform Every Global update (including pure translation) raised RecalcUniforms, so each moving shaded node invoked shader.update() every frame of a scroll just to hit the value-key cache and return. Shader uniforms are a function of resolvedProps + node w/h only - that is exactly the shader value-key cache key - so transform-only changes can never affect them. RecalcUniforms is now raised exactly where it can matter: - the w/h setters - Autosizer.applyDimensions (direct props.w/h write) - CoreTextNode layout application (direct props.w/h write) - the shader setter (already did; covers initial assignment) Shader prop setters already raise the flag themselves. Co-Authored-By: Claude Fable 5 --- src/core/Autosizer.ts | 4 +- src/core/CoreNode.test.ts | 83 +++++++++++++++++++++++++++++++++++++++ src/core/CoreNode.ts | 16 ++++++-- src/core/CoreTextNode.ts | 8 +++- 4 files changed, 105 insertions(+), 6 deletions(-) diff --git a/src/core/Autosizer.ts b/src/core/Autosizer.ts index 34b4f10..6cd7f78 100644 --- a/src/core/Autosizer.ts +++ b/src/core/Autosizer.ts @@ -15,7 +15,9 @@ export enum AutosizeUpdateType { const applyDimensions = (node: CoreNode, w: number, h: number) => { node.props.w = w; node.props.h = h; - node.setUpdateType(UpdateType.Local); + // Direct props.w/h write bypasses the w/h setters, so raise RecalcUniforms + // here too — dimensions feed shader uniforms. + node.setUpdateType(UpdateType.Local | UpdateType.RecalcUniforms); }; const getFilteredChildren = ( diff --git a/src/core/CoreNode.test.ts b/src/core/CoreNode.test.ts index 90751b0..bbcc44f 100644 --- a/src/core/CoreNode.test.ts +++ b/src/core/CoreNode.test.ts @@ -1180,4 +1180,87 @@ describe('set color()', () => { expect(node.clippingRect.h).toBe(30); }); }); + + describe('RecalcUniforms scoping', () => { + const makeAttachedNode = () => { + // Fresh stage per node: earlier tests can mutate the shared stage + // mock's bound objects through by-reference strictBound assignment + // in createRenderBounds. + const localStage = mock({ + strictBound: createBound(0, 0, 200, 200), + preloadBound: createBound(0, 0, 200, 200), + defaultTexture: { + state: 'loaded', + }, + renderer: mock() as CoreRenderer, + }); + const parent = new CoreNode(localStage, defaultProps()); + parent.globalTransform = Matrix3d.identity(); + parent.worldAlpha = 1; + + const node = new CoreNode( + localStage, + defaultProps({ parent, w: 100, h: 100 }), + ); + node.alpha = 1; + return node; + }; + + it('should not set RecalcUniforms on pure translation', () => { + const node = makeAttachedNode(); + node.update(0, clippingRect); + + node.x = 50; + node.y = 25; + + expect(node.updateType & UpdateType.Local).toBe(UpdateType.Local); + expect(node.updateType & UpdateType.RecalcUniforms).toBe(0); + }); + + it('should set RecalcUniforms when w changes', () => { + const node = makeAttachedNode(); + node.update(0, clippingRect); + + node.w = 150; + + expect(node.updateType & UpdateType.RecalcUniforms).toBe( + UpdateType.RecalcUniforms, + ); + }); + + it('should set RecalcUniforms when h changes', () => { + const node = makeAttachedNode(); + node.update(0, clippingRect); + + node.h = 75; + + expect(node.updateType & UpdateType.RecalcUniforms).toBe( + UpdateType.RecalcUniforms, + ); + }); + + it('should run the shader updater on resize but not on translation', () => { + const node = makeAttachedNode(); + const shader = { + shaderKey: 'test', + update: vi.fn(), + attachNode: vi.fn(), + time: undefined, + }; + // Assignment raises RecalcUniforms | IsRenderable via the setter + node.shader = shader as never; + node.update(0, clippingRect); + expect(shader.update).toHaveBeenCalledTimes(1); + + // Pure translation: no uniform recompute + node.x = 50; + node.update(0, clippingRect); + expect(shader.update).toHaveBeenCalledTimes(1); + + // Resize: uniforms depend on dimensions, must recompute + node.w = 150; + node.update(0, clippingRect); + expect(shader.update).toHaveBeenCalledTimes(2); + }); + }); }); diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index a951391..6f1386c 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -1355,7 +1355,13 @@ export class CoreNode extends EventEmitter { this.calculateRenderCoords(); this.updateBoundingRect(); - updateType |= UpdateType.RenderState | UpdateType.RecalcUniforms; + // RecalcUniforms is intentionally NOT set here: shader uniforms are a + // function of resolvedProps + w/h only (that is exactly the shader + // value-key cache key), so pure transform changes (translate, scale, + // rotate) cannot affect them. The flag is raised where w/h actually + // change: the w/h setters, Autosizer.applyDimensions, text layout + // application, and the shader setter itself. + updateType |= UpdateType.RenderState; //only propagate children updates if not autosizing if ((updateType & UpdateType.Autosize) === 0) { @@ -2232,7 +2238,9 @@ export class CoreNode extends EventEmitter { const props = this.props; if (props.w !== value) { props.w = value; - let updateType = UpdateType.Local; + // Dimensions feed shader uniforms (e.g. factored corner radius), so a + // resize must recompute them; see the Global-update branch in update(). + let updateType = UpdateType.Local | UpdateType.RecalcUniforms; if ( props.texture !== null && @@ -2262,7 +2270,9 @@ export class CoreNode extends EventEmitter { const props = this.props; if (props.h !== value) { props.h = value; - let updateType = UpdateType.Local; + // Dimensions feed shader uniforms (e.g. factored corner radius), so a + // resize must recompute them; see the Global-update branch in update(). + let updateType = UpdateType.Local | UpdateType.RecalcUniforms; if ( props.texture !== null && diff --git a/src/core/CoreTextNode.ts b/src/core/CoreTextNode.ts index fca319b..639034b 100644 --- a/src/core/CoreTextNode.ts +++ b/src/core/CoreTextNode.ts @@ -271,8 +271,12 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps { this.props.w = width; this.props.h = height; - // Text dimensions may have changed, recalculate transforms and bounds - this.setUpdateType(UpdateType.Local | UpdateType.RenderBounds); + // Text dimensions may have changed, recalculate transforms and bounds. + // RecalcUniforms because the direct props.w/h write above bypasses the + // w/h setters and dimensions feed shader uniforms. + this.setUpdateType( + UpdateType.Local | UpdateType.RenderBounds | UpdateType.RecalcUniforms, + ); // Handle SDF renderer (uses layout caching) if (textRendererType === 'sdf') {