Skip to content

fix(text): propagate WorldPosition dirty in _onRootCanvasModify when ReferenceResolutionPerUnit changes#2981

Merged
cptbtptpbcptdtptp merged 5 commits into
galacean:dev/2.0from
cptbtptpbcptdtptp:fix/text-bounds-dirty-flag
May 7, 2026
Merged

fix(text): propagate WorldPosition dirty in _onRootCanvasModify when ReferenceResolutionPerUnit changes#2981
cptbtptpbcptdtptp merged 5 commits into
galacean:dev/2.0from
cptbtptpbcptdtptp:fix/text-bounds-dirty-flag

Conversation

@cptbtptpbcptdtptp

@cptbtptpbcptdtptp cptbtptpbcptdtptp commented Apr 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • _updateLocalData (both TextRenderer and UI Text) calls _freeTextChunks + _buildChunk → allocateSubChunk, which under PrimitiveChunk's first-fit + free-list-merge allocator can hand back a slot previously owned by another renderer. _buildChunk writes UV/color but not pos, so the new slot retains the previous owner's pos floats as residue.
  • The bounds getter path runs _updateLocalData then checks WorldPosition. When only LocalPositionBounds is dirty (e.g. UI Text's _onRootCanvasModify(ReferenceResolutionPerUnit)), _updatePosition is skipped and _setDirtyFlagFalse(Font) clears all dirty bits at once. The next _render then 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.
  • Fix: force WorldPosition dirty at the end of _updateLocalData so the contract "after this call, pos must be rewritten" is unconditionally honored regardless of caller.

Test plan

  • _updateLocalData must leave WorldPosition dirty on exit (dirty-flag invariant)
  • bounds getter with only LocalPositionBounds dirty rewrites pos even when slot memory is poisoned (corrupted-slot)
  • Destroying a sibling renderer occupying a lower offset, then triggering bounds getter on the survivor, keeps the survivor's pos correct after the slot moves (full slot-reuse repro)
  • All three regression tests fail without this fix and pass with it

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed text rendering to ensure positions and bounds update reliably when the UI canvas reference resolution per unit changes, preventing visual drift and ensuring correct placement across dynamic canvas updates.
  • Tests

    • Added regression tests validating text vertex/world-position updates during canvas resolution changes and when sibling text components are removed, improving robustness of layout and rendering behavior.

…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.
@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4776e7dd-a306-4447-809d-193f000d7bb9

📥 Commits

Reviewing files that changed from the base of the PR and between d61472d and a74b231.

📒 Files selected for processing (1)
  • packages/ui/src/component/advanced/Text.ts

Walkthrough

A one-line fix in Text._onRootCanvasModify adds DirtyFlag.WorldPosition to the dirty-flag update when reference resolution per unit changes, ensuring vertex positions are recalculated. Regression tests validate vertex position rewrites via a new introspection helper.

Changes

Text world-position dirty flag fix

Layer / File(s) Summary
Implementation: mark WorldPosition dirty on resolution change
packages/ui/src/component/advanced/Text.ts
In _onRootCanvasModify, mark DirtyFlag.WorldPosition in addition to DirtyFlag.LocalPositionBounds when ReferenceResolutionPerUnit changes, triggering vertex position recalculation via _updatePosition().
Tests: vertex position introspection helper
tests/src/ui/Text.test.ts
Add readTextPosFloats helper to directly read per-vertex position components from internal _textChunks for test assertions.
Tests: regression suite for dirty propagation
tests/src/ui/Text.test.ts
Add regression tests validating vertex position rewrites on referenceResolutionPerUnit changes and correct position preservation after sibling component destruction; include WebGLEngine teardown.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble at flags, a tiny tweak,
WorldPosition wakes when pixels speak,
Vertices shift and tests will see,
Bounds and transforms in harmony,
A quiet hop for rendering's tweak.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately describes the main change: marking WorldPosition dirty in _onRootCanvasModify when ReferenceResolutionPerUnit changes, which is the core fix applied to Text.ts.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Apr 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.14%. Comparing base (a6f0504) to head (7e0a873).

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     
Flag Coverage Δ
unittests 78.14% <100.00%> (+0.09%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@augmentcode

augmentcode Bot commented Apr 30, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: 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:

  • In both core TextRenderer and UI Text, force DirtyFlag.WorldPosition after _updateLocalData rebuilds chunks so _updatePosition always rewrites vertex pos.
  • Documents the allocator/slot-reuse scenario in code comments to clarify why UV/color-only writes are insufficient.
  • Adds regression tests for TextRenderer and UI Text to enforce the dirty-flag invariant and validate the bounds-getter path rewrites positions even with “poisoned” slot memory.
  • Includes an end-to-end repro test where a sibling renderer is destroyed and the survivor reuses the freed slot without position corruption.

Technical Notes: The fix ensures callers that only mark LocalPositionBounds (e.g., root canvas resolution changes) still trigger a position rewrite before dirty flags are cleared in the bounds path.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread tests/src/ui/Text.test.ts
*/
describe("Text - bounds-getter slot residue regression", async () => {
const canvas = document.createElement("canvas");
const engine = await WebGLEngine.create({ canvas });

@augmentcode augmentcode Bot Apr 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

@codecov

codecov Bot commented Apr 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.26%. Comparing base (a6f0504) to head (a74b231).
⚠️ Report is 1 commits behind head on dev/2.0.

Additional details and impacted files
@@             Coverage Diff             @@
##           dev/2.0    #2981      +/-   ##
===========================================
+ Coverage    78.04%   78.26%   +0.21%     
===========================================
  Files          906      906              
  Lines        99892    99892              
  Branches     10190    10193       +3     
===========================================
+ Hits         77960    78178     +218     
+ Misses       21763    21544     -219     
- Partials       169      170       +1     
Flag Coverage Δ
unittests 78.26% <100.00%> (+0.21%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/src/core/2d/text/TextRenderer.test.ts (1)

401-403: 💤 Low value

Consider adding a note about maintaining sync with source enum.

These constants duplicate internal DirtyFlag enum values from TextRenderer.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

📥 Commits

Reviewing files that changed from the base of the PR and between a6f0504 and 7e0a873.

📒 Files selected for processing (4)
  • packages/core/src/2d/text/TextRenderer.ts
  • packages/ui/src/component/advanced/Text.ts
  • tests/src/core/2d/text/TextRenderer.test.ts
  • tests/src/ui/Text.test.ts

GuoLei1990

This comment was marked as outdated.

cptbtptpbcptdtptp and others added 2 commits May 7, 2026 14:28
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GuoLei1990

This comment was marked as outdated.

Previous fix added _setDirtyFlagTrue(WorldPosition) at the end of
_updateLocalData in both TextRenderer and UI Text. That treats the
output side as the place to declare invalidation, which conflates
two concerns: dirty flags should declare staleness from input
semantics, and update methods should be pure compute units that
don't propagate flags themselves.

Root cause is on the input side: _onRootCanvasModify(ReferenceResolutionPerUnit)
declared LocalPositionBounds dirty but not WorldPosition, even though
ReferenceResolutionPerUnit affects both local layout and the world
positions derived from it. Fix the declaration where the input
semantic event lives.

TextRenderer needs no change — it has no entry point that dirties
LocalPositionBounds without also dirtying WorldPosition (all setters
use DirtyFlag.Position which includes both).

Tests rewritten from white-box (poking private _dirtyFlag, hardcoded
enum values) to public-API integration tests that drive the bug
through uiCanvas.referenceResolutionPerUnit and assert observable
vertex position changes. The new tests fail without the fix
(maxDelta = 0, positions don't update) and pass with it.

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

总结

增量审查(第三轮)。PR 经过重构:从输出端 _updateLocalData 末尾强制 WorldPosition dirty 改为输入端 _onRootCanvasModify 标记。方向正确——dirty flag 应在输入语义处声明失效,update 方法应是纯计算单元。TextRenderer 无需修改(所有输入路径均通过 DirtyFlag.Position 设置,已包含 WorldPosition)。

但当前修复漏了一个 flag:_onRootCanvasModify 只设了 LocalPositionBounds | WorldPosition(0x18),而 referenceResolutionPerUnit 变更实际影响 local layout、world position 和 world bounding volume 三个维度,应设 DirtyFlag.Position(0x19 = LocalPositionBounds | WorldPosition | WorldVolume)。

已关闭问题

  1. [P2] UI Text 回归测试缺少 afterAll(engine.destroy()) — 已在 43c16a2 修复,后在 d61472de0 重写但保留了 afterAll

问题

  • [P2] packages/ui/src/component/advanced/Text.ts:313_onRootCanvasModify 设置了 DirtyFlag.LocalPositionBounds | DirtyFlag.WorldPosition(0x18),缺少 WorldVolume(0x1)。referenceResolutionPerUnit 变更会导致 local positions 和 world positions 改变,world bounding volume 同样会变。当前 bounds getter 中 _isContainDirtyFlag(RendererUpdateFlags.WorldVolume) 为 false,_updateBounds 被跳过,返回 stale BoundingBox——可能影响 frustum culling 和 raycasting。

    修复:用 DirtyFlag.Position 替代手工组合,语义完整且更简洁:

    _onRootCanvasModify(flag: RootCanvasModifyFlags): void {
      if (flag === RootCanvasModifyFlags.ReferenceResolutionPerUnit) {
        this._setDirtyFlagTrue(DirtyFlag.Position);
      }
    }
  • [P2] PR 标题 — "mark WorldPosition dirty after slot reallocation in _updateLocalData" 描述的是旧方案(输出端修改),当前实现是在 _onRootCanvasModify 输入端标记。建议更新为 "fix(text): propagate WorldPosition dirty in _onRootCanvasModify when ReferenceResolutionPerUnit changes"。

简化建议

代码干净。上述 DirtyFlag.Position 替换同时是简化建议:用命名组合常量替代手工 OR。

自检 Checklist

  • 重复检查 — WorldVolume 缺失是本轮新发现,不在已关闭清单中;PR 标题问题同理
  • 前置检查 — 追溯 RendererUpdateFlags.WorldVolume = 0x1,确认与 DirtyFlag.Position 中的 0x1 bit 是同一 flag;bounds getter 中 _updateBounds 依赖此 bit(Text.ts:236),当前修复不会触发
  • 代码验证 — 验证了 RendererUpdateFlags.WorldVolume = 0x1(Renderer.ts:520)、DirtyFlag.Position = 0x19(Text.ts:694)、_onRootCanvasModify 当前设置 0x18(Text.ts:313),确认 0x1 bit 缺失

…erUnit change

Use DirtyFlag.Position (= LocalPositionBounds | WorldPosition | WorldVolume)
instead of the manual two-flag combination. ReferenceResolutionPerUnit
also affects world bounding volume; without the WorldVolume bit,
_updateBounds is skipped in the bounds getter and stale BoundingBox
leaks into frustum culling and raycasting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cptbtptpbcptdtptp cptbtptpbcptdtptp changed the title fix(text): mark WorldPosition dirty after slot reallocation in _updateLocalData fix(text): propagate WorldPosition dirty in _onRootCanvasModify when ReferenceResolutionPerUnit changes May 7, 2026
@cptbtptpbcptdtptp cptbtptpbcptdtptp merged commit e19b764 into galacean:dev/2.0 May 7, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants