fix(text): mark WorldPosition dirty after slot reallocation in _updateLocalData#2981
fix(text): mark WorldPosition dirty after slot reallocation in _updateLocalData#2981cptbtptpbcptdtptp wants to merge 1 commit intogalacean:dev/2.0from
Conversation
…eLocalData
Both Text (UI) and TextRenderer share a `bounds` getter that runs
`_updateLocalData` then checks `WorldPosition` dirty. `_updateLocalData`
internally `_freeTextChunks` + `_buildChunk → allocateSubChunk`, which
under PrimitiveChunk's first-fit + free-list-merge allocator can return
a slot previously owned by another renderer. `_buildChunk` writes UV
and color but never pos (pos is `_updatePosition`'s job), so the new
slot retains the previous owner's pos floats as residue.
Before this fix, when a path sets only `LocalPositionBounds` dirty
(e.g. `Text._onRootCanvasModify(ReferenceResolutionPerUnit)` in UI
Text), the bounds getter would:
1. see LocalPositionBounds → run _updateLocalData (slot may swap)
2. see WorldPosition not dirty → skip _updatePosition
3. _setDirtyFlagFalse(Font) clear all dirty bits at once
The next _render also sees clean dirty bits and uploads the residue
pos to GPU — the renderer ends up rendering at someone else's old
world position. In practice this manifested as text glyphs jumping
to the wrong spot or appearing missing after UI tab switches that
free + reallocate chunk slots in the same frame.
Fix: force WorldPosition dirty at the end of _updateLocalData so the
contract "after this call, pos must be rewritten" is unconditionally
honored regardless of which caller invoked it.
Tests cover three layers:
- dirty-flag invariant: _updateLocalData must leave WorldPosition
dirty on exit
- corrupted-slot: bounds getter with only LocalPositionBounds dirty
rewrites pos even when the slot memory is poisoned
- full slot-reuse repro: destroy a sibling renderer occupying a
lower offset, then trigger bounds getter on the survivor — its
pos must remain correct after the slot moves
Without the fix, all three regression tests fail with the survivor
rendering at the destroyed sibling's old position.
WalkthroughThis PR adds explicit Changes
Sequence DiagramsequenceDiagram
participant Caller as External Caller
participant UpdateLocal as _updateLocalData()
participant BuildChunk as _buildChunk()
participant DirtyFlag as Dirty Flag State
participant Bounds as bounds Getter
participant UpdatePos as _updatePosition()
Caller->>UpdateLocal: Call with cleared _dirtyFlag
UpdateLocal->>BuildChunk: Execute (updates UV/Color only)
BuildChunk-->>UpdateLocal: Complete
Note over UpdateLocal,DirtyFlag: FIX: Mark WorldPosition as dirty
UpdateLocal->>DirtyFlag: Set WorldPosition = dirty
DirtyFlag-->>UpdateLocal: Confirmed
UpdateLocal-->>Caller: Return
Caller->>Bounds: Access bounds property
Bounds->>DirtyFlag: Check WorldPosition dirty flag
alt WorldPosition is dirty
Bounds->>UpdatePos: Execute to rewrite vertex pos
UpdatePos->>UpdatePos: Overwrite stale pos data
UpdatePos-->>Bounds: Complete
end
Bounds-->>Caller: Return bounds value
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev/2.0 #2981 +/- ##
===========================================
+ Coverage 78.04% 78.14% +0.09%
===========================================
Files 906 906
Lines 99892 99902 +10
Branches 10190 10173 -17
===========================================
+ Hits 77960 78067 +107
+ Misses 21763 21665 -98
- Partials 169 170 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
🤖 Augment PR SummarySummary: Fixes a text rendering edge case where vertex-position data can be left as “residue” after text chunk slot reallocation, causing glyphs to jump/misrender. Changes:
Technical Notes: The fix ensures callers that only mark 🤖 Was this summary useful? React with 👍 or 👎 |
| */ | ||
| describe("Text - bounds-getter slot residue regression", async () => { | ||
| const canvas = document.createElement("canvas"); | ||
| const engine = await WebGLEngine.create({ canvas }); |
There was a problem hiding this comment.
tests/src/ui/Text.test.ts:170: This new regression suite creates a WebGLEngine but never calls engine.destroy(), and this file now creates two engines total. That can leak WebGL contexts across tests and make CI runs flaky due to context/resource exhaustion.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev/2.0 #2981 +/- ##
===========================================
+ Coverage 78.04% 78.14% +0.09%
===========================================
Files 906 906
Lines 99892 99902 +10
Branches 10190 10173 -17
===========================================
+ Hits 77960 78067 +107
+ Misses 21763 21665 -98
- Partials 169 170 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/src/core/2d/text/TextRenderer.test.ts (1)
401-403: 💤 Low valueConsider adding a note about maintaining sync with source enum.
These constants duplicate internal
DirtyFlagenum values fromTextRenderer.ts. If those values change, tests may silently pass/fail incorrectly.Consider adding a comment noting this dependency, or alternatively importing/exporting the enum (if feasible for the project's architecture).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/src/core/2d/text/TextRenderer.test.ts` around lines 401 - 403, Tests define TR_DIRTY_LOCAL_POSITION_BOUNDS and TR_DIRTY_WORLD_POSITION which duplicate the internal DirtyFlag enum from TextRenderer; update the test to either import/export the DirtyFlag enum from the TextRenderer module (preferred) or add a clear comment above TR_DIRTY_LOCAL_POSITION_BOUNDS and TR_DIRTY_WORLD_POSITION stating they must remain in sync with DirtyFlag in TextRenderer and reference the enum names, so future changes to DirtyFlag will be noticed and the test values updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@tests/src/core/2d/text/TextRenderer.test.ts`:
- Around line 401-403: Tests define TR_DIRTY_LOCAL_POSITION_BOUNDS and
TR_DIRTY_WORLD_POSITION which duplicate the internal DirtyFlag enum from
TextRenderer; update the test to either import/export the DirtyFlag enum from
the TextRenderer module (preferred) or add a clear comment above
TR_DIRTY_LOCAL_POSITION_BOUNDS and TR_DIRTY_WORLD_POSITION stating they must
remain in sync with DirtyFlag in TextRenderer and reference the enum names, so
future changes to DirtyFlag will be noticed and the test values updated
accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: c1b44e90-72b6-4c6d-934c-7e0f6fd29443
📒 Files selected for processing (4)
packages/core/src/2d/text/TextRenderer.tspackages/ui/src/component/advanced/Text.tstests/src/core/2d/text/TextRenderer.test.tstests/src/ui/Text.test.ts
Summary
_updateLocalData(bothTextRendererand UIText) calls_freeTextChunks+_buildChunk → allocateSubChunk, which underPrimitiveChunk's first-fit + free-list-merge allocator can hand back a slot previously owned by another renderer._buildChunkwrites UV/color but not pos, so the new slot retains the previous owner's pos floats as residue.boundsgetter path runs_updateLocalDatathen checksWorldPosition. When onlyLocalPositionBoundsis dirty (e.g. UI Text's_onRootCanvasModify(ReferenceResolutionPerUnit)),_updatePositionis skipped and_setDirtyFlagFalse(Font)clears all dirty bits at once. The next_renderthen uploads the residue pos to GPU — text glyphs jump to the wrong spot or appear missing after UI tab switches that free + reallocate chunk slots in the same frame.WorldPositiondirty at the end of_updateLocalDataso the contract "after this call, pos must be rewritten" is unconditionally honored regardless of caller.Test plan
_updateLocalDatamust leaveWorldPositiondirty on exit (dirty-flag invariant)boundsgetter with onlyLocalPositionBoundsdirty rewrites pos even when slot memory is poisoned (corrupted-slot)boundsgetter on the survivor, keeps the survivor's pos correct after the slot moves (full slot-reuse repro)🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests