feat(overhaul): port S5 particle cell visibility

Publish the exact completed walk landscape set as the typed retained particle-view product. Apply retail CLandCell membership versus constant-true non-null CEnvCell eligibility while preserving the x87 distance and AP-116 behavior.

Delete the null-root terrain visibility reconstruction and the dead drawable-cell point-light feedback chain. Retire AP-117, correct AP-85 and AD-21, and keep directional shadows, building degrade, AP-232, probes, RHI, shaders, and DAT outside this chunk.

Automated return: Release 0 warnings/0 errors; Core VFX 111/111; App particle/frame/renderer/terrain 146/146; warmed production allocation 1/1 at 0 B. First official hermetic artifact s5-c1-hermetic-20260904 is preserved at 16759/16760 with only the stale 162-row assertion; after the bounded 161 correction, exact pin 1/1 and fresh s5-c1-hermetic-corrected-20260904 16760/16760. InstalledDat 385 pass, 10 documented failures, 1 documented skip, no new identity.

Mutation 1: making EnvCell eligibility set-dependent first failed ParticleSystemTests.cs:553 Assert.True, expected true actual false. Mutation 2: making outdoor eligibility constant true first failed ParticleSystemTests.cs:559 Assert.False, expected false actual true.

Mutation 3: feeding the diagnostic union first failed WorldSceneRendererTests.cs:279 HashSet equality, expected [16842755], actual [16843008, 16842755]. Mutation 4: restoring CollectVisibleCells first failed TerrainParticleCellVisibilityTests.cs:37 and named TerrainModernRenderer.cs.

Mutation 5: restoring ObserveDrawableCells first failed TerrainParticleCellVisibilityTests.cs:37 and named WorldRenderFrameBuilder.cs. Mutation 6: changing the inclusive boundary from <= to < first failed ParticleSystemTests.cs:519 Assert.True, expected true actual false.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-04 16:27:46 +02:00
parent efb0756190
commit 94a6b5ef39
18 changed files with 352 additions and 269 deletions

View file

@ -1159,10 +1159,14 @@ therefore remains an OH6 portal-fan approximation. The current ordinary-part
classifier is a materially aligned candidate and OH4 must protect it while classifier is a materially aligned candidate and OH4 must protect it while
removing only contradictory view/admission residue. removing only contradictory view/admission residue.
The walk's visited EnvCells and landscape land cells still form different The walk's visited EnvCells and landscape land cells form different typed
typed inputs. Outdoor particle, light, and shadow consumers must borrow the inputs. Campaign OVERHAUL S5-c1 made particle update borrow the exact completed
landscape half rather than infer visibility from aggregate owner ids; OH8 owns landscape half for `CLandCell::IsInView`, while EnvCell owners use retail's
that final consumer cutover. constant-true `CEnvCell::IsInView` virtual. Point lights remain a separate
resident-registry product and consume neither walk half. Null-root safety
frames publish no cell-visibility answer; the former terrain frustum/AABB
reconstruction and last-frame point-light feedback seam are deleted.
Directional shadows remain the next typed landscape consumer (S5-c2).
WorldBuilder contributes DAT decoding, mesh preparation, residency, and Vulkan WorldBuilder contributes DAT decoding, mesh preparation, residency, and Vulkan
batch mechanics only; it makes no visibility or ordering decision. Forced merge batch mechanics only; it makes no visibility or ordering decision. Forced merge

File diff suppressed because one or more lines are too long

View file

@ -680,8 +680,8 @@ Update immediately when a slice changes state. Chat is not the ledger.
| S4-c2 attempts 15 | **ATTEMPT 1/5 CONSUMED; ATTEMPT 2/5 PASSED AND LANDED 2026-09-04** — reviewed implementation `daaeab0ba`; campaign stack `89f1e2676``9ccb61a8e``0aa166aa0``252886e84`; packet §17 | G3/G4 UNPASSED | Sequential retail/evidence then production/gate-honesty PASS. Fresh: Release 0W/0E; shader 32/32; affected 37/37; real allocation 2/2 at 0 B; production 239/239. Official no-retry hermetic 16,734/1: sole unchanged global `Console.Out` capture race, exact theory 2/2 isolated. InstalledDat 255/10 documented/1. Validation routes `selfgate-20260904-115818-s4c2-landed-route` and `selfgate-20260904-120025-s4c2-landed-g3b` PASS provisional. First soak failed on known #461/#462; retry and S4-c1 control both 9/9 graceful; stationary averages CPU p95 0.01 ms, GPU p95 +0.16 ms, alloc p50 +1.2 KiB, update p95 0.12 ms. Three attempts remain unused. | | S4-c2 attempts 15 | **ATTEMPT 1/5 CONSUMED; ATTEMPT 2/5 PASSED AND LANDED 2026-09-04** — reviewed implementation `daaeab0ba`; campaign stack `89f1e2676``9ccb61a8e``0aa166aa0``252886e84`; packet §17 | G3/G4 UNPASSED | Sequential retail/evidence then production/gate-honesty PASS. Fresh: Release 0W/0E; shader 32/32; affected 37/37; real allocation 2/2 at 0 B; production 239/239. Official no-retry hermetic 16,734/1: sole unchanged global `Console.Out` capture race, exact theory 2/2 isolated. InstalledDat 255/10 documented/1. Validation routes `selfgate-20260904-115818-s4c2-landed-route` and `selfgate-20260904-120025-s4c2-landed-g3b` PASS provisional. First soak failed on known #461/#462; retry and S4-c1 control both 9/9 graceful; stationary averages CPU p95 0.01 ms, GPU p95 +0.16 ms, alloc p50 +1.2 KiB, update p95 0.12 ms. Three attempts remain unused. |
| S4-c3a | **LANDED 2026-09-04 after the owner-authorized §21 evidence exception.** Reviewed scratch stack `44e2bc227b``b6bf6c131``eea5793d2``14397b14c``359061b82``255194e90``9cfddf301`; campaign cherry-picks `a86ec73ec``8e0c6fb14``01674bcc7``8bd75ba31``b6b015604``316193043``67c76026e`. The earlier stop remains in history at `06b986622`. | G3/G4 UNPASSED | Final retail lens PASS; production behavior/lifecycle/allocation/scope clean. The narrow independent gate-honesty re-review PASS reproduced the exact first failure (AP-table-boundary `Assert.True`, `rowIndex=-1`, before `Assert.Single`), restored AP-241 byte-identically, and passed the pin 1/1. Fresh campaign: Release 0W/0E; focused class lane 96/96; no-surface 1/1; AP pin 1/1; real allocation 2/2 at 0 B; shader 32/32; `git diff --check` clean. No client launched. NEXT: write c3b's deletion-only contract from the landed code; G3 stays locked until c3b lands. | | S4-c3a | **LANDED 2026-09-04 after the owner-authorized §21 evidence exception.** Reviewed scratch stack `44e2bc227b``b6bf6c131``eea5793d2``14397b14c``359061b82``255194e90``9cfddf301`; campaign cherry-picks `a86ec73ec``8e0c6fb14``01674bcc7``8bd75ba31``b6b015604``316193043``67c76026e`. The earlier stop remains in history at `06b986622`. | G3/G4 UNPASSED | Final retail lens PASS; production behavior/lifecycle/allocation/scope clean. The narrow independent gate-honesty re-review PASS reproduced the exact first failure (AP-table-boundary `Assert.True`, `rowIndex=-1`, before `Assert.Single`), restored AP-241 byte-identically, and passed the pin 1/1. Fresh campaign: Release 0W/0E; focused class lane 96/96; no-surface 1/1; AP pin 1/1; real allocation 2/2 at 0 B; shader 32/32; `git diff --check` clean. No client launched. NEXT: write c3b's deletion-only contract from the landed code; G3 stays locked until c3b lands. |
| S4-c3b | **LANDED 2026-09-04** — implementation `26e97ba41`, provenance contract `5110bf676`, packet-only correction `3f2f00c9f`; packet §22§25. Dead classic-group `LocalSortCenters`/`CachedBatch.LocalSortCenter` storage and the alpha camera-parameter/digest chain are deleted. Live per-cell/particle CYpt keys, opaque `SortDistance`, building/private/portal distances, two FIFO lists, all state/barriers, and AP/AD rows remain. | **G3 LEAD SELF-GATE PROVISIONAL PASS; owner acceptance/owner-only rows pending. G4 UNPASSED.** | Retail/deletion lens PASS. Production lens found no code defect and one artifact-provenance omission; packet-only fix round 1 passed its narrow re-review. Fresh campaign: Release 0W/0E; App 132/132; Core 29/29; allocation 2/2 at 0 B; shader 32/32. G3: route 1 13/13, route 2 4/4, route 3 retry 3/3, all exit 0/graceful. The first route-3 attempt stopped before Nanto on registered #462. Exact PNG paths: packet §25. | | S4-c3b | **LANDED 2026-09-04** — implementation `26e97ba41`, provenance contract `5110bf676`, packet-only correction `3f2f00c9f`; packet §22§25. Dead classic-group `LocalSortCenters`/`CachedBatch.LocalSortCenter` storage and the alpha camera-parameter/digest chain are deleted. Live per-cell/particle CYpt keys, opaque `SortDistance`, building/private/portal distances, two FIFO lists, all state/barriers, and AP/AD rows remain. | **G3 LEAD SELF-GATE PROVISIONAL PASS; owner acceptance/owner-only rows pending. G4 UNPASSED.** | Retail/deletion lens PASS. Production lens found no code defect and one artifact-provenance omission; packet-only fix round 1 passed its narrow re-review. Fresh campaign: Release 0W/0E; App 132/132; Core 29/29; allocation 2/2 at 0 B; shader 32/32. G3: route 1 13/13, route 2 4/4, route 3 retry 3/3, all exit 0/graceful. The first route-3 attempt stopped before Nanto on registered #462. Exact PNG paths: packet §25. |
| S5-c1 | **STOPPED 2026-09-04 after fix round 2; scratch stack not landed.** Packet §9. Implementation `9aadc917a`; comment-only fix rounds `74a2e4c48` and `a28384d80`. Retail lens PASS; production lens found no code/test defect but FAILed on two documentation/evidence items: null-root really completes an empty `hasCompletedView=true` transaction while architecture/AD-21 say no answer, and the dirty-base official artifacts are not bound to the reviewed implementation source. | **MANDATORY THIRD-ROUND STOP; G4 UNPASSED.** | Reproduced scratch evidence: Release 0W/0E; Core 111/111; App 146/146; allocation 1/1 at 0 B; corrected hermetic 16,760/16,760; InstalledDat 385/10 documented/1; three 30-entry manifests verify. No client/self-gate and no campaign cherry-pick. Explicit owner direction is required for any documentation/evidence-only exception; history remains intact. | | S5-c1 | **IMPLEMENTED 2026-09-04; sequential review pending.** Packet `s5-consumers-material-closeout-packet.md` §§19. The exact walk landscape set is now the typed retained particle product; land cells require membership while non-null EnvCells retain their constant-true virtual. AP-117's null-root reconstruction and the dead point-light feedback seam are deleted; AP-117 is retired and AP-85/AD-21 are corrected. | G4 UNPASSED | Focused Core 111/111, App 146/146, real warmed allocation 1/1 at 0 B, Release 0W/0E. The first official artifact is preserved at 16,759/16,760 with only the stale AP-row-count pin; §8 corrected 162→161 and the exact pin passes 1/1. Fresh official hermetic 16,760/16,760; InstalledDat 385/10 documented/1 with no new identity. Directional shadows, building degrade, AP-232, probes, and closeout remain later chunks. |
| S5 | STOPPED at c1 after the two allowed fix rounds; c2c5 and closeout remain undispatched | G4 UNPASSED | Packet §9 is authoritative. Never merge main before G4. | | S5 | IN FLIGHT — c1 implemented/review pending; c2c5 and closeout remain | G4 | Packet §7 is the bounded decomposition; never merge main before G4. |
--- ---

View file

@ -307,6 +307,7 @@ weather/sky post-pass
| Outside cells reached by the static bbox transit remain in the CELLARRAY. | Dirty `CellTransit` removed an outdoor prune globally, including collision callers. | Correct retail fact, wrong ownership boundary risk. OH3 must port the exact bbox route without mutating unrelated sphere/collision routes. | | Outside cells reached by the static bbox transit remain in the CELLARRAY. | Dirty `CellTransit` removed an outdoor prune globally, including collision callers. | Correct retail fact, wrong ownership boundary risk. OH3 must port the exact bbox route without mutating unrelated sphere/collision routes. |
| Dynamics and attached children inherit the root's exact ordered CELLARRAY and publish exact per-part entries. | `WalkProductionWorldData` buckets whole `RenderProjectionRecord`s by `ShadowObjectRegistry.GetOwnerCells`, resolving equipped children by a parent callback. The draw classifier later tries to restore per-part stamps. | Membership is reconstructed from the collision registry, loses the canonical `CShadowPart` transaction/order, and creates a second model. | | Dynamics and attached children inherit the root's exact ordered CELLARRAY and publish exact per-part entries. | `WalkProductionWorldData` buckets whole `RenderProjectionRecord`s by `ShadowObjectRegistry.GetOwnerCells`, resolving equipped children by a parent callback. The draw classifier later tries to restore per-part stamps. | Membership is reconstructed from the collision registry, loses the canonical `CShadowPart` transaction/order, and creates a second model. |
| `LScape::draw` runs once and visibility unions all views. | `RetailFrameWalk.DrawLandscape` correctly installs one view set and walks blocks/cells once. | Keep. | | `LScape::draw` runs once and visibility unions all views. | `RetailFrameWalk.DrawLandscape` correctly installs one view set and walks blocks/cells once. | Keep. |
| `CLandCell::IsInView` consumes the previous completed landscape stamp; `CEnvCell::IsInView` is constant true. | `RetailPViewFrameResult.VisibleLandscapeCells` publishes the walk's exact landscape half; `ParticleSystem.ApplyRetailView` branches by cell-id family. Frames without a completed walk product publish no outdoor answer. | **FIXED by S5-c1 (2026-09-04).** The competing null-root terrain reconstruction is deleted; point lights remain resident-registry based. |
| Per landscape cell: terrain -> building -> ordinary objects. | `RetailFrameWalk.DrawLandscape` now emits one `OnLandCellTurn` per admitted land cell BEFORE that cell's own building/object turn (`RenderDeviceD3D::DrawBlock`'s real per-cell interleave), and `WalkFrameDriver.Replay` submits each cell's terrain through a deferred, order-preserving batch rather than one whole pre-stage draw. | FIXED by S3 chunk 3 (commit 671eb3ad4 + fix round 1) — the whole-stage `TerrainSlice(0)`/`DrawTerrainSlice` this row described is deleted outright; row retired. | | Per landscape cell: terrain -> building -> ordinary objects. | `RetailFrameWalk.DrawLandscape` now emits one `OnLandCellTurn` per admitted land cell BEFORE that cell's own building/object turn (`RenderDeviceD3D::DrawBlock`'s real per-cell interleave), and `WalkFrameDriver.Replay` submits each cell's terrain through a deferred, order-preserving batch rather than one whole pre-stage draw. | FIXED by S3 chunk 3 (commit 671eb3ad4 + fix round 1) — the whole-stage `TerrainSlice(0)`/`DrawTerrainSlice` this row described is deleted outright; row retired. |
| Cell object draw consumes the cell's exact stable far-to-near `shadow_part_list`. | The driver materializes per-cell record buckets, classifies/batches them, and uses a scope-level ordered stream plus separate alpha collection. | OH3/OH7 must replace reconstructed membership/order with exact part entries and exact per-cell alpha barriers. | | Cell object draw consumes the cell's exact stable far-to-near `shadow_part_list`. | The driver materializes per-cell record buckets, classifies/batches them, and uses a scope-level ordered stream plus separate alpha collection. | OH3/OH7 must replace reconstructed membership/order with exact part entries and exact per-cell alpha barriers. |
| Building is alpha drain -> portal pass -> shell, then cell objects. | `RetailFrameWalk.DrawBuilding` and the driver's building events model this order. | Mechanism broadly matches, but its correctness depends on the sibling OH1 built-mesh/view and depth-lifecycle findings. | | Building is alpha drain -> portal pass -> shell, then cell objects. | `RetailFrameWalk.DrawBuilding` and the driver's building events model this order. | Mechanism broadly matches, but its correctness depends on the sibling OH1 built-mesh/view and depth-lifecycle findings. |

View file

@ -1,6 +1,6 @@
# Campaign OVERHAUL v2 — S5 consumers, material, and closeout packet # Campaign OVERHAUL v2 — S5 consumers, material, and closeout packet
**Status:** S5-c1 STOPPED 2026-09-04 after fix round 2; not landed. **Status:** S5-c1 IMPLEMENTED 2026-09-04; sequential review pending.
**Branch:** `claude/campaign-w-retail-frame-walk`. **Branch:** `claude/campaign-w-retail-frame-walk`.
**Gate:** G4 remains unpassed. Nothing merges to `main` before G4. **Gate:** G4 remains unpassed. Nothing merges to `main` before G4.
@ -275,45 +275,56 @@ failed test first, then a fresh official hermetic lane under a new artifact
name and the required InstalledDat lane. Preserve the original failed artifact name and the required InstalledDat lane. Preserve the original failed artifact
and record both identities in the implementer return. and record both identities in the implementer return.
## 9. S5-c1 mandatory stop after fix round 2 — 2026-09-04 ## 9. S5-c1 implementation result — 2026-09-04
The reviewed scratch stack is intentionally **not landed**: The bounded implementation publishes `WalkFrameDriver.VisitedLandscapeCellIds`
as the typed landscape product, retains it transactionally in the particle
visibility owner, and applies the retail cell-family virtual: membership for
land cells, constant true for non-null EnvCells. The null-root terrain
reconstruction and the dead point-light drawable-cell feedback chain are
deleted. AP-117 is retired; AP-85 and AD-21 now describe the surviving code.
- gate-correction contract `2bb565b0b`; The real warmed allocation pin initially exposed that enumerating the retained
- implementation `9aadc917a`; `SortedSet<int>` of world-simulation handles allocated on every `Apply`. The
- retail fix-round-1 contract/implementation `afaebcad6` / `74a2e4c48`; owner is now a sorted `List<int>` maintained at lifecycle mutation time, so the
- retail fix-round-2 contract/implementation `ed8200664` / `a28384d80`. production Apply loop preserves stable handle order and measures 0 B without
changing queue, router, draw, or particle tick behavior. This is an
implementation-mechanism correction, not a retail behavior deviation.
The retail lens ultimately passed. It verified the paired executable/PDB bytes, Focused returns before the official lanes: Core VFX 111/111, App particle /
the exact land-cell versus EnvCell virtual behavior, previous-completed-frame frame / renderer / terrain 146/146, and the real warmed production allocation
timing, x87/AP-116 edges, typed landscape handoff, and unchanged particle pin 1/1 at 0 B. Release built with 0 warnings / 0 errors. The first official
draw/queue/tick behavior. The production lens found no production-code or test hermetic artifact, `artifacts/s5-c1-hermetic-20260904`, is deliberately
defect and reproduced Release 0 warnings / 0 errors, Core VFX 111/111, App preserved at 16,759/16,760: its sole failure was the stale 162-row test literal
focused 146/146, allocation 1/1 at 0 B, AP-116 3/3, all six non-vacuous after AP-117's retirement. Section 8 authorized only that coupled test
mutations, all source guards, and all three 30-entry artifact manifests with correction; the exact failed pin then passed 1/1 with the truthful 161-row
zero mismatch. Official artifacts remain in the scratch worktree: header and physical count. The one-shot corrected official lane at
`artifacts/s5-c1-hermetic-corrected-20260904` then passed 16,760/16,760,
0 skipped, across 14 assemblies. The inclusive InstalledDat lane at
`artifacts/s5-c1-installed-20260904` produced the documented global set:
385 passed, 10 failed, 1 skipped. The failures are `TowerAscent`, the two
#383 layout sweeps, #458 `Oh_doorway_still_first_frame_diff`, and the six
`AlphaFlushCounts_*` transcript Facts; the skip is
`Sweep_the_lookin_gate_decodes_against_the_street_fixture`. No new failure or
skip identity appeared.
- `artifacts/s5-c1-hermetic-20260904`: 16,759 passed / 1 stale-row failure; Independent mutation ledger (each mutation restored exactly before the next):
- `artifacts/s5-c1-hermetic-corrected-20260904`: 16,760/16,760;
- `artifacts/s5-c1-installed-20260904`: 385 passed / 10 documented failed /
1 documented skipped, with no new identity.
The final production lens nevertheless failed on two correction-worthy 1. EnvCell eligibility made set-dependent: `ApplyRetailView_UsesLandscapeMembershipButEnvCellConstantVirtual`
documentation/evidence findings after both allowed fix rounds: first failed `Assert.True`, expected true / actual false, at
`ParticleSystemTests.cs:553`.
1. `acdream-architecture.md` and AD-21 say a null-root frame publishes no 2. Outdoor eligibility made constant true: the same test first failed
cell-visibility answer. Production actually completes `Assert.False`, expected false / actual true, at `ParticleSystemTests.cs:559`.
`hasCompletedView=true` with an empty landscape set; only login/portal carry 3. Renderer fed the diagnostic union: `PViewWorld_PublishesOnlyLandscapeCellsToParticleVisibility`
false. Scratch packet §11.1 states this correctly, so the implementation's first failed the HashSet equality at `WorldSceneRendererTests.cs:279`,
claim that architecture/register prose is truthful is false. expected `[16842755]`, actual `[16843008, 16842755]`.
2. The packet reports the official lanes but does not reconcile their recorded 4. A production `CollectVisibleCells` symbol was restored:
`WorktreeDirty=true` bases (`00f134f9e` for the first lane and `2bb565b0b` `Production_source_has_no_reconstructed_particle_visibility_feedback`
for the corrected/InstalledDat lanes) to implementation commit `9aadc917a`, first failed at `TerrainParticleCellVisibilityTests.cs:37`, naming
nor state that later commits are prose/comments only. The manifests prove `TerrainModernRenderer.cs`.
their 30 recorded evidence files, not the dirty source content. 5. A production `ObserveDrawableCells` symbol was restored: the same source
guard first failed at line 37, naming `WorldRenderFrameBuilder.cs`.
Correcting either finding would be fix round 3. The campaign rule therefore 6. The inclusive boundary was changed from `<=` to `<`:
requires this stop: do not cherry-pick the scratch stack, do not run a graphical `ApplyRetailView_UsesOwnerVisibilityAndInclusiveAuthoredDistance` first
gate on it, and do not dispatch S5-c2. Resumption requires explicit owner failed `Assert.True`, expected true / actual false, at
direction on whether to authorize an evidence/documentation-only exception; `ParticleSystemTests.cs:519`.
history must remain intact either way. G3 stays provisional and G4 unpassed.

View file

@ -23,6 +23,7 @@ internal sealed class RetailPViewRenderer
// call. Every walk consumer reads it synchronously in this frame. // call. Every walk consumer reads it synchronously in this frame.
private readonly HashSet<uint> _drawableCellsScratch = new(); private readonly HashSet<uint> _drawableCellsScratch = new();
private readonly HashSet<uint> _visibleCellsScratch = new(); private readonly HashSet<uint> _visibleCellsScratch = new();
private readonly HashSet<uint> _visibleLandscapeCellsScratch = new();
// FW3 visual-gate fix: the interior root's dynamics phase, invoked by // FW3 visual-gate fix: the interior root's dynamics phase, invoked by
// the driver's clearInteriorDepth closure at the walk's pre-clear // the driver's clearInteriorDepth closure at the walk's pre-clear
@ -294,6 +295,8 @@ internal sealed class RetailPViewRenderer
_drawableCellsScratch.Clear(); _drawableCellsScratch.Clear();
_drawableCellsScratch.UnionWith(walkDriver.VisitedCells); _drawableCellsScratch.UnionWith(walkDriver.VisitedCells);
walkDriver.CopyVisibleCellsTo(_visibleCellsScratch); walkDriver.CopyVisibleCellsTo(_visibleCellsScratch);
_visibleLandscapeCellsScratch.Clear();
_visibleLandscapeCellsScratch.UnionWith(walkDriver.VisitedLandscapeCellIds);
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled) if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled)
{ {
@ -437,6 +440,7 @@ internal sealed class RetailPViewRenderer
clipAssembly, clipAssembly,
drawableCells, drawableCells,
_visibleCellsScratch, _visibleCellsScratch,
_visibleLandscapeCellsScratch,
counts, counts,
sourceCounts, sourceCounts,
diagnosticPartition: null); diagnosticPartition: null);
@ -865,10 +869,17 @@ public sealed class RetailPViewFrameResult
public ClipFrameAssembly ClipAssembly { get; private set; } = null!; public ClipFrameAssembly ClipAssembly { get; private set; } = null!;
public HashSet<uint> DrawableCells { get; private set; } = null!; public HashSet<uint> DrawableCells { get; private set; } = null!;
/// <summary>
/// The production retail landscape walk's exact outdoor land-cell
/// <c>in_view</c> set. Particle consumers borrow this typed half rather
/// than the diagnostic union.
/// </summary>
public HashSet<uint> VisibleLandscapeCells { get; private set; } = null!;
/// <summary> /// <summary>
/// The production retail walk's exact visited-cell set. This is the one /// The production retail walk's exact visited-cell set. This is the one
/// visibility answer consumed by EnvCell preparation, particles, lights, /// diagnostic visibility union. Typed consumers must use
/// and directional-shadow filtering. /// <see cref="DrawableCells"/> or <see cref="VisibleLandscapeCells"/>.
/// </summary> /// </summary>
public HashSet<uint> VisibleCells { get; private set; } = null!; public HashSet<uint> VisibleCells { get; private set; } = null!;
@ -881,6 +892,7 @@ public sealed class RetailPViewFrameResult
ClipFrameAssembly clipAssembly, ClipFrameAssembly clipAssembly,
HashSet<uint> drawableCells, HashSet<uint> drawableCells,
HashSet<uint> visibleCells, HashSet<uint> visibleCells,
HashSet<uint> visibleLandscapeCells,
RenderFrameDiagnosticCounts diagnosticCounts, RenderFrameDiagnosticCounts diagnosticCounts,
RenderProjectionCounts sourceCounts, RenderProjectionCounts sourceCounts,
InteriorEntityPartition.Result? diagnosticPartition) InteriorEntityPartition.Result? diagnosticPartition)
@ -888,6 +900,7 @@ public sealed class RetailPViewFrameResult
ClipAssembly = clipAssembly; ClipAssembly = clipAssembly;
DrawableCells = drawableCells; DrawableCells = drawableCells;
VisibleCells = visibleCells; VisibleCells = visibleCells;
VisibleLandscapeCells = visibleLandscapeCells;
DiagnosticCounts = diagnosticCounts; DiagnosticCounts = diagnosticCounts;
SourceCounts = sourceCounts; SourceCounts = sourceCounts;
DiagnosticPartition = diagnosticPartition; DiagnosticPartition = diagnosticPartition;

View file

@ -67,7 +67,6 @@ public sealed partial class TerrainModernRenderer : IDisposable
// Reusable per-frame buffers. // Reusable per-frame buffers.
private readonly List<int> _visibleSlots = new(); private readonly List<int> _visibleSlots = new();
private readonly HashSet<uint> _visibleCellIds = new();
private readonly HashSet<uint> _walkVisibleLandblocks = new(); private readonly HashSet<uint> _walkVisibleLandblocks = new();
private DrawElementsIndirectCommand[] _deicScratch = Array.Empty<DrawElementsIndirectCommand>(); private DrawElementsIndirectCommand[] _deicScratch = Array.Empty<DrawElementsIndirectCommand>();
@ -108,15 +107,6 @@ public sealed partial class TerrainModernRenderer : IDisposable
/// cref="DrawLandCells"/> batches (indirect draws) submitted THIS FRAME.</summary> /// cref="DrawLandCells"/> batches (indirect draws) submitted THIS FRAME.</summary>
internal int WalkDrawCount => _walkDrawsThisFrame; internal int WalkDrawCount => _walkDrawsThisFrame;
/// <summary>
/// Outdoor landcells admitted by the current landscape view. The set is
/// accumulated across doorway landscape slices and consumed after the
/// completed render frame by particle visibility.
/// </summary>
internal HashSet<uint> VisibleCellIds => _visibleCellIds;
public void BeginVisibilityFrame() => _visibleCellIds.Clear();
/// <summary> /// <summary>
/// Resets the per-GPU-fenced-frame-slot draw state. A retail outside view /// Resets the per-GPU-fenced-frame-slot draw state. A retail outside view
/// may draw terrain more than once in a frame. /// may draw terrain more than once in a frame.
@ -249,16 +239,15 @@ public sealed partial class TerrainModernRenderer : IDisposable
// Campaign FW4: RetailFrameWalk is the sole visibility authority. // Campaign FW4: RetailFrameWalk is the sole visibility authority.
// Terrain remains one full-landblock MDI draw, but only landblocks // Terrain remains one full-landblock MDI draw, but only landblocks
// containing a walk-admitted landcell participate, and the published // containing a walk-admitted landcell participate. The walk publishes
// landcell set is the walk's exact in_view set rather than a second // its exact in_view set directly to typed consumers; this draw path
// frustum-derived approximation. // does not reconstruct or retain a second visibility answer.
_walkVisibleLandblocks.Clear(); _walkVisibleLandblocks.Clear();
if (inViewLandcells is not null) if (inViewLandcells is not null)
{ {
foreach (uint cellId in inViewLandcells) foreach (uint cellId in inViewLandcells)
{ {
_walkVisibleLandblocks.Add(cellId & 0xFFFF0000u); _walkVisibleLandblocks.Add(cellId & 0xFFFF0000u);
_visibleCellIds.Add(cellId);
} }
} }
@ -279,16 +268,6 @@ public sealed partial class TerrainModernRenderer : IDisposable
continue; continue;
} }
_visibleSlots.Add(slot); _visibleSlots.Add(slot);
if (inViewLandcells is null)
{
CollectVisibleCells(
_visibleCellIds,
data.LandblockId,
data.WorldOrigin,
data.AabbMin.Z,
data.AabbMax.Z,
frustum);
}
} }
if (_visibleSlots.Count == 0) return; if (_visibleSlots.Count == 0) return;
@ -480,53 +459,6 @@ public sealed partial class TerrainModernRenderer : IDisposable
// Private helpers // Private helpers
// ---------------------------------------------------------------- // ----------------------------------------------------------------
/// <summary>
/// S3 landing hygiene (H2): frustum-only now. Retail never view-clips
/// terrain (see <see cref="DrawLandCells"/>'s doc); the CPU/GPU
/// clip-region equivalence check this used to also run (a doorway
/// slice's clip-space planes plus its NDC-AABB scissor fallback) had
/// exactly one caller — <see cref="Draw"/> — and that caller's own two
/// clip-region parameters were themselves dead (grep: no production
/// caller ever passed either), so the equivalence helper and its
/// homogeneous-plane subroutine are deleted along with them.
/// </summary>
internal static void CollectVisibleCells(
HashSet<uint> destination,
uint landblockId,
Vector3 worldOrigin,
float zMin,
float zMax,
FrustumPlanes? frustum)
{
ArgumentNullException.ThrowIfNull(destination);
const float cellSize = AcDream.Core.Physics.TerrainSurface.CellSize;
const int cellsPerSide = AcDream.Core.Physics.TerrainSurface.CellsPerSide;
uint prefix = landblockId & 0xFFFF0000u;
for (int cellX = 0; cellX < cellsPerSide; cellX++)
{
float minX = worldOrigin.X + cellX * cellSize;
float maxX = minX + cellSize;
for (int cellY = 0; cellY < cellsPerSide; cellY++)
{
float minY = worldOrigin.Y + cellY * cellSize;
float maxY = minY + cellSize;
var cellMin = new Vector3(minX, minY, zMin);
var cellMax = new Vector3(maxX, maxY, zMax);
if (frustum is not null
&& !FrustumCuller.IsAabbVisible(frustum.Value, cellMin, cellMax))
{
continue;
}
uint low = AcDream.Core.Physics.TerrainSurface.ComputeOutdoorCellLowId(
cellX * cellSize,
cellY * cellSize);
destination.Add(prefix | low);
}
}
}
private void EnsureCapacity(int newCapacity) private void EnsureCapacity(int newCapacity)
{ {
if (newCapacity <= _alloc.Capacity) if (newCapacity <= _alloc.Capacity)

View file

@ -5,7 +5,7 @@ namespace AcDream.App.Rendering.Vfx;
internal interface IWorldSceneParticleVisibility internal interface IWorldSceneParticleVisibility
{ {
void MarkVisibleCells(HashSet<uint> cellIds); void MarkVisibleLandscapeCells(HashSet<uint> cellIds);
void CompleteFrame(); void CompleteFrame();
@ -15,8 +15,10 @@ internal interface IWorldSceneParticleVisibility
/// <summary> /// <summary>
/// Bridges the retained retail PView result into the next physics update's /// Bridges the retained retail PView result into the next physics update's
/// <c>CObjCell::IsInView</c> particle gate. The controller owns only immutable /// <c>CObjCell::IsInView</c> particle gate. The controller owns only immutable
/// frame meaning: one completed viewer position plus the AC cells admitted by /// frame meaning: one completed viewer position plus the outdoor landscape
/// that completed view. It neither creates emitters nor performs rendering. /// cells admitted by that completed view. EnvCells use retail's constant-true
/// virtual and therefore never enter this retained set. The controller neither
/// creates emitters nor performs rendering.
/// </summary> /// </summary>
public sealed class ParticleVisibilityController : IWorldSceneParticleVisibility public sealed class ParticleVisibilityController : IWorldSceneParticleVisibility
{ {
@ -50,12 +52,24 @@ public sealed class ParticleVisibilityController : IWorldSceneParticleVisibility
_frameUsesWorldView = true; _frameUsesWorldView = true;
} }
public void MarkVisibleCells(HashSet<uint> cellIds) public void MarkVisibleLandscapeCells(HashSet<uint> cellIds)
{ {
ArgumentNullException.ThrowIfNull(cellIds); ArgumentNullException.ThrowIfNull(cellIds);
if (!_frameOpen || !_frameUsesWorldView) if (!_frameOpen || !_frameUsesWorldView)
return; return;
foreach (uint cellId in cellIds)
{
uint low = cellId & 0xFFFFu;
if (low == 0u || low >= 0x0100u)
{
throw new ArgumentException(
$"Landscape visibility accepts only outdoor land cells; "
+ $"0x{cellId:X8} is not one.",
nameof(cellIds));
}
}
_buildingCellIds.UnionWith(cellIds); _buildingCellIds.UnionWith(cellIds);
} }

View file

@ -100,9 +100,6 @@ internal interface IWorldRenderFrameBuilder
bool waitingForLogin, bool waitingForLogin,
DayGroupData? activeDayGroup); DayGroupData? activeDayGroup);
void ObserveDrawableCells(IReadOnlySet<uint> drawableCells);
void ClearDrawableCells();
} }
internal interface IWorldFrameCameraSource internal interface IWorldFrameCameraSource
@ -135,9 +132,6 @@ internal interface IWorldFrameEnvironmentPreparation
in RenderFrameFoundation foundation, in RenderFrameFoundation foundation,
DayGroupData? activeDayGroup); DayGroupData? activeDayGroup);
void ObserveDrawableCells(IReadOnlySet<uint> drawableCells);
void ClearDrawableCells();
} }
internal interface IWorldFrameAnimatedEntitySource internal interface IWorldFrameAnimatedEntitySource
@ -205,10 +199,6 @@ internal sealed class WorldRenderFrameBuilder : IWorldRenderFrameBuilder
return new WorldRenderFrame(camera, roots, buildings, animated); return new WorldRenderFrame(camera, roots, buildings, animated);
} }
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells) =>
_environment.ObserveDrawableCells(drawableCells);
public void ClearDrawableCells() => _environment.ClearDrawableCells();
} }
internal sealed class RuntimeWorldFrameCameraSource : IWorldFrameCameraSource internal sealed class RuntimeWorldFrameCameraSource : IWorldFrameCameraSource
@ -343,7 +333,6 @@ internal sealed class RuntimeWorldFrameVisibilityPreparation
{ {
private readonly RetailSelectionScene? _selection; private readonly RetailSelectionScene? _selection;
private readonly ParticleVisibilityController _particles; private readonly ParticleVisibilityController _particles;
private readonly TerrainModernRenderer? _terrain;
private readonly WorldRevealCoordinator? _reveal; private readonly WorldRevealCoordinator? _reveal;
private readonly WbFrustum? _environmentFrustum; private readonly WbFrustum? _environmentFrustum;
@ -356,7 +345,7 @@ internal sealed class RuntimeWorldFrameVisibilityPreparation
{ {
_selection = selection; _selection = selection;
_particles = particles ?? throw new ArgumentNullException(nameof(particles)); _particles = particles ?? throw new ArgumentNullException(nameof(particles));
_terrain = terrain; _ = terrain;
_reveal = reveal; _reveal = reveal;
_environmentFrustum = environmentFrustum; _environmentFrustum = environmentFrustum;
} }
@ -365,7 +354,6 @@ internal sealed class RuntimeWorldFrameVisibilityPreparation
{ {
_selection?.SetViewFrustum(camera.Frustum); _selection?.SetViewFrustum(camera.Frustum);
_particles.BeginFrame(camera.Position); _particles.BeginFrame(camera.Position);
_terrain?.BeginVisibilityFrame();
if (waitingForLogin) if (waitingForLogin)
return; return;
@ -541,13 +529,6 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation
_lightingUbo?.Upload(ubo); _lightingUbo?.Upload(ubo);
} }
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells) =>
ArgumentNullException.ThrowIfNull(drawableCells);
public void ClearDrawableCells()
{
}
private void UpdateSunFromSky(SkyKeyframe keyframe, bool playerInsideCell) private void UpdateSunFromSky(SkyKeyframe keyframe, bool playerInsideCell)
{ {
// CellManager::ChangePosition @ 0x004559B0 keys this on the player // CellManager::ChangePosition @ 0x004559B0 keys this on the player

View file

@ -8,8 +8,6 @@ namespace AcDream.App.Rendering;
internal interface IWorldScenePassExecutor internal interface IWorldScenePassExecutor
{ {
HashSet<uint>? TerrainVisibleCellIds { get; }
void BeginFrame(); void BeginFrame();
void PrepareFlatWorldClip(); void PrepareFlatWorldClip();
@ -100,8 +98,6 @@ internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
_particleRenderer = particleRenderer; _particleRenderer = particleRenderer;
} }
public HashSet<uint>? TerrainVisibleCellIds => _terrain?.VisibleCellIds;
public void BeginFrame() public void BeginFrame()
{ {
_visibleParticleOwners.Clear(); _visibleParticleOwners.Clear();

View file

@ -211,8 +211,6 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase
// this phase only enforces it. // this phase only enforces it.
_passes.DrawFlatTerrain(in camera, roots.PlayerLandblockId); _passes.DrawFlatTerrain(in camera, roots.PlayerLandblockId);
terrainDrawn = true; terrainDrawn = true;
if (_passes.TerrainVisibleCellIds is { } flatTerrainCells)
_particleVisibility.MarkVisibleCells(flatTerrainCells);
} }
else else
{ {
@ -250,11 +248,11 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase
camera.Camera.View, camera.Camera.View,
_diagnostics.CameraCellResolution)); _diagnostics.CameraCellResolution));
// One visibility answer: the walk's exact visited-cell set // CLandCell::IsInView consumes only the walk's exact landscape
// feeds effects and the frame environment (point lights and // half. EnvCells use CEnvCell::IsInView's constant-true result;
// directional-shadow filtering). // VisibleCells remains a diagnostics-only union.
_particleVisibility.MarkVisibleCells(pviewResult.VisibleCells); _particleVisibility.MarkVisibleLandscapeCells(
_frames.ObserveDrawableCells(pviewResult.VisibleCells); pviewResult.VisibleLandscapeCells);
_diagnostics.EmitPViewInput( _diagnostics.EmitPViewInput(
pviewResult.VisibleCells, pviewResult.VisibleCells,
pviewResult.ClipAssembly.OutsideViewSlices.Length, pviewResult.ClipAssembly.OutsideViewSlices.Length,
@ -287,7 +285,6 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase
_entities.LandblockEntries, _entities.LandblockEntries,
roots.PlayerLandblockId, roots.PlayerLandblockId,
world.AnimatedEntityIds); world.AnimatedEntityIds);
_frames.ClearDrawableCells();
} }
_passes.DisableClipDistances(); _passes.DisableClipDistances();

View file

@ -20,7 +20,11 @@ public sealed class ParticleSystem : IParticleSystem
// thousands of finite/fading emitters. // thousands of finite/fading emitters.
private readonly SortedSet<int> _allHandles = []; private readonly SortedSet<int> _allHandles = [];
private readonly SortedSet<int> _simulationHandles = []; private readonly SortedSet<int> _simulationHandles = [];
private readonly SortedSet<int> _worldSimulationHandles = []; // ApplyRetailView is a per-frame production path. SortedSet's enumerator
// allocates its traversal stack, so retain this subset as a sorted list;
// lifecycle mutations pay the binary insertion/removal cost and the
// warmed view walk stays allocation-free in emitter-spawn order.
private readonly List<int> _worldSimulationHandles = [];
private readonly SortedSet<int>[] _renderableHandlesByPass = private readonly SortedSet<int>[] _renderableHandlesByPass =
[[], [], []]; [[], [], []];
private readonly SortedSet<int>[] _renderableUnattachedHandlesByPass = private readonly SortedSet<int>[] _renderableUnattachedHandlesByPass =
@ -147,7 +151,7 @@ public sealed class ParticleSystem : IParticleSystem
_allHandles.Add(handle); _allHandles.Add(handle);
_simulationHandles.Add(handle); _simulationHandles.Add(handle);
if (visibilityPolicy == ParticleVisibilityPolicy.World) if (visibilityPolicy == ParticleVisibilityPolicy.World)
_worldSimulationHandles.Add(handle); AddWorldSimulationHandle(handle);
AddEmitterToRenderIndexes(emitter); AddEmitterToRenderIndexes(emitter);
for (int i = 0; i < desc.InitialParticles; i++) for (int i = 0; i < desc.InitialParticles; i++)
@ -330,7 +334,7 @@ public sealed class ParticleSystem : IParticleSystem
if (visibilityPolicy == ParticleVisibilityPolicy.World) if (visibilityPolicy == ParticleVisibilityPolicy.World)
{ {
if (emitter.SimulationEnabled) if (emitter.SimulationEnabled)
_worldSimulationHandles.Add(handle); AddWorldSimulationHandle(handle);
} }
else else
{ {
@ -343,22 +347,25 @@ public sealed class ParticleSystem : IParticleSystem
/// <summary> /// <summary>
/// Applies <c>CPhysicsObj::ShouldDrawParticles</c> (0x0050FE60) to every /// Applies <c>CPhysicsObj::ShouldDrawParticles</c> (0x0050FE60) to every
/// live emitter. The App layer supplies the previous completed retail /// live emitter. The App layer supplies the previous completed retail
/// PView's cell set, equivalent to <c>CObjCell::IsInView</c> when the next /// PView's exact landscape-cell set. The cell-family branch below invokes
/// physics update runs. /// the matching retail virtual when the next physics update runs:
/// <c>CLandCell::IsInView</c> reads that completed set, while
/// <c>CEnvCell::IsInView</c> is constant true.
/// </summary> /// </summary>
public void ApplyRetailView( public void ApplyRetailView(
Vector3 viewerPosition, Vector3 viewerPosition,
IReadOnlySet<uint> visibleCellIds, IReadOnlySet<uint> visibleLandscapeCellIds,
bool hasCompletedView, bool hasCompletedView,
float rangeMultiplier = 1f) float rangeMultiplier = 1f)
{ {
ArgumentNullException.ThrowIfNull(visibleCellIds); ArgumentNullException.ThrowIfNull(visibleLandscapeCellIds);
if (!float.IsFinite(rangeMultiplier) || rangeMultiplier <= 0f) if (!float.IsFinite(rangeMultiplier) || rangeMultiplier <= 0f)
rangeMultiplier = 1f; rangeMultiplier = 1f;
LastRetailViewEmitterVisitCount = 0; LastRetailViewEmitterVisitCount = 0;
foreach (int handle in _worldSimulationHandles) for (int i = 0; i < _worldSimulationHandles.Count; i++)
{ {
int handle = _worldSimulationHandles[i];
if (!_byHandle.TryGetValue(handle, out ParticleEmitter? emitter)) if (!_byHandle.TryGetValue(handle, out ParticleEmitter? emitter))
continue; continue;
LastRetailViewEmitterVisitCount++; LastRetailViewEmitterVisitCount++;
@ -374,8 +381,12 @@ public sealed class ParticleSystem : IParticleSystem
float maxDistance = emitter.Desc.MaxDegradeDistance * rangeMultiplier; float maxDistance = emitter.Desc.MaxDegradeDistance * rangeMultiplier;
float distance = RetailDistance(emitter.OwnerPosition, viewerPosition); float distance = RetailDistance(emitter.OwnerPosition, viewerPosition);
uint ownerCellLow = emitter.OwnerCellId & 0xFFFFu;
bool ownerCellInView = ownerCellLow >= 0x0100u
|| (ownerCellLow > 0u
&& visibleLandscapeCellIds.Contains(emitter.OwnerCellId));
emitter.ViewEligible = emitter.OwnerCellId != 0 emitter.ViewEligible = emitter.OwnerCellId != 0
&& visibleCellIds.Contains(emitter.OwnerCellId) && ownerCellInView
// The x87 comparison in ShouldDrawParticles admits unordered // The x87 comparison in ShouldDrawParticles admits unordered
// comparisons (NaN) and reject a negative authored range. // comparisons (NaN) and reject a negative authored range.
&& (float.IsNaN(distance) && (float.IsNaN(distance)
@ -442,7 +453,7 @@ public sealed class ParticleSystem : IParticleSystem
emitter.SimulationEnabled = true; emitter.SimulationEnabled = true;
_simulationHandles.Add(handle); _simulationHandles.Add(handle);
if (emitter.VisibilityPolicy == ParticleVisibilityPolicy.World) if (emitter.VisibilityPolicy == ParticleVisibilityPolicy.World)
_worldSimulationHandles.Add(handle); AddWorldSimulationHandle(handle);
else else
emitter.ViewEligible = true; emitter.ViewEligible = true;
} }
@ -1007,6 +1018,13 @@ public sealed class ParticleSystem : IParticleSystem
} }
} }
private void AddWorldSimulationHandle(int handle)
{
int index = _worldSimulationHandles.BinarySearch(handle);
if (index < 0)
_worldSimulationHandles.Insert(~index, handle);
}
private static bool IsRenderable(ParticleEmitter emitter) private static bool IsRenderable(ParticleEmitter emitter)
=> emitter.PresentationVisible && emitter.ViewEligible; => emitter.PresentationVisible && emitter.ViewEligible;

View file

@ -1,58 +1,42 @@
using System.Numerics; using AcDream.App.Tests.Rendering.Walk;
using AcDream.App.Rendering;
namespace AcDream.App.Tests.Rendering; namespace AcDream.App.Tests.Rendering;
/// <summary> /// <summary>
/// S3 landing hygiene (H2): <see cref="TerrainModernRenderer.CollectVisibleCells"/> /// Campaign OVERHAUL S5-c1: the null-root terrain path remains a safety draw,
/// is frustum-only now — its clip-plane-list and NDC-AABB scissor /// but it may not reconstruct or publish a competing cell-visibility answer.
/// parameters and the CPU/GPU clip-region equivalence check they fed had /// The same architecture pin protects the deleted point-light feedback seam.
/// exactly one production caller (<see cref="TerrainModernRenderer.Draw"/>),
/// and that caller never actually passed either (grep). The doorway-clip/scissor
/// rejection cases this file used to pin (<c>RejectsCellsOutsideDoorwayClipPlanes</c>,
/// <c>RejectsCellsOutsideDoorwayScissorAabb</c>, <c>UnionsCellsFromEveryLandscapeSlice</c>)
/// pinned a mechanism nothing in production ever fed — deleted with the
/// parameters. What remains still pins the one thing this method's callers
/// actually rely on: it publishes the visible terrain cell set per landblock,
/// respecting an explicit frustum when one is given.
/// </summary> /// </summary>
public sealed class TerrainParticleCellVisibilityTests public sealed class TerrainParticleCellVisibilityTests
{ {
[Fact] [Fact]
public void CollectVisibleCells_PublishesLandscapeCellsWithoutEntitySurvivors() public void ProductionRetainsNoReconstructedVisibilityOrLightFeedbackSymbols()
{ {
var cells = new HashSet<uint>(); string sourceRoot = Path.Combine(WalkOracleTraceRepoRoot.Find(), "src");
string[] production = Directory.GetFiles(
sourceRoot,
"*.cs",
SearchOption.AllDirectories);
string[] forbidden =
[
"CollectVisibleCells",
"TerrainVisibleCellIds",
"ObserveDrawableCells",
"ClearDrawableCells",
];
TerrainModernRenderer.CollectVisibleCells( foreach (string symbol in forbidden)
cells, {
0xA9B4FFFFu, string[] owners = production
Vector3.Zero, .Where(path => File.ReadAllText(path).Contains(
zMin: 0f, symbol,
zMax: 20f, StringComparison.Ordinal))
frustum: null); .Select(path => Path.GetRelativePath(sourceRoot, path))
.OrderBy(path => path, StringComparer.Ordinal)
Assert.Equal(64, cells.Count); .ToArray();
Assert.Contains(0xA9B40001u, cells); Assert.True(
Assert.Contains(0xA9B40040u, cells); owners.Length == 0,
} $"Production symbol {symbol} remains in: {string.Join(", ", owners)}");
}
[Fact]
public void CollectVisibleCells_ForceDrawnParentStillUsesCellFrustum()
{
var cells = new HashSet<uint>();
FrustumPlanes frustum = FrustumPlanes.FromViewProjection(Matrix4x4.Identity);
// The parent landblock may have entered TerrainModernRenderer's draw
// list through neverCullLandblockId. The cell collector deliberately
// receives no such bypass and still rejects its off-frustum cells.
TerrainModernRenderer.CollectVisibleCells(
cells,
0xA9B4FFFFu,
new Vector3(100f, 100f, 100f),
zMin: 100f,
zMax: 120f,
frustum);
Assert.Empty(cells);
} }
} }

View file

@ -28,7 +28,7 @@ public sealed class ParticleVisibilityControllerTests
controller.BeginFrame(Vector3.Zero); controller.BeginFrame(Vector3.Zero);
controller.UseWorldView(); controller.UseWorldView();
controller.MarkVisibleCells(new HashSet<uint> { 0x01010001u }); controller.MarkVisibleLandscapeCells(new HashSet<uint> { 0x01010001u });
controller.CompleteFrame(); controller.CompleteFrame();
controller.Apply(particles, 1f); controller.Apply(particles, 1f);
@ -84,15 +84,125 @@ public sealed class ParticleVisibilityControllerTests
controller.BeginFrame(Vector3.Zero); controller.BeginFrame(Vector3.Zero);
controller.UseWorldView(); controller.UseWorldView();
controller.MarkVisibleCells([0x01010001u]); controller.MarkVisibleLandscapeCells([0x01010001u]);
controller.CompleteFrame(); controller.CompleteFrame();
controller.BeginFrame(new Vector3(1000f, 1000f, 0f)); controller.BeginFrame(new Vector3(1000f, 1000f, 0f));
controller.UseWorldView(); controller.UseWorldView();
controller.MarkVisibleCells([0x02020001u]); controller.MarkVisibleLandscapeCells([0x02020001u]);
controller.AbortFrame(); controller.AbortFrame();
controller.Apply(particles, 1f); controller.Apply(particles, 1f);
Assert.True(Assert.Single(particles.EnumerateEmitters()).ViewEligible); Assert.True(Assert.Single(particles.EnumerateEmitters()).ViewEligible);
} }
[Fact]
public void CompleteAbortAndResetPublishOnlyCompleteLandscapeTransactions()
{
var particles = new ParticleSystem(new EmitterDescRegistry(), new Random(1));
int first = particles.SpawnEmitter(
new EmitterDesc
{
DatId = 0x32000004u,
Type = ParticleType.Still,
MaxDegradeDistance = 100f,
MaxParticles = 1,
},
Vector3.Zero,
attachedObjectId: 43u);
int second = particles.SpawnEmitter(
new EmitterDesc
{
DatId = 0x32000005u,
Type = ParticleType.Still,
MaxDegradeDistance = 100f,
MaxParticles = 1,
},
Vector3.Zero,
attachedObjectId: 44u);
particles.UpdateEmitterOwnerCell(first, 0x0101_0001u);
particles.UpdateEmitterOwnerCell(second, 0x0101_0002u);
var controller = new ParticleVisibilityController();
controller.BeginFrame(Vector3.Zero);
controller.UseWorldView();
controller.MarkVisibleLandscapeCells([0x0101_0001u]);
controller.CompleteFrame();
controller.BeginFrame(new Vector3(1000f, 0f, 0f));
controller.UseWorldView();
controller.MarkVisibleLandscapeCells([0x0101_0002u]);
controller.AbortFrame();
controller.Apply(particles, 1f);
Assert.True(particles.EnumerateEmitters().Single(e => e.Handle == first).ViewEligible);
Assert.False(particles.EnumerateEmitters().Single(e => e.Handle == second).ViewEligible);
controller.BeginFrame(Vector3.Zero);
controller.UseWorldView();
controller.MarkVisibleLandscapeCells([0x0101_0002u]);
controller.CompleteFrame();
controller.Apply(particles, 1f);
Assert.False(particles.EnumerateEmitters().Single(e => e.Handle == first).ViewEligible);
Assert.True(particles.EnumerateEmitters().Single(e => e.Handle == second).ViewEligible);
controller.Reset();
controller.Apply(particles, 1f);
Assert.All(particles.EnumerateEmitters(), emitter => Assert.False(emitter.ViewEligible));
}
[Theory]
[InlineData(0u)]
[InlineData(0x0101_0100u)]
public void MarkVisibleLandscapeCells_RejectsNonLandscapeIds(uint cellId)
{
var controller = new ParticleVisibilityController();
controller.BeginFrame(Vector3.Zero);
controller.UseWorldView();
ArgumentException error = Assert.Throws<ArgumentException>(
() => controller.MarkVisibleLandscapeCells([cellId]));
Assert.Contains($"0x{cellId:X8}", error.Message, StringComparison.Ordinal);
}
[Fact]
public void ProductionApply_WarmedParticleViewPathDoesNotAllocate()
{
var particles = new ParticleSystem(new EmitterDescRegistry(), new Random(1));
int outdoor = particles.SpawnEmitter(
new EmitterDesc
{
DatId = 0x32000006u,
Type = ParticleType.Still,
MaxDegradeDistance = 100f,
MaxParticles = 1,
},
Vector3.Zero,
attachedObjectId: 45u);
int environment = particles.SpawnEmitter(
new EmitterDesc
{
DatId = 0x32000007u,
Type = ParticleType.Still,
MaxDegradeDistance = 100f,
MaxParticles = 1,
},
Vector3.Zero,
attachedObjectId: 46u);
particles.UpdateEmitterOwnerCell(outdoor, 0x0101_0001u);
particles.UpdateEmitterOwnerCell(environment, 0x0101_0100u);
var controller = new ParticleVisibilityController();
controller.BeginFrame(Vector3.Zero);
controller.UseWorldView();
controller.MarkVisibleLandscapeCells([0x0101_0001u]);
controller.CompleteFrame();
long allocated = ZeroAllocationProbe.MeasureWarmed(
() => controller.Apply(particles, 1f),
batchSize: 256,
warmupBatches: 2,
samples: 4);
Assert.True(allocated == 0, $"Particle view allocated {allocated} bytes.");
}
} }

View file

@ -316,7 +316,7 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.DoesNotContain( Assert.DoesNotContain(
"one stable far-to-near stream keyed", architecture, StringComparison.OrdinalIgnoreCase); "one stable far-to-near stream keyed", architecture, StringComparison.OrdinalIgnoreCase);
Assert.Contains("162 active rows", register, StringComparison.Ordinal); Assert.Contains("161 active rows", register, StringComparison.Ordinal);
int apSectionStart = register.IndexOf( int apSectionStart = register.IndexOf(
"## 3. Documented approximation (AP)", StringComparison.Ordinal); "## 3. Documented approximation (AP)", StringComparison.Ordinal);
int apSectionEnd = register.IndexOf( int apSectionEnd = register.IndexOf(
@ -338,7 +338,7 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.Single(File.ReadLines(registerPath), static line => Assert.Single(File.ReadLines(registerPath), static line =>
line.StartsWith("| AP-243 |", StringComparison.Ordinal)); line.StartsWith("| AP-243 |", StringComparison.Ordinal));
Assert.Equal( Assert.Equal(
162, 161,
File.ReadLines(registerPath).Count(static line => File.ReadLines(registerPath).Count(static line =>
line.StartsWith("| AP-", StringComparison.Ordinal))); line.StartsWith("| AP-", StringComparison.Ordinal)));
Assert.Contains("greater than 50 m", register, StringComparison.Ordinal); Assert.Contains("greater than 50 m", register, StringComparison.Ordinal);

View file

@ -52,9 +52,6 @@ public sealed class WorldRenderFrameBuilderTests
in foundation, in foundation,
waitingForLogin: true, waitingForLogin: true,
activeDayGroup: null); activeDayGroup: null);
var drawableCells = new HashSet<uint> { 0x01010001u };
builder.ObserveDrawableCells(drawableCells);
builder.ClearDrawableCells();
Assert.Equal( Assert.Equal(
[ [
@ -74,8 +71,6 @@ public sealed class WorldRenderFrameBuilderTests
Assert.Null(result.ClipRoot); Assert.Null(result.ClipRoot);
Assert.True(visibility.WaitingForLogin); Assert.True(visibility.WaitingForLogin);
Assert.Equal(foundation, environment.Foundation); Assert.Equal(foundation, environment.Foundation);
Assert.Same(drawableCells, environment.DrawableCells);
Assert.Equal(1, environment.ClearCount);
} }
[Fact] [Fact]
@ -254,7 +249,7 @@ public sealed class WorldRenderFrameBuilderTests
} }
[Fact] [Fact]
public void Runtime_environment_uses_resident_lights_independent_of_drawable_cells() public void Runtime_environment_uses_all_resident_lights()
{ {
const uint visibleCell = 0x01010100u; const uint visibleCell = 0x01010100u;
const uint hiddenCell = 0x01010101u; const uint hiddenCell = 0x01010101u;
@ -280,10 +275,6 @@ public sealed class WorldRenderFrameBuilderTests
lightingUbo: null, lightingUbo: null,
new WorldRenderRangeState(4, 12), new WorldRenderRangeState(4, 12),
skyPes: null); skyPes: null);
var borrowed = new HashSet<uint> { visibleCell };
environment.ObserveDrawableCells(borrowed);
borrowed.Clear();
borrowed.Add(hiddenCell);
WorldCameraFrame camera = CameraFrame(new FlyCamera()); WorldCameraFrame camera = CameraFrame(new FlyCamera());
WorldRootFrame roots = default; WorldRootFrame roots = default;
RenderFrameFoundation foundation = default; RenderFrameFoundation foundation = default;
@ -293,7 +284,6 @@ public sealed class WorldRenderFrameBuilderTests
Assert.Contains(visibleLight, lighting.PointSnapshot); Assert.Contains(visibleLight, lighting.PointSnapshot);
Assert.Contains(hiddenLight, lighting.PointSnapshot); Assert.Contains(hiddenLight, lighting.PointSnapshot);
environment.ClearDrawableCells();
environment.Prepare(in camera, in roots, in foundation, activeDayGroup: null); environment.Prepare(in camera, in roots, in foundation, activeDayGroup: null);
Assert.Contains(visibleLight, lighting.PointSnapshot); Assert.Contains(visibleLight, lighting.PointSnapshot);
@ -544,10 +534,6 @@ public sealed class WorldRenderFrameBuilderTests
{ {
public RenderFrameFoundation Foundation { get; private set; } public RenderFrameFoundation Foundation { get; private set; }
public IReadOnlySet<uint>? DrawableCells { get; private set; }
public int ClearCount { get; private set; }
public void Prepare( public void Prepare(
in WorldCameraFrame camera, in WorldCameraFrame camera,
in WorldRootFrame roots, in WorldRootFrame roots,
@ -558,10 +544,6 @@ public sealed class WorldRenderFrameBuilderTests
Foundation = foundation; Foundation = foundation;
} }
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells) =>
DrawableCells = drawableCells;
public void ClearDrawableCells() => ClearCount++;
} }
private sealed class RecordingAnimated( private sealed class RecordingAnimated(

View file

@ -194,9 +194,7 @@ public sealed class WorldSceneRendererTests
"flat:clip", "flat:clip",
"flat:sky", "flat:sky",
"flat:terrain", "flat:terrain",
"visibility:mark",
"flat:entities", "flat:entities",
"frame:clear-cells",
"passes:disable-clip", "passes:disable-clip",
"particles:global", "particles:global",
"flat:weather", "flat:weather",
@ -220,7 +218,7 @@ public sealed class WorldSceneRendererTests
} }
[Fact] [Fact]
public void PViewWorld_PublishesTheWalkVisibleCellsOnce() public void PViewWorld_PublishesTheWalkLandscapeCellsOnce()
{ {
var root = new LoadedCell var root = new LoadedCell
{ {
@ -240,7 +238,6 @@ public sealed class WorldSceneRendererTests
"passes:begin", "passes:begin",
"pview:draw", "pview:draw",
"visibility:mark", "visibility:mark",
"frame:observe-cells",
"diagnostics:pview", "diagnostics:pview",
"passes:disable-clip", "passes:disable-clip",
"particles:none", "particles:none",
@ -265,7 +262,7 @@ public sealed class WorldSceneRendererTests
} }
[Fact] [Fact]
public void PViewWorld_PublishesLookInCellsToParticleAndLightVisibility() public void PViewWorld_PublishesOnlyLandscapeCellsToParticleVisibility()
{ {
var root = new LoadedCell var root = new LoadedCell
{ {
@ -276,15 +273,11 @@ public sealed class WorldSceneRendererTests
rig.Renderer.Render(default); rig.Renderer.Render(default);
// Retail gates effects on cell->IsInView() (ShouldDrawParticles // The borrowed result deliberately has three distinct products:
// @0x0050FE60), and a cell entered through a building portal // DrawableCells contains the EnvCell, VisibleCells is their diagnostic
// (DrawBuilding @0x0059F2A0 -> PView::ConstructView @0x005A57B0) is // union, and VisibleLandscapeCells contains only this outdoor id.
// drawn by the same traversal as a flooded cell. The particle gate and Assert.Equal([0x0101_0003u], rig.Visibility.MarkedCells);
// light-candidate scope must therefore receive the walk's one visited Assert.DoesNotContain(0x0101_0100u, rig.Visibility.MarkedCells);
// set, including look-ins.
Assert.Contains(0x01010003u, rig.Visibility.MarkedCells);
Assert.NotNull(rig.Frames.ObservedCells);
Assert.Contains(0x01010003u, rig.Frames.ObservedCells!);
} }
[Fact] [Fact]
@ -925,15 +918,6 @@ public sealed class WorldSceneRendererTests
return frame; return frame;
} }
public IReadOnlySet<uint>? ObservedCells { get; private set; }
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells)
{
calls.Add("frame:observe-cells");
ObservedCells = new HashSet<uint>(drawableCells);
}
public void ClearDrawableCells() => calls.Add("frame:clear-cells");
} }
private sealed class EntitySource : IWorldSceneEntitySource private sealed class EntitySource : IWorldSceneEntitySource
@ -995,7 +979,7 @@ public sealed class WorldSceneRendererTests
{ {
public HashSet<uint> MarkedCells { get; } = []; public HashSet<uint> MarkedCells { get; } = [];
public void MarkVisibleCells(HashSet<uint> cellIds) public void MarkVisibleLandscapeCells(HashSet<uint> cellIds)
{ {
calls.Add("visibility:mark"); calls.Add("visibility:mark");
MarkedCells.UnionWith(cellIds); MarkedCells.UnionWith(cellIds);
@ -1015,13 +999,14 @@ public sealed class WorldSceneRendererTests
public PViewRenderer(List<string> calls) public PViewRenderer(List<string> calls)
{ {
_calls = calls; _calls = calls;
// Distinct flood-only vs in-view sets: 0x01010003 is a look-in // Distinct typed products: EnvCell shell preparation, diagnostic
// cell that is drawn but never part of the main flood. // union, and the outdoor landscape IsInView answer.
_interiorResult = new RetailPViewFrameResult().Reset( _interiorResult = new RetailPViewFrameResult().Reset(
ClipFrameAssembler.BeginWalkFrame( ClipFrameAssembler.BeginWalkFrame(
ClipFrame.NoClip(), outdoorRoot: false), ClipFrame.NoClip(), outdoorRoot: false),
[0x01010001u], [0x0101_0100u],
[0x01010001u, 0x01010003u], [0x0101_0100u, 0x0101_0003u],
[0x0101_0003u],
default, default,
default, default,
diagnosticPartition: null); diagnosticPartition: null);
@ -1030,6 +1015,7 @@ public sealed class WorldSceneRendererTests
ClipFrame.NoClip(), outdoorRoot: true), ClipFrame.NoClip(), outdoorRoot: true),
[], [],
[], [],
[],
default, default,
default, default,
diagnosticPartition: null); diagnosticPartition: null);
@ -1062,8 +1048,6 @@ public sealed class WorldSceneRendererTests
public bool ThrowOnAbort { get; set; } public bool ThrowOnAbort { get; set; }
public HashSet<uint>? TerrainVisibleCellIds { get; } = [0x01010001u];
public RenderFrameFoundation? SkyFoundation { get; private set; } public RenderFrameFoundation? SkyFoundation { get; private set; }
public RenderFrameFoundation? WeatherFoundation { get; private set; } public RenderFrameFoundation? WeatherFoundation { get; private set; }

View file

@ -529,6 +529,62 @@ public sealed class ParticleSystemTests
Assert.False(Assert.Single(sys.EnumerateEmitters()).ViewEligible); Assert.False(Assert.Single(sys.EnumerateEmitters()).ViewEligible);
} }
[Fact]
public void ApplyRetailView_UsesLandscapeMembershipButEnvCellConstantVirtual()
{
var sys = MakeSystem();
var desc = new EmitterDesc
{
DatId = 0x3200007Bu,
Type = ParticleType.Still,
MaxDegradeDistance = 10f,
MaxParticles = 1,
};
int outdoor = sys.SpawnEmitter(desc, Vector3.Zero, attachedObjectId: 21u);
int environment = sys.SpawnEmitter(desc, Vector3.Zero, attachedObjectId: 22u);
int cellLess = sys.SpawnEmitter(desc, Vector3.Zero, attachedObjectId: 23u);
sys.UpdateEmitterOwnerCell(outdoor, 0x0101_0001u);
sys.UpdateEmitterOwnerCell(environment, 0x0101_0100u);
sys.ApplyRetailView(Vector3.Zero, new HashSet<uint> { 0x0101_0001u }, true);
ParticleEmitter[] first = sys.EnumerateEmitters().ToArray();
Assert.True(first.Single(emitter => emitter.Handle == outdoor).ViewEligible);
Assert.True(first.Single(emitter => emitter.Handle == environment).ViewEligible);
Assert.False(first.Single(emitter => emitter.Handle == cellLess).ViewEligible);
sys.ApplyRetailView(Vector3.Zero, new HashSet<uint>(), true);
ParticleEmitter[] second = sys.EnumerateEmitters().ToArray();
Assert.False(second.Single(emitter => emitter.Handle == outdoor).ViewEligible);
Assert.True(second.Single(emitter => emitter.Handle == environment).ViewEligible);
Assert.False(second.Single(emitter => emitter.Handle == cellLess).ViewEligible);
}
[Fact]
public void ApplyRetailView_NoCompletedViewRejectsBothCellFamilies()
{
var sys = MakeSystem();
var desc = new EmitterDesc
{
DatId = 0x3200007Du,
Type = ParticleType.Still,
MaxDegradeDistance = 10f,
MaxParticles = 1,
};
int outdoor = sys.SpawnEmitter(desc, Vector3.Zero, attachedObjectId: 31u);
int environment = sys.SpawnEmitter(desc, Vector3.Zero, attachedObjectId: 32u);
sys.UpdateEmitterOwnerCell(outdoor, 0x0101_0001u);
sys.UpdateEmitterOwnerCell(environment, 0x0101_0100u);
sys.ApplyRetailView(
Vector3.Zero,
new HashSet<uint> { 0x0101_0001u },
hasCompletedView: false);
Assert.All(sys.EnumerateEmitters(), emitter => Assert.False(emitter.ViewEligible));
}
[Fact] [Fact]
public void ApplyRetailView_UnorderedOwnerDistanceMatchesRetailX87Comparison() public void ApplyRetailView_UnorderedOwnerDistanceMatchesRetailX87Comparison()
{ {