Windowed perspective writeback in UIEntity::updateVisibility; closes #316

Clip the demote+promote passes in updateVisibility() to an AABB sized to
fov_radius instead of two full-W*H walks per entity. A prev_fov window cache
demotes last tick's promoted rect (not the current one) so a moving entity
leaves no trailing "ghost vision". On a 1000x1000 grid the Phase 5.2 benchmark's
flat ~25-36 ms/entity writeback overhead collapses to single-digit microseconds
(384x-6577x speedup on the cheap algorithms; below timing noise on the rest).

Adversarial verify caught a regression the happy-path test missed: the
documented from_bytes -> assign -> update_visibility() load/resume path left
permanent ghost-VISIBLE cells, because prev_fov only bounds engine-promoted
cells. Fixed with a one-shot perspective_full_demote_pending flag (full demote
only on the tick after an external assignment; per-turn hot path stays
windowed). Documented the engine-owned demote contract on the perspective_map
property.

- src/UIEntity.cpp/.h: windowed demote/promote, prev_fov cache, demote flag
- src/DiscreteMap.cpp/.h: demoteVisibleRect(x0, y0, x1, y1)
- tests/regression/issue_316_sparse_perspective_test.py: 7-section regression
  (equivalence matrix, radius-0, moving disjoint, trailing-edge, grid resize,
  load/resume assignment, AABB margin lock)
- docs regenerated for the perspective_map docstring change

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv
This commit is contained in:
John McCardle 2026-06-21 09:40:05 -04:00
commit 265425321c
9 changed files with 598 additions and 23 deletions

View file

@ -108,7 +108,7 @@
<body>
<div class="container">
<h1>McRogueFace API Reference</h1>
<p><em>Generated on 2026-06-21 06:42:31</em></p>
<p><em>Generated on 2026-06-21 09:39:04</em></p>
<p><em>This documentation was dynamically generated from the compiled module.</em></p>
<div class="toc">
@ -1979,7 +1979,7 @@ Attributes:
<li><span class='property-name'>move_speed</span>: Animation duration for behavior movement in seconds (float). 0 = instant. Default: 0.15.</li>
<li><span class='property-name'>name</span>: Entity name for lookup (str).</li>
<li><span class='property-name'>opacity</span>: Render opacity (float). 0.0 = fully transparent, 1.0 = fully opaque.</li>
<li><span class='property-name'>perspective_map</span>: Per-entity FOV memory (DiscreteMap). 3-state values per cell: 0=unknown, 1=discovered, 2=visible. Lazy-allocated on first access once entity has a grid; returns None otherwise. The returned DiscreteMap is a live reference. Assigning a DiscreteMap replaces the entity&#x27;s memory; size must match the grid or ValueError is raised. Assign None to clear.</li>
<li><span class='property-name'>perspective_map</span>: Per-entity FOV memory (DiscreteMap). 3-state values per cell: 0=unknown, 1=discovered, 2=visible. Lazy-allocated on first access once entity has a grid; returns None otherwise. The returned DiscreteMap is a live reference. Assigning a DiscreteMap replaces the entity&#x27;s memory (e.g. loading a saved perspective via from_bytes); size must match the grid or ValueError is raised, and the next updateVisibility() demotes any loaded visible cells to discovered before recomputing FOV. Assign None to clear. Note: updateVisibility() only auto-demotes visible cells the engine itself promoted; if you write 2 (visible) into the live map by hand at a cell outside the entity&#x27;s current FOV, it will not be auto-demoted -- use 1 (discovered) to reveal remembered cells, or assign a whole map to set arbitrary state.</li>
<li><span class='property-name'>pos</span>: Pixel position relative to grid (Vector). Computed as draw_pos * tile_size. Requires entity to be attached to a grid.</li>
<li><span class='property-name'>shader</span>: GPU shader for visual effects (Shader or None). Set to None to disable shader rendering.</li>
<li><span class='property-name'>sight_radius</span>: FOV radius for TARGET trigger (int). Default: 10.</li>