[Refactoring] Let perspective-enabled grids take the render early-out (track FOV/perspective_map changes in content-generation) #352

Open
opened 2026-07-11 04:36:06 +00:00 by john · 1 comment
Owner

Follow-up to #351.

#351 adds a clean-state render early-out to UIGridView::render() (skip the full re-rasterization when the grid's content-generation and the camera parameters are unchanged). To keep that first cut provably correct, perspective-enabled grids are carved out — they always re-render — because the perspective overlay depends on the watched entity's perspective_map, whose values change during UIEntity::updateVisibility() (promote/demote) without going through any tracked mutation choke point.

Why this matters

grid.perspective is not a debug overlay — it's the core feature that binds a grid's rendered visibility to a player entity's FOV, which is the single most common "hero grid" configuration. So the #351 carve-out means the one grid a game most wants to be cheap is the one that never benefits, while secondary grids (minimaps, static UI grids, additional views of the same data) do get the early-out. That's a fine starting split, but the hero grid should eventually benefit too — e.g. a player standing still has a static FOV and should not force a full grid re-raster every frame.

Proposed systemic fix

Tie perspective/FOV changes into the same content-generation signal #351 introduces:

  • Bump the grid content-generation (or a dedicated perspective_generation) when perspective_map actually changes — i.e. inside UIEntity::updateVisibility() where cells are promoted/demoted (and on perspective_entity reassignment / enable-disable).
  • Then UIGridView::render() can drop the "always redraw when perspective_enabled" carve-out and early-out whenever the FOV genuinely didn't change frame-to-frame.

Acceptance

  • A perspective grid with a stationary player and no world changes does not re-rasterize its RenderTexture every frame.
  • Any FOV change (player moves, a door opens changing transparency, perspective entity swapped) still updates the overlay on the next frame — regression test both.

References

  • Parent: #351 (UIGridView::render() early-out)
  • src/UIEntity.cpp updateVisibility() (perspective_map promote/demote, #294/#316), src/UIGridView.cpp:237-279 (perspective overlay)
Follow-up to #351. #351 adds a clean-state render early-out to `UIGridView::render()` (skip the full re-rasterization when the grid's content-generation and the camera parameters are unchanged). To keep that first cut provably correct, **perspective-enabled grids are carved out — they always re-render** — because the perspective overlay depends on the watched entity's `perspective_map`, whose values change during `UIEntity::updateVisibility()` (promote/demote) **without** going through any tracked mutation choke point. ## Why this matters `grid.perspective` is not a debug overlay — it's the core feature that binds a grid's rendered visibility to a player entity's FOV, which is the single most common "hero grid" configuration. So the #351 carve-out means the one grid a game most wants to be cheap is the one that never benefits, while secondary grids (minimaps, static UI grids, additional views of the same data) do get the early-out. That's a fine starting split, but the hero grid should eventually benefit too — e.g. a player standing still has a static FOV and should not force a full grid re-raster every frame. ## Proposed systemic fix Tie perspective/FOV changes into the same content-generation signal #351 introduces: - Bump the grid content-generation (or a dedicated `perspective_generation`) when `perspective_map` actually changes — i.e. inside `UIEntity::updateVisibility()` where cells are promoted/demoted (and on `perspective_entity` reassignment / enable-disable). - Then `UIGridView::render()` can drop the "always redraw when perspective_enabled" carve-out and early-out whenever the FOV genuinely didn't change frame-to-frame. ## Acceptance - A perspective grid with a stationary player and no world changes does not re-rasterize its RenderTexture every frame. - Any FOV change (player moves, a door opens changing transparency, perspective entity swapped) still updates the overlay on the next frame — regression test both. ## References - Parent: #351 (`UIGridView::render()` early-out) - `src/UIEntity.cpp` `updateVisibility()` (perspective_map promote/demote, #294/#316), `src/UIGridView.cpp:237-279` (perspective overlay)
Author
Owner

The children half of this carve-out is now cheap to remove (2026-07-13)

#364 moved grid children from GridData to UIGridView, so a child's parent is now the view itself. Two consequences for this issue:

  1. The view has a precise per-child dirty signal. Any child mutation (move, recolor, add, remove) reaches the view as render_dirty via the ordinary parent-chain push, and UIGridView::render now consults render_dirty in can_skip and calls clearDirty() after rasterizing.

  2. So the && (!children || children->empty()) carve-out in can_skip is now redundant and can be deleted -- restoring the #351 early-out for grids that have children. Today any grid with even one child pays a permanent full re-render every frame, which is the cost noted in the #364 thread. Left in place deliberately (conservative = always correct) rather than removed on a bugfix branch.

Removing it needs one thing verified first: that every mutation path on a child drawable actually calls markContentDirty()/markCompositeDirty(). If any setter is missing it, dropping the carve-out shows a stale child -- the dangerous direction. Note that #368 (the was_dirty latch in markContentDirty that silently swallows every invalidation after the first) must be fixed before this is safe, or the propagation this relies on is not trustworthy in the first place. #368 blocks the children half of this issue.

The perspective/FOV half of the carve-out is untouched and still needs its own tracking.

## The children half of this carve-out is now cheap to remove (2026-07-13) #364 moved grid children from `GridData` to `UIGridView`, so a child's `parent` is now the view itself. Two consequences for this issue: 1. **The view has a precise per-child dirty signal.** Any child mutation (move, recolor, add, remove) reaches the view as `render_dirty` via the ordinary parent-chain push, and `UIGridView::render` now consults `render_dirty` in `can_skip` and calls `clearDirty()` after rasterizing. 2. **So the `&& (!children || children->empty())` carve-out in `can_skip` is now redundant** and can be deleted -- restoring the #351 early-out for grids that *have* children. Today any grid with even one child pays a permanent full re-render every frame, which is the cost noted in the #364 thread. Left in place deliberately (conservative = always correct) rather than removed on a bugfix branch. Removing it needs one thing verified first: that **every** mutation path on a child drawable actually calls `markContentDirty()`/`markCompositeDirty()`. If any setter is missing it, dropping the carve-out shows a stale child -- the dangerous direction. Note that **#368** (the `was_dirty` latch in `markContentDirty` that silently swallows every invalidation after the first) must be fixed before this is safe, or the propagation this relies on is not trustworthy in the first place. **#368 blocks the children half of this issue.** The perspective/FOV half of the carve-out is untouched and still needs its own tracking.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
john/McRogueFace#352
No description provided.