Add paired depth-write variants for all five blended SetSurface families in ordinary and atmospheric Wb pipeline sets at both sample counts and in EnvCell. Select them only from the carried AlphaTestEnabled state, retain pure-Clip and Translucent override behavior, and cover disposal plus partial-construction rollback.
Correct the three stale depth oracles and the AP register count/temporary AP-232 overclaim.
Required restored mutations and first discriminating failures:
1. Wb StraightAlpha+Clip collapsed to depth-off: WalkStaticStreamPopulatorTests.ImmediateBuildingDetail_UsesExactResolvedSetSurfaceState failed at line 1278; expected wb-mesh-alpha-depth-write-1x, binds were wb-mesh-alpha-1x.
2. EnvCell raw Additive+Clip collapsed to depth-off: EnvCellAlphaDrawSourceTests.DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity failed at line 132; expected envcell-raw-additive-depth-write, actual envcell-raw-additive.
3. EnvCell non-Clip raw Additive forced depth-on: the same production transcript failed at line 132; first row expected envcell-raw-additive, actual envcell-raw-additive-depth-write.
4. Late Translucent|Clip override forced alpha-test/depth-on: the same production transcript failed at line 132; expected envcell-alpha, actual envcell-alpha-depth-write.
Resolve exact SetSurface blend, alpha-test, and fog state once during extraction and preserve it through recipe-10 prepared payloads, Wb/EnvCell command data, Vulkan pipelines, push constants, and both ordinary/atmospheric one-pass shaders. Preserve AP-240 Wb pure-Clip immediate opaque/A2C while EnvCell uses retail premultiplied Clip; detail-off routing remains unchanged. Correct AP-232 and register the remaining detail-off state divergence.
Mutation first failures (all restored):
- raw Add->SRCALPHA/ONE: WalkStaticStreamPopulatorTests.ImmediateBuildingDetail_UsesExactResolvedSetSurfaceState line 1278, expected wb-mesh-raw-additive-1x.
- inverse-add->alpha-add: same test line 1278, expected wb-mesh-inverse-additive-1x.
- remove Env inverse: EnvCellAlphaDrawSourceTests.DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity line 132, expected envcell-inverse.
- raw IsAdditive precedence: same test line 132, Translucent|Clip|Additive expected envcell-alpha.
- Wb paletted ParamB=0: OrderPreservingSubmitterTests line 333, expected 0.392156869.
- Wb DDS ParamB=0.05: same test line 333, expected 0.784313738.
- disable Alpha+Clip test: WalkStaticStreamPopulatorTests line 1286, expected 0.784313738.
- always fog raw Add: same test line 1287, expected no-fog true.
- disable fog non-Add: same test line 1287, expected no-fog false.
- X=a*qA: RetailDetailTextureContractTests line 261, shared squared-alpha substring absent.
- X includes base alpha: same test line 262, forbidden baseTexel.a present.
- CLIP uses 0.05: EnvCellAlphaDrawSourceTests.ClipShaders_UseGreaterEqualForThePerRangeReference line 367.
- second detail draw: EnvCell detail-on line 131, collection contained 2 draws.
- straight-alpha substitute: Wb immediate line 1278, expected wb-mesh-additive-1x.
- omit ordered detail arm: OrderPreservingSubmitterTests line 318, expected (77,3.5), got (0,0).
- omit atmospheric combine: RetailDetailTextureContractTests line 260, shared include absent.
- drop serialized opacity: ObjectMeshDataSerializerTests line 292, expected opacity bits, got 1.0.
- stale detail arm: atmospheric adjacency line 402, expected slot 0, got 77.
- per-frame surface map: EnvCell warmed allocation line 285, expected 0 B, got 204800 B.
Replace the building and EnvCell detail replay with retail's exact single-pass stage result, including authored surface opacity, squared detail alpha, final-alpha clipping, and the original subset pipeline/order. Arm the ordered walk command in place to close#471, delete the replay pipelines/shaders, and advance prepared content to recipe 9.
Mutation witnesses (each restored before commit):
- X=a*qA: RetailDetailTextureContractTests.BothShaderFamiliesUseTheSharedOnePassSourceAndDebugPrecedesDetailSample line 174, missing materialAlpha * detail.a * detail.a.
- X*=base alpha: same test line 175, forbidden baseTexel.a found.
- CLIP against base alpha: EnvCellAlphaDrawSourceTests.ClipShaders_UseGreaterEqualForThePerRangeReference line 260, final-X conditional missing.
- second detail draw: EnvCellAlphaDrawSourceTests.DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity line 105, Assert.Single saw 2 MDI calls.
- straight-alpha substitution: WalkStaticStreamPopulatorTests.ImmediateBuildingDetail_RetainsOriginalFramebufferFamily line 1244, Additive first failed (only wb-mesh-alpha-1x recorded; InvAlpha also failed).
- omit ordered arm: OrderPreservingSubmitterTests.PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace line 305, expected (77,3.5), got (0,0).
- omit atmospheric combine: RetailDetailTextureContractTests.BothShaderFamiliesUseTheSharedOnePassSourceAndDebugPrecedesDetailSample line 173, atmospheric shared include missing.
- drop serialized opacity: ObjectMeshDataSerializerTests.SurfaceOpacity_RoundTripsBitExactlyAndDeterministically line 288, first reported 0.5 bits 1056964608 vs 1065353216.
- stale detail arm: ordered adjacency test line 307, expected following ordinary (0,0), got (77,3.5).
- per-frame surface map: EnvCellAlphaDrawSourceTests.ProductionWholeLeaf_WarmedScanSubmitRhiAndFilteredReplayDoNotAllocate line 178, expected 0 B, got 147456 B.
Verification before commit: shader compiler 23/23; focused App 213/213; Content 75/75; Core Wb 10/10; launcher migration 6/6; Release solution build 0 warnings / 0 errors; git diff --check clean.
Borrow the exact prior-completed landscape visibility transaction and S2 CELLARRAY owner for the opt-in IA-24 directional-shadow pack. Select terrain by authored 1..64 cells, ordinary casters by CELLARRAY, and buildings by outdoor EffectCellId with no fallback.
Keep retained caster/material/terrain topology stable across visibility-only frames. Publish exact arbitrary active instance runs and bounded terrain commands through separate selection sequences; preserve transform-journal, fade/retry, deferral, shader/RHI, ordinary world, and pack-off behavior. Amend IA-24 and the S5 ledger.
Pre-commit gates: Release solution build 0 warnings/0 errors; focused visibility/frame/caster/prepared/GPU/terrain/pack lane 137/137; warmed caster/prepared/terrain selectors 0 B; git diff --check clean. Official hermetic and InstalledDat evidence intentionally run post-commit from this exact clean tree.
Mutation evidence (each restored exactly): (1) CELLARRAY->Parent first failed PriorLandscapeSelection expected [201,202,203,205], actual [204,205]. (2) all resident terrain first failed Assert.Single with 3 commands. (3) building EffectCell->anchor first failed expected trailing 205, actual 206. (4) admit missing membership first failed with extra 204. (5) completed->building scratch first failed completed-view Assert.True, expected true/actual false. (6) selection advanced BuildSequence first failed expected 1/actual 2. (7) alternating->prefix first failed active command count expected 3/actual 1.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Remove the dead classic-group CachedBatch/InstanceGroup sort-center sidecar and its cache, append, reset, and diagnostic-digest plumbing. Remove the discarded camera parameter chain from grouped alpha deferral and dispatcher submission diagnostics while preserving walk/particle CYpt keys, FIFO submission ordinals, and opaque camera distance.
Add production-facing reflection/source pins for the deleted shapes and camera chain, retained per-cell and opaque owners, the exact three AlphaFlushCounts reasons, and deleted alpha-order symbols. Update directly affected cache/digest/group lifecycle tests and record the complete section 22 result.
Gates: Release 0W/0E; focused App 132/132 plus Core 29/29; new pins 4/4; real allocation pins 2/2 at 0 B; shaders 32/32; hermetic 16755/16755; InstalledDat exactly 385 pass, 10 documented fail, 1 skip; diff-check clean.
Mutations: InstanceGroup and CachedBatch sidecars fail their Assert.Null pins; Defer and digest camera parameters fail method-shape pins; false count prose fails the exact-reason pin; RetailAlphaOrdering resurrection fails the deletion pin. Each was restored independently before the final matrix.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
F1 (BLOCKING, punch-fan view alignment). ClipFrameAssembler.
ReassembleOutsideViewFromWalk now keeps exactly ONE outside-view slice per
walk view, index-aligned with RetailFrameWalk.DrawBuilding's own
ActiveViewIndex (retail building_view = portal_view_num @0x0059f3bf): a
collapsed view (ClipPlaneSet.IsNothingVisible — retail polyClipFinish under
a degenerate view leaves <3 vertices, 0x59BDBC-0x59BDBF) now appends its
own ClipViewSlice.NothingVisible=true slot instead of being skipped, which
used to shift every later view's slice down by one. DrawWalkPunchFan draws
NOTHING for a NothingVisible slice and throws ArgumentOutOfRangeException
when activeViewIndex is out of range (fail-loud; never draws unclipped).
New tests: WalkOutsideViewReassemblyTests.
FirstViewCollapses_SecondSurvives_SlicesStayIndexAligned and
PunchLeaf_UsesIndexAlignedSlice_DrawsNothingForCollapsed_ThrowsOutOfRange.
MUTATION M1 (revert the append-nothing-on-skip fix, restoring the old
skip): both new tests fail with
Assert.Equal() Failure: Values differ
Expected: 2
Actual: 1
MUTATION M2 (restore the old ": default" unclipped fallback in
DrawWalkPunchFan): the punch-leaf test fails with
Assert.Equal() Failure: Values differ
Expected: 0
Actual: 1
(both restored; verified clean afterward).
F2 (major, exit-seal pin through the real producer + both overflow cases).
The exit-seal CPU/GPU equivalence pin moved from ClipFrameLayoutTests (which
drove ClipFrame.AppendSlot(ClipPlaneSet) via the zero-caller
ClipFrameAssembler.Assemble) to the new
tests/.../Walk/WalkFrameDriverClipSealTests.cs, which drives
WalkFrameDriver.BeginFrame + the real IWalkEventSink.OnInteriorFloodDrawTurn
hook — the SAME path RetailPViewPassExecutor.DrawExitPortalMask resolves
through via WalkFrameDriver.InteriorFloodViewClipPlanesAt. The new pin
compares WalkFrameDriver's captured planes EXACTLY (not just geometrically)
against ClipPlaneSet.From's independent CCW-normalized-perpendicular
computation, since AppendClipSlot's per-edge formula is bit-identical to
ClipPlaneSet's own. Two overflow cases added: a 9-vertex portal view exit-seals
to exactly the 4 conservative AABB planes containing every source vertex
(over-include, never under-include — AppendClipSlot's fallback for a view too
complex for the 8-plane budget); a 9-vertex OUTSIDE view (the punch-fan
sibling, WalkOutsideViewReassemblyTests.
NineVertexOutsideView_PunchSliceHasZeroPlanes_DrawsUnclipped_NotNothingVisible)
produces a zero-plane, NOT-NothingVisible slice — draws fully unclipped,
distinct from F1's "draw nothing" state.
MUTATION M1 (flip the winding selection, `ccw = area2 < 0f`):
Assert.Equal() Failure: Values are not within 4 decimal places
Expected: 0.9191 (rounded from 0.919144988)
Actual: 0.8944 (rounded from 0.89442724)
MUTATION M2 (drop the normalize from the per-edge plane formula):
Assert.Equal() Failure: Values are not within 4 decimal places
Expected: 0.9191 (rounded from 0.919144988)
Actual: 0.7 (rounded from 0.700000048)
MUTATION M3 (delete the >8-plane AABB overflow branch — the per-edge loop
then indexes a fixed 8-plane stackalloc with a 9-edge polygon):
System.IndexOutOfRangeException : Index was outside the bounds of the array.
at WalkFrameDriver.AppendClipSlot(...) line 1915
at WalkFrameDriver.CaptureViews(...) -> CaptureCellViews -> CaptureCellViewRoute
-> EmitFloodTurns -> IWalkEventSink.OnInteriorFloodDrawTurn
(all three restored; verified clean afterward).
F3 (minor/major). RetailPViewPassExecutor.cs's false bracket comment
(neither KEEP clip — exit seals nor punch fans — ever called Enable/
DisableClipDistances; the six DisableClipDistances() call sites all sat
around the UNCLIPPED particle/weather draws, whose bodies are no-ops on the
only backend) is rewritten to the truth; the six no-op calls and the
private DisableClipDistances() wrapper are deleted.
IWorldPassSurface.EnableClipDistances/DisableClipDistances stay on the
INTERFACE — WorldScenePassExecutor (the separate flat-world path) still
calls them.
F4a (EC transcript exactness). WalkFrameDriver.EmitFloodTurns now prints one
"EC" line per LIVE VIEW of the flood cell (retail's setup_view/DrawEnvCell
loop, PView::DrawCells @0x005a4ab1-0x005a4acc, fires once per live portal_view
slice, not once per cell) via the captured route's own SliceCount — read
directly off _lookInTurns[viewRouteIndex] rather than through
InteriorFloodViewSliceCountAt's flood-index indirection, because this same
loop also serves a building's LOOK-IN flood (WalkDrawStage.LookInStatic),
whose cells never populate InteriorFloodCells (the interior ROOT flood only)
— indexing through that accessor threw ArgumentOutOfRangeException on a real
look-in turn during verification, fixed before landing. The actual CellShell
DRAW event is unchanged (the frame stamp still dedupes it to one submission).
F4b (transcript exactness, trailing weather OC).
WalkTraceConformanceTests.Recorder now implements IWalkEventSink.
OnWeatherTurn (the interface's silent no-op default previously left the
replay side of Signature8 blind to the weather turn's own "OC" line), and
WalkTraceReplayContext.Signature8(WalkOracleFrame) no longer excludes the
trailing per-frame weather ObjectCellTurn — every ObjectCellTurn now reads
literally, on both sides. Implementing OnWeatherTurn alone left
Still_fixture_first_frame_reproduces_exactly(terrace-edge.walk) diverging
ONLY on the newly-un-excluded trailing OC (EXPECTED ...OC:f4180104|
OC:f418000b vs ACTUAL ...OC:f4180104 — a length-12 tail, nothing else
differs across a 16.6k-char signature) because
WalkTraceReplayContext.WeatherGateOpen had always defaulted to false (the
harness never needed to fire the walk's weather hook before). Per the
review round's own instruction not to weaken the pin, the harness itself is
fixed instead: WalkTraceReplayContext now implements ViewerCellId (= the
harness's own WalkFrame cameraCellId argument, matching production's
RetailPViewFrameInput.ViewerCellId invariant) and WeatherGateOpen
(retail's own SmartBox::is_player_outside gate, (cellId & 0xFFFF) < 0x100,
with no App-level render-toggle concept to AND against). MUTATION (delete
Recorder.OnWeatherTurn): Still_fixture_first_frame_reproduces_exactly
(terrace-edge.walk) fails with EXPECTED ending "...OC:f4180104|OC:f418000b"
vs ACTUAL ending "...OC:f4180104" (the trailing OC missing) — restored;
verified clean afterward.
Fixture-row status (InstalledDat lane, WalkTraceConformanceTests, 14 rows):
13 pass, 1 fails — Oh_doorway_still_first_frame_diff, the PRE-EXISTING
[Trait("Status","KnownFailure")] #458 row (a documented block-plane
precision boundary at token index 165, LC/SC content, upstream of any
EC/OC — unaffected by and unrelated to this round's OC change). Every OTHER
row, including every eight-kind (Signature8) row that now compares the
weather OC literally, passes exactly.
F5 (major, dead per-cell clip machinery). WbDrawDispatcher.SetClipRouting
(the only writer of the per-instance routing-active flag) had ZERO
production callers; EnvCellRenderer.SetClipRouting was called only with
null (WorldScenePassExecutor's BeginFrame/AbortFrame) — no path could ever
arm the per-cell clip-region table, on the walk path OR the flat path (the
S3 landing-hygiene AD-17 correction's "live for the flat path" clause is
itself corrected here: dormant in every path, deleted). Deleted:
WbDrawDispatcher's SetClipRouting/ClearClipRouting and their four backing
fields; EnvCellRenderer's SetClipRouting and its backing field (its
RenderModernMDIInternal write now unconditionally clears instanceClipSlot);
the ProbeClipRouteEnabled-gated per-frame [clip-route-disp] dispatch probe
and its three backing fields (the OTHER two ACDREAM_PROBE_CLIPROUTE
producers, [clip-route] and [clip-route-scis] in WorldRenderDiagnostics.cs,
are untouched — a different, still-live mechanism); ResolveEntitySlot,
IsIndoorCellId and the ClipSlotCull sentinel; the ACDREAM_CLIP_DEBUG probe
(RenderingDiagnostics.ClipDebugNoShellTrim) and its docs/launch-options.md
row, since the "clip shells" branch it toggled between is gone.
ResolveSlotForFrame is simplified to a parameterless
`(uint Slot, bool Culled) ResolveSlotForFrame() => (0u, false)` per the
review's own framing ("it becomes slot 0, never culled") rather than
deleted outright, since its callers (WbDrawDispatcher.cs and
WbDrawDispatcher.WalkClassify.cs) still want the same two-value shape.
tests/.../Wb/WbDrawDispatcherClipSlotTests.cs (12 tests, all exercising the
deleted routing arm via ResolveEntitySlot/the active ResolveSlotForFrame
branch) is replaced with one pin on the new parameterless behavior.
Shaders: mesh_modern.vert, mesh_atmospheric.vert, mesh_detail.vert,
particle.vert and particle_mesh.vert each lose their CellClip struct +
binding=2 ClipRegionBuf fetch, gl_ClipDistance write loop, and gl_PerVertex
redeclaration (portal_depth.vert's own, separate gl_PerVertex/gl_ClipDistance
pair — the KEEP mechanism, its planes handed through the TerrainClip UBO at
binding=2 in the UBO namespace — is untouched; verified it never read the
deleted SSBO). Recompiled via tools/compile-shaders.ps1 (glslc backend
detected; managed shaderc path ran); 24/24 pairs compiled. Re-pinned in
VulkanShaderManifestTests.RetailOracleSpirvSha256 (the only three of the
five edited shaders that carry a byte-exact retail-oracle pin):
mesh_modern.vert.spv = f9ed4ee7140ccd136130559dbea68545f733f6022f52b085b6df4bcb787223c6
particle.vert.spv = 95ce6ecf834930a92da5c5fe9aef513b38b5ba104704b98c1606af71fe17eaf3
particle_mesh.vert.spv = 043482b97c2ed036511692f89c75a0a6c298aba48cb519e5e3aff7fe7ba6371b
(mesh_atmospheric.vert.spv / mesh_detail.vert.spv changed too but carry no
retail-oracle pin, so no re-pin was needed for them.)
tests/.../ParticleBindlessInstanceTests.cs's
Assert.Contains("clipRegions[aClipSlot]", vertex) — a real breakage the
grep sweep caught — is deleted with a note; the surrounding aClipSlot
attribute-declaration assertion stays.
Vertex-layout residue kept THIS round, fed 0 by the CPU, unread by any
shader (S5's instance-buffer-layout revisit): instanceClipSlot[] (binding=3
SSBO) in mesh_modern.vert/mesh_atmospheric.vert/mesh_detail.vert; aClipSlot
(a per-vertex attribute) in particle.vert/particle_mesh.vert.
Grep sweep — "SetClipRouting|_clipRoutingActive|ClearClipRouting|
BeginScissor|NdcScissorRect|SetTerrainClip|BindTerrainClip|TerrainBytes|
ScissorNdcAabb|UseScissorFallback" over src/ and tests/: EMPTY (also swept
and rewrote several PRE-EXISTING prose mentions of BeginScissor/
ClearClipRouting in RetailPViewPassExecutor.cs, RetailPViewRenderer.cs,
WorldPassSurface.cs and RetailPViewPassExecutorTests.cs that predate this
commit's own work, purely to satisfy the literal sweep). "clipRegions|
instanceClipSlot|aClipSlot": every remaining match is the vertex-layout
residue named above, or a comment/assertion describing that same residue.
Deviations from a fully literal reading of F5 (recorded here since none of
them cross the mandatory automated gates, all of which pass):
- The C#-side ClipRegions SSBO publish/bind pipeline
(RhiWorldPassSurface.PrepareClipFrame's publish, WorldFrameSections.
ClipRegions, and ClipFrame's internal std430 byte-packing —
RegionBytes/RegionBytesForTest/CellClipStrideBytes) is NOT deleted this
round; it is left as documented dead residue (verified zero shader
readers) rather than rewritten. The mandatory grep sweep targets the
shader-source identifier `clipRegions` (lowercase), which is fully clean;
the C# publish machinery sits behind ~8 files (ClipFrame.cs,
WorldPassSurface.cs, WbDrawDispatcher.Rhi.cs, WorldPassScope.cs,
GpuBindingModel.cs, EnvCellRenderer.Rhi.cs, ParticleRenderer.Rhi.cs,
WbDrawDispatcher.OrderedStream.cs, plus the ClipFrameLayoutTests std430
pins) and CellClipPlanesOffset/MaxPlanes are the SAME constants
portal_depth.vert's KEEP mechanism uses for its own, unrelated UBO layout
— a full rewrite carried materially higher regression risk than this
round's mandatory scope justified. ClipFrame.cs and WorldPassSurface.cs
now both name this residue explicitly in their doc comments for a future
round to finish.
- SetClipRegionSsbo/_sharedClipRegionSsbo in WbDrawDispatcher.cs (a
pre-existing, already fully dead GL-era write-only field/method with zero
callers, unrelated to per-instance clip-slot routing) is left untouched —
outside F5's named scope and outside the mandatory grep sweep.
- WalkTraceReplayContext.ViewerCellId/WeatherGateOpen (F4b, detailed above)
is an addition beyond the literal §12 text, made to keep the InstalledDat
lane at exactly the four established known failures without weakening
the newly-un-excluded OC pin.
Gates: dotnet build tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c
Release and dotnet build src/AcDream.App/AcDream.App.csproj -c Release both
0 warnings/0 errors; dotnet build AcDream.slnx -c Release 0 warnings/0
errors; hermetic lane 6815/6815 passed; InstalledDat lane 243 passed / 4
failed / 1 skipped (exactly the four known failures: two #383 layout tests,
TowerAscentReplayTests, Oh_doorway_still_first_frame_diff #458);
VulkanShaderDescriptorContractTests/VulkanShaderManifestTests/
RenderPackSpirvValidatorTests/SkyVertexLayoutTests 34/34 passed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Campaign OVERHAUL S3 chunk 4 (docs/research/2026-09-01-overhaul/s3-walk-ownership-map.md §10). Retail
polygon-clips exactly three things on the CPU — portal polygons (PView::GetClip), punch fans
(DrawPortalPolyInternal against building_view), and exit seals (setup_view + polyClipFinish);
ordinary meshes, cell shells, terrain, particles and the weather are never view-clipped. Every
acdream-only clip rule beyond those three is deleted; the two that remain get a new CPU/GPU
equivalence pin.
DELETED (no retail counterpart):
- RetailPViewRenderer.DrawLandscapeDynamicsPhase's per-outside-view-slice weather loop
(SetTerrainClip + ClearClipRouting + the old DrawLandscapeSliceLate leaf, one call per active
landscape view) — retail's GameSky::Draw(sky,1) @0x00506ff0 runs ONCE, unclipped, after
LScape::draw's whole block loop. RetailPViewPassExecutor.DrawWeatherOnce stays the one call site;
it now also submits the rain particles (ParticleRenderPass.SkyPostScene) as ONE unclipped
submission, folded in from the deleted loop's per-slice clipSlot draw.
- RetailPViewPassExecutor.DrawLandscapeSliceLate, RetailPViewLandscapeLateSliceContext,
SetTerrainClip (the walk-level wrapper — its only caller was the deleted loop),
ClearClipRouting (same), SetCellShellClipRouting (already had no caller), and
UseIndoorMembershipOnlyRouting (4 call sites in RetailPViewRenderer.cs) — all "first view clips
the shell" residue: SetCellShellClipRouting was the ONLY place EnvCellRenderer.SetClipRouting
ever received a non-null value anywhere in the app, so once it's gone,
UseIndoorMembershipOnlyRouting's own clear calls are provably permanent no-ops. AbortFrame's
matching TryAbort(SetClipRouting(null))/TryAbort(ClearClipRouting) lines are dropped for the
same reason. KEPT: BeginDoorwayScissor, _surface.BindTerrainClip(), EnableClipDistances/
DisableClipDistances, and IWorldPassSurface.SetTerrainClip/ClipFrame.SetTerrainClip/
PrepareClipFrame's terrain-clip publish — all still have real consumers (DrawWalkSky's per-slice
outdoor sky mesh draw; the always-bound TerrainClip UBO section) or are shared classic (non-walk)
infrastructure out of this chunk's ownership (WorldScenePassExecutor, PortalTunnelPresentation).
- WbDrawDispatcher.WalkClassify.ResolvePartClipSlots (renamed ResolvePartVisible, returns bool) and
RetailWholeMeshSlot: the per-view clip-slot LIST always collapsed to either nothing or one
element (RenderDeviceD3D::DrawMesh @0x005A0860 submits the whole mesh once any view admits it) —
never anything but a Boolean wearing a list-shaped costume. IWalkLookInViewSource.
VisibleClipSlotsInLookInTurn is deleted outright (no consumer besides that collapse);
SphereVisibleInLookInTurn gained VisibleClipSlotsInLookInTurn's own testSphere parameter so the
no-authored-sphere case ("any admitted view, geometry ignored") still has a home.
EmitClassifiedBatches now emits exactly one batch per surviving TryClassifyBatch using the
entity's own classic slot (always 0 while _clipRoutingActive is never armed by the walk path).
- The six ProbeCathedralSkip*/ProbeCathedralShellOrderEnabled discriminators (RenderingDiagnostics,
their six docs/launch-options.md rows, every read site in WalkFrameDriver/
RetailPViewPassExecutor.WalkLeaf/RetailPViewRenderer, and the WalkFrameDriver.
TraceCathedralShellOrder method + RetailPViewRenderer._probeCathedralShellOrderFrame it served) —
the investigation is closed: the cathedral leak is retail behavior, not an acdream defect to
discriminate.
O1-O4 (the ledger's carried chunk-1 items, closed here because this chunk owns the weather path):
IWalkEventSink gains OnWeatherTurn(viewerCellId), fired by RetailFrameWalk.DrawLandscape
UNCONDITIONALLY at retail's own call-site shape (GameSky::Draw(sky,1) is itself unconditional from
LScape::draw; the is_player_outside gate lives inside it) but gated by the walk context's own new
WeatherGateOpen/ViewerCellId members (default false/0, so every other IRetailFrameWalkContext
implementer — test fixtures, the FW1 conformance replay context — is unaffected).
WalkProductionFrameContext wires RetailPViewFrameInput.ViewerCellId and
RetailPViewPassExecutor.ShouldDrawWeatherOnce(RenderSky, RenderWeather, PlayerCellId) into those two
members. WalkFrameDriver.OnWeatherTurn prints the "OC" transcript line at COLLECT time; the print is
removed from DrawWeatherOnce (the DRAW stays there, at Replay). Because DrawLandscape's new call sits
at the very end of the method, an outdoor root's transcript naturally ends with the line and an
interior root's lands strictly between "LS" and the flood's first "EC" — exactly retail's position —
with no extra plumbing.
Kept items each get a synthetic-view CPU/GPU equivalence pin (ClipFrameLayoutTests): exit seals via
the real ClipFrame.AppendSlot/GetSlotPlanes round trip, punch fans via the direct
ClipViewSlice.Planes = cps.PlaneArray assignment ClipFrameAssembler.Assemble uses — both assert every
polygon edge midpoint has ~0 signed distance under the GPU-read-back planes and non-negative distance
under every plane.
Every new pin's mutation was hand-verified this session (temporarily reintroduced, confirmed the
exact failing assertion, then reverted): a duplicated OnWeatherTurn call fails Assert.Single with
"2 matching items" on both the outdoor and interior position tests; suppressing the Collect-time
call fails the same assertions with "did not contain any matching items"; perturbing one GPU plane's
offset by 0.05 fails the exit-seal equivalence pin with "the closest plane was only 0.050000012
away". WalkProductionFrameContextTests pins that the ViewerCellId/WeatherGateOpen storage seam
itself never drops or swaps its own two constructor/Reset arguments.
No register row added or removed — every change here deletes an acdream-only rule; none introduces
a new deviation. Full solution build: 0 warnings/0 errors. App hermetic 6,829/6,829 (was 6,828 base
+ 1 new pin). InstalledDat 244 pass/1 skip/4 known (2x #383, TowerAscent, and the pre-existing #458
WalkLandscape.CheckBlocks block-visibility divergence — unrelated to this chunk, untouched by it).
Core.Tests Rendering 216/216.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Uploads a cell-shell mesh whose storage order is the reverse of its surface
order and asserts each uploaded batch's FirstIndex reads back its own
indices from the arena. Guards the G1 regression fixed at 8c6563ca.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The directional-shadow topology rebuilt on every streaming-churn frame
and was the measured body of the run-hitch stalls (701 of 708 baseline
stalls alloc-correlated):
- The draw sort comparer's enum-vs-enum CompareTo bound to
Enum.CompareTo(object) and boxed BOTH operands on every comparison —
a constant ~38.9 MB of garbage per topology rebuild (~4M boxes),
handing the GC a forced gen0 collection mid-frame. The full ~100k-draw
sort is replaced outright: draws hash-group by exact DrawKey in one
O(n) pass over retained chained-index arrays, and only the
few-thousand DISTINCT group keys sort (order-preserving packed
material|cull|firstIndex|baseVertex + count|slot|layer|foliage keys,
first-appearance tie-break) — bit-identical emission order to the old
stable sort, near-zero allocation, and no per-draw comparisons at all.
- The caster frame sorts 4-byte indices keyed on SortKey.Value instead
of shuffling multi-hundred-byte records through a boxing comparer.
- Owner-approved pipelining: on a frame whose shadow inputs just changed
(the same frame already paying frame-view/landscape rebuilds), the
caster-frame and prepared-draws topology rebuilds defer to the next
quieter frame, capped at two consecutive deferrals — inside the GPU
fence depth, so retained draws never reference a released arena range.
First build, generation change, caster BuildSequence change, and
journal overflow force the immediate path; deferred refreshes skip
identity-mismatched journal rows.
Owner-accepted in both presentation modes: stall frames 5.8/s -> ~0.45/s
uncapped (0.49/s capped), median stall 20.3 -> 13.7 ms, >25 ms frames
near zero, 275 fps uncapped baseline restored. Allocation gate: a warmed
topology rebuild must allocate <2 KiB (DirectionalShadowPreparedDrawTests).
docs/ISSUES.md carries the full evidence trail; the residual
content-proportional rebuild milliseconds are filed as the
incremental-topology successor, and the pre-existing town-view scaling
latch is filed as #432.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
UploadGfxObjMeshData built every completed mesh's index data three-plus
times over in LINQ transients (per-batch Indices.ToArray copies plus an
unsized SelectMany growth) on the render thread, up to the per-frame
upload budget. The conversion now fills one exact-size retained
CPUIndices array (the same one the B.4b pick path keeps) and hands the
shared arena (offset, count) segments of it; CPUPositions fills by a
direct pre-sized loop; the Sum/Any/FirstOrDefault transients are gone.
GlobalMeshBuffer.UploadMesh takes the segment form — the staged bytes
per batch are unchanged. Gate: a warmed completion must allocate near
its retained-copy size (MeshPipelineDeviceSeamTests).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Narrow re-review of a82959f1: APPROVE, with follow-ups. All items landed.
N2 (structural): (a) extracted the ONE shared InstanceGroup-from-key
construction seam, WbDrawDispatcher.CreateGroupFromKey(key, registration,
frame) — before this there were two near-identical `new InstanceGroup
{ ... }` initializers (GetOrCreateInstanceGroup and GetOrCreatePackedGroup)
that had already drifted once (the round-2 F1 bug). Both routes call it now;
CreateGroupFromKey's own `new()` is the only production InstanceGroup
construction site repo-wide, same precedent as AppendPackedInstance. (b)
GroupKey.FoliageFlags lost its `= 0u` default and moved before CullMode in
the declaration (CullMode keeps its default, C# requires optional params to
trail required ones), so a `new GroupKey(...)` that omits it is a compile
error. Fixed every real construction site the reorder/requirement touched:
the 2 production sites, ToKey (a reconstruction from InstanceGroup the
review didn't count but the reorder broke), and 5 test sites (one more than
the review's "4" — InstanceGroupClearTests had a second, implicit
target-typed `MakeKey` factory the original count missed). Verified by a
full solution build.
N1: added CreateGroupFromKey_CopiesFoliageFlagsFromTheKey
(InstanceGroupClearTests) — a key carrying FoliageFlags 0x2 in, the created
group's FoliageFlags 0x2 out. That test plus N2b's required field are what
actually guard the round-2 F1 blocker; reworded PackedDispatcherOracleTests'
existing test comment to say what IT proves (the classification-to-
BuildIndirectArrays-to-BatchData.flags path), not that it guards the
classifier.
N3: corrected the plan's round-2 paragraph — folding FoliageFlags into the
G2/G3 digest is correct and symmetric, but CompareClassifiedOutput only
runs from RenderScenePViewFrameProductController.BuildAndCompare, which has
no production caller anywhere in src/AcDream.App/, and both of
RenderScenePViewFrameProductTests's own callers construct the controller
without the optional dispatcher argument — so the fold catches nothing
until that oracle is wired to an actual caller.
N4: the plan's F6 note now names both classification caches — the classic
route's EntityClassificationCache.EntityCacheEntry (self-heals per entity
on its own next eviction) and the packed route's
PackedProjectionClassificationEntry/PackedClassifiedBatch.Key
(PackedProjectionClassificationCache.BeginFrame clears its entire cache in
one shot on a RenderSceneGeneration change) — and notes neither mechanism
is keyed to a pack switch specifically.
N5: deleted the now-unused single-generic ComputeEntityHasCutoutSubset<T>
overload; its 4 test call sites now use the two-generic, zero-alloc
overload with an unused int context and a static (_, value) => value
lambda, so there is exactly one ComputeEntityHasCutoutSubset to keep
correct.
A6 (reviewer-filed): ResolveFoliageWind's _windMean/_windGust started at 0
and always eased toward the weather target by clock delta, with no
distinction for a graph's first-ever advance. A pinned clock
(ACDREAM_SKY_PHASE_SECONDS, the offline pixel gate's determinism pin) never
advances between calls, so the wind reached only whatever fraction the
first (1-second-clamped) step produced and sat there forever; live, the
first 10 s after a graph is constructed (pack selection / login) spun up
from dead calm even though the weather already IS what it is. Fixed at the
root: the first advance (_windFrameSerial == -1, the constructor sentinel)
now snaps _windMean/_windGust straight to the target; every later advance
eases over WeatherSystem.TransitionSeconds exactly as before. Added a
SetWindClockSecondsOverrideForTesting seam (_windClockSecondsOverride is no
longer readonly) so a hermetic test can advance the pinned clock by an
exact amount between two resolves without a real-time Thread.Sleep; two new
tests prove the first-advance snap is exact and a second advance still
eases at the normal rate. The three existing indoor/wind-disabled/amplitude
gate tests pass unchanged.
Verify: Release build 0 warnings/0 errors. App hermetic-lane filter
6,046/0 failed. Core.Tests 4,695/0 failed. Full hermetic-filtered solution:
15,274/0 failed across 15 projects.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Narrow re-review of 43e3abed found every A1-A5/A7/A8 item resolved but
one new blocker in the production packed classifier.
F1 (BLOCKER): RetailPViewPassExecutor.DrawPackedProductionRoute — the
route production world geometry actually draws from — never computed
FoliageFlags at all. WbDrawDispatcher.PackedOracle.cs's
ClassifyPackedBatches built its GroupKey with the field defaulting to
0u, and GetOrCreatePackedGroup never copied it onto the created
InstanceGroup, so production BatchData.flags bits 1/2 were always zero
for every scenery entity: the world geometry never swayed even though
the independently-classified shadow caster did, so shadows visibly
swayed under rigid trees. Both classifier call sites now compute
FoliageFlags via the identical FoliageWindClassification.Classify call
and entity-scoped HasCutoutSubset OR the classic (non-packed) path
uses, and GetOrCreatePackedGroup copies it exactly like
GetOrCreateInstanceGroup always has. The G2/G3 classified-output
digest (AddOpaqueSubmissionGroup/BuildTransparentSubmissionDigest) now
also folds GroupKey.FoliageFlags into its hash — present in the key
since round 1 but never actually read by either digest function, so a
content-level (not just group-count-level) classic-vs-packed
divergence is now caught.
F2 (medium): the delayed-alpha replay path (PrepareDeferredAlphaDraws)
hardcoded Flags = 1, dropping bits 1/2 for any group replayed through
it — a trunk instance promoted into the alpha-blend group mid-fade
(the #188 translucency-promotion case) would stop swaying for the
duration of its fade. Now 1u | key.FoliageFlags.
F3 (nit): ComputeEntityHasCutoutSubset's three call sites (classic,
caster, and the newly-fixed packed classifier) each allocated a
closure over _meshAdapter per Setup entity per frame. A new
context-taking overload passes the mesh adapter as an explicit
argument to a static lambda instead, letting the compiler cache one
delegate for the method's lifetime rather than allocating fresh ones.
A3 test gap: WbDrawDispatcher.BindDirectionalShadowReceiver is now
internal so DirectionalShadowGpuTests can drive it directly with a
bare RecordingGpuDevice pass encoder, proving it emits
UniformAtmosphericFrame with the exact buffer/offset/size a
DirectionalShadowFrameBinding carries — paired with the existing test
proving that binding carries the caster's real bind forward untouched.
F1's missing test: PackedDispatcherOracleTests chains
FoliageWindClassification.Classify (called with the packed
classifier's exact argument shape) for a real 0x8... scenery entity id
through BuildIndirectArrays — the same shared, already-tested
production step both classic and packed group lists feed into BatchData —
proving the resulting flags word carries bit 0x2. Driving
ClassifyPackedBatches/GetOrCreatePackedGroup directly was not a "cheap
test": both are private instance methods reachable only through the
full RetailPViewPassExecutor route, which needs a real IGpuDevice,
world-pass scope, mesh manager, and compiled pipelines to construct —
no test anywhere in the App test project stands one up.
Nits: F4 corrects foliage_wind.glsl's header comment from "bit 31" to
the top-nibble test; F5 documents at the receiver bind site that the
caster's own AtmosphericFrameBufferBinding has its seven ABI v1
members zero/Identity by construction (only the two v2 wind members
are valid) — safe today because mesh_atmospheric.vert reads that
binding solely for wind displacement, flagged as a footgun for a
future v1-reading addition to that shader; F6 notes in the plan
(rather than fixes) that EntityCacheEntry does not proactively
invalidate when FoliageWindExclusions changes on a pack switch —
harmless with the pack off, self-heals on the entry's next natural
eviction.
foliage_wind.glsl's F4 comment-only change updated the SPIR-V
manifest's source hashes for the five includers (mesh_atmospheric.vert
+ four directional_shadow_world_* casters); the compiled .spv bytes
are byte-identical since comments do not affect bytecode.
Verify: Release build 0 warnings/0 errors. App hermetic-lane filter
6,043/0 failed. Core.Tests 4,695/0 failed. RenderPackValidator 30/30.
Full hermetic-filtered solution: 15,271/0 failed across 15 projects.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Opus dual-lens review of the three VM6 commits (0930c35d, 39e8408c,
6cc5e183) found two blockers and two should-fix issues; all landed here
along with the review's nits and documentation corrections.
Blockers:
- A1: the procedural-scenery classifier tested bit 31 alone instead of
the full top nibble (0xF000_0000 == 0x8000_0000), so it also matched
LandblockStaticEntityIdAllocator's 0xC... namespace (fences/gates/
building shells with a cutout subset), the 0xDA11_D0xx paperdoll id,
and the 0xFFFF_FF01 portal-tunnel id as procedural scenery — all
three would have swayed. ProceduralSceneryIdAllocator.IsInNamespace
now does the exact top-nibble test; FoliageWindClassification
delegates to it.
- A2: GroupKey (the receiver's instance-batching key) did not carry
FoliageFlags while the caster's dedup key already did, so a scenery
instance and a non-scenery instance sharing a mesh subset coalesced
into one receiver InstanceGroup whose flags were last-writer-wins —
disagreeing with the correctly-keyed caster. GroupKey now carries
FoliageFlags, computed before key construction and set exactly once
at group creation; the imperative re-stamp is gone, and CachedBatch's
now-redundant FoliageFlags field is removed.
Should-fix:
- A3: the world receiver pass bound UniformAtmosphericFrame only by
accident (leftover from the caster pass, which runs first each
frame, since Vulkan binding state isn't reset between passes).
DirectionalShadowFrameBinding now carries the caster's exact
AtmosphericFrameBufferBinding and BindDirectionalShadowReceiver binds
it explicitly.
- A4: a Setup-composed tree's opaque trunk part never got the trunk
flag because HasCutoutSubset is cached per GfxObj part, not per
entity. FoliageWindClassification.ComputeEntityHasCutoutSubset now
ORs HasCutoutSubset across an entity's resolved sibling parts once
per entity, threaded into ClassifyBatches/AddDirectionalShadowBatches
via a new optional override parameter.
Nits: A5 hashes the per-vertex flutter seed relative to the instance
origin instead of absolute world XY (fp32 sin() precision loss at far
landblock corners), mirrored in both foliage_wind.glsl and
FoliageWindModel; A7 documents the max(maxHeight, 0.5) divide-guard as
a deliberate pseudocode divergence; A8 switches FoliageWindExclusions'
construction to ToFrozenSet() and softens the "never stale" doc
comment to "no slower than one frame behind."
Tests added: top-nibble classification (0xFFFFFFFFu now correctly
false), GroupKey inequality across entity-driven scenery/landblock-
static classification, a caster-batch test proving the same pairing
never coalesces, ComputeEntityHasCutoutSubset unit + end-to-end
two-part-Setup tests, the caster→receiver AtmosphericFrame binding
carry-through, flutter-hash translation invariance relative to
instance origin, and a Storm-wind mid-height displacement floor
guarding against a "no motion" regression.
Docs: plan VM6 body corrected to the five-row WeatherKind table, "bits
1 and 2", "all four" caster shaders, and top-nibble wording throughout;
the owner gate checklist's Rain/Storm step; the stale v1-only shader-
interface compatibility entry; semantic-bindings-v1.md's v2 members
folded into the main 192-byte block; the IA-25 register row's top-
nibble wording; AtmosphericFrameInputs.cs's ABI size reference.
foliage_wind.glsl's A5 change recompiled exactly the five shaders that
include it (mesh_atmospheric.vert, the four directional_shadow_world_*
casters) plus the manifest; no other .spv changed.
Verify: Release build 0 warnings/0 errors. App hermetic-lane filter
6,041/0 failed (no environment-specific failures this run).
RenderPackValidator 30/30. Full hermetic-filtered solution: 15,269/0
failed across 15 projects.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Procedural-scenery foliage (trees/bushes — entity ids in the
ProceduralSceneryIdAllocator's 0x8XXYYIII namespace) sways with weather in
mesh_atmospheric.vert and all four directional_shadow_world_* caster vertex
shaders, both calling the identical new foliage_wind.glsl include so the
shadow moves with the leaf by construction.
Classification (FoliageWindClassification, AcDream.App.Rendering.Wb): two
new BatchData.flags bits, computed once per (entity, subset) from four
inputs — entity id (bit 31 for procedural scenery), the pack's declared
FoliageExclusions membership, the subset's TranslucencyKind, and
ObjectRenderData.HasCutoutSubset (computed once per mesh at build time, not
per frame). Bit 1 marks an alpha-cutout leaf subset; bit 2 marks an opaque
trunk subset (only when its own mesh also owns a cutout subset, so rocks
stay still). WbDrawDispatcher.ClassifyBatches (world receiver) and
AddDirectionalShadowBatches (caster) call this with the same four inputs, so
casters and receivers classify identically without needing to share state.
Retail's mesh_modern/terrain_modern/mesh_detail pipelines never read these
bits, so pack-off output is unaffected.
Motion model (foliage_wind.glsl, mirrored bit-for-bit in the new
FoliageWindModel for hermetic CPU tests): height-squared-scaled slow lean
for every foliage subset, plus branch swing and per-vertex-hash-decorrelated
flutter for cutout subsets only. AtmosphericPostProcessGraph.ResolveFoliageWind
resolves the wind block once per frame.Serial — advanced by whichever of
RenderDirectionalShadows (which runs first) or RenderPostProcess is called
first that frame, with the second reading the already-advanced state, which
is what keeps the caster and receiver reading byte-identical clock/strength
values. The per-day-group mean/gust target (AtmospherePolicyDeclaration.
FoliageWindByDayGroup, keyed by the same day-group index convention
ActiveDayGroupMultipliers already established: Clear/Cloudy/Overcast/Rainy)
eases toward its target over WeatherSystem.TransitionSeconds (10s) so a
weather change never snaps; wind-enabled off or indoor instead gates the
OUTPUT to an exact zero (not an asymptotic approach) so a settings toggle or
cell transition is immediate. The wind clock is a Stopwatch started at graph
construction (monotonic, session-relative magnitude for GPU sin() accuracy),
overridable by the same ACDREAM_SKY_PHASE_SECONDS pin SkyRenderer already
uses, for deterministic offline gates.
New settings: wind-enabled, wind-strength, wind-direction-degrees (225°
default — no authored retail wind direction exists to read),
wind-lean-metres, wind-branch-metres, wind-flutter-metres (0 on Low),
wind-canopy-height-metres.
Register row IA-25 files this as an intentional, strictly opt-in divergence:
retail applies no per-vertex wind displacement to any geometry. Known,
accepted limitation: classification is per mesh-subset (one BatchData.flags
word per indirect-draw batch), not per entity instance, so the rare case of
one mesh subset being reachable from both a procedural-scenery and a
non-scenery placement would classify all of that subset's instances alike.
Tests: FoliageWindClassificationTests (the full classification matrix),
FoliageWindModelTests (identity on non-foliage/calm-wind/base-vertex,
canopy-top displacement bound, z-never-increases, trunk has no flutter
term), RenderPackAtmospherePolicyEvaluationTests (exact day-group lookup,
no interpolation across day-group ids, easing convergence without overshoot
or discontinuity), AtmosphericShaderAbiTests (each of the five shaders calls
acdreamFoliageDisplace exactly once; mesh_modern/terrain/mesh_detail call it
never), and four AtmosphericPostProcessGraphTests additions (indoor/disabled
exact-zero gating, settings-to-UBO wiring, same-frame-Serial idempotency —
the last proxies the caster/receiver agreement invariant without needing
this hermetic harness's WbDrawDispatcher/TerrainModernRenderer dependency
chain to exercise RenderDirectionalShadows directly).
App hermetic filter: 6015/6017 (the same 2 pre-existing failures as VM6a,
confirmed unrelated). Core.Tests hermetic: 4697/4697. RenderPackValidator.Tests:
30/30. Full solution Debug and Release builds green. Shader recompile
touched exactly the 5 edited files' .spv (plus manifest); the retail oracle
set and every other pack shader are byte-identical.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Opus dual-lens review of 05970306 + 388457a7 (APPROVE WITH FIXES). Four
items, all landed:
1. FOG (behavioural). Retail's D3D fixed-function fog stage runs AFTER the
texture-stage pipeline, so the detail contribution must be fogged, not
just the base. mesh_modern.frag already fogs the base colour
(applyFog(rgb, vWorldPos)) before mesh_detail's replay draws over it;
mesh_detail.frag previously emitted raw detail.rgb, understating fog by
f*a*(fog-detail). Fix: mesh_detail.vert now outputs vWorldPos (mirroring
mesh_modern.vert); mesh_detail.frag declares the identical SceneLighting
UBO and applyFog function (copied verbatim, same binding/std140/math) and
fogs detail.rgb before emitting it. This collapses algebraically to
retail's fog-after-combine order:
(1-a)*mix(base,fog,f) + a*mix(detail,fog,f) = mix(lerp(base,detail,a),fog,f)
RetailDetailTextureContract gains ExpectedFogged(base,detail,opacity,fog,
fogFactor); RetailDetailTextureContractTests pins the identity across 200
random samples within 1e-6.
2. EnvCellRenderer.Rhi.cs's DrawEnvCell-category comment still said "apply
the 10-50 m positive-view-depth fade" — a stale claim from before VM1
removed the fade. Replaced with the mip-chain attenuation statement that
mesh_detail.vert's header comment already carries.
3. Added the test the VM1 contract required but never had: TerrainAtlas
.TryCreateDetailTexture uploads a full mip chain (MipLevelCount ==
RhiWorldTextureArray.MipLevelsFor(w,h), GenerateMipChain called) and
registers with the repeat/linear world sampler, not single-level or
clamped. Drives the private method directly (reflection) against a
synthetic PFID_A8R8G8B8 RenderSurface through a minimal in-memory
IDatReaderWriter fake, so the lane stays hermetic (no installed DAT).
4. #226 pseudocode note: noted that retail's stage-1 OUTPUT alpha
(MODULATE(TEXTURE, CURRENT), 0x0059c549) — the framebuffer blend weight a
delayed-alpha subset composites with — is not modelled; acdream instead
draws a second pass weighted by detail.a*diffuseAlpha. Identical for
opaque subsets, a bounded difference on translucent building/EnvCell
subsets already covered by the existing AP-34 shared-alpha-queue
divergence row. Also qualified the tmpmaterial.Diffuse.a = 1f (0x0059cb99)
citation to name its exact branch (burnedInStaticLights < 0 &&
*(render_device+0x7e4) == 0); the other branch leaves diffuse FromVertex,
but the opaque->1 / fading->opacity mapping still holds either way.
Nit also folded in: EnvCellRendererTests' new SubmitRhi instance-alpha test
is now a [Theory] over WbRenderPass.Opaque and .Transparent, pinning the
bind-before-first-draw invariant on both passes.
Regenerated mesh_detail's committed SPIR-V and the shader manifest
(tools/compile-shaders.ps1); no other shader pair changed.
Verified: dotnet build AcDream.slnx -c Release (0 warnings, 0 errors);
dotnet test on AcDream.App.Tests (Release, hermetic lanes) green, including
the shader manifest tests explicitly; AcDream.Core.Tests unaffected/green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
EnvCellRenderer.Rhi's SubmitRhi bound StorageInstances/StorageBatches/
StorageClipSlots/StorageGlobalLights/StorageInstanceLightSets every frame
but never GpuBindingModel.StorageInstanceAlpha (binding 7) — the SSBO
mesh_modern.vert reads as instanceAlpha[instanceIndex] (vOpacityMultiplier,
#188) and, as of Campaign VM VM1 (05970306), mesh_detail.vert now reads the
same way (vDetailOpacity). Without a bind of its own, both the interior
shell pass and the interior detail replay read whatever section
WbDrawDispatcher's own SubmitRhi last bound in the same pass — an unrelated
object's opacity array, indexed by these EnvCell instance ids.
This predates VM1 (6c79d35c has the same omission on the mesh_modern side);
VM1 must not widen a latent defect by adding a second unconditional reader
of the same unbound slot.
Fix, root cause, no guard: EnvCellRenderer now owns _instanceAlphaData, a
grow-only float[] parallel to _gpuInstanceTransforms (same pattern as
_clipSlotData/_lightSetData), filled with the constant 1.0f every frame —
EnvCell shells have no #188 TransparentPartHook translucency fade (that
mechanism fades object PARTS, never cells) — and bound at
GpuBindingModel.StorageInstanceAlpha alongside the renderer's other
per-frame ring sections, before any draw in the pass.
Test: EnvCellRendererTests.SubmitRhi_BindsConstantOneInstanceAlphaBeforeAnyDrawInThePass
drives SubmitRhi directly (reflection, mirroring the file's existing
private-method test pattern) with N seeded cell instances and one real
draw command, then asserts against RecordingGpuDevice that
StorageInstanceAlpha is bound with exactly N floats all equal to 1.0f, and
that the bind precedes the pass's first MultiDrawIndexedIndirect call.
Verified failing (StorageInstanceAlpha was never bound) with the fix
temporarily reverted, then passing restored.
Verified: dotnet build AcDream.slnx -c Release (0 warnings, 0 errors);
dotnet test on AcDream.App.Tests (Release, hermetic lanes) green,
5960/5960 (5959 baseline + 1 new test).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The first commit converted the four members the issue named and left the other
sites alone, reasoning that none had been observed failing. A 20-run
complete-solution baseline disproved that within minutes:
run 2 LiveEntityRuntimeTests.AnimationView_HotSpatialTraversal…
run 14 StaticRenderProjectionJournalTests.ActiveAnimatedSynchronization…
run 18 StaticRenderProjectionJournalTests.ActiveAnimatedSynchronization…
run 19 CurrentRenderSceneOracleTests.SurfaceOverrideFingerprint…
Both new names are the same shape as the four — one warm call, then a
thousand-iteration loop inside the measured window — and neither had been
recorded anywhere. "Not observed failing" only ever meant "not yet observed",
and leaving known-shape sites in place would have guaranteed the acceptance gate
failed. Run 19 is the sharper lesson: the issue named
`SurfaceOverrideFingerprint_DictionaryHotPathAllocatesNothing`, and the first
commit converted a *different* test in that same file, so the actually-named
member was still on the old shape. Matching by file was not matching by test.
Every strict-zero site in the assembly is now on the probe — ten tests. Two came
out stricter rather than merely steadier:
`StaticRenderProjectionJournalTests` was measuring a synchronise whose journal
does **not** coalesce. Repeating it grew the journal by 1,000 entries per call —
192,000 by the end of a probe run — so the steady state the test claimed to
measure did not exist and the single-call window had been hiding it. Its step is
now the whole frame cycle, synchronise *and* drain, which puts `DrainTo` inside
the measured window for the first time and asserts the journal ends empty.
`RetailInboundEventDispatcherTests` asserted a hard-coded 1,001 callbacks. It
now counts its own dispatches and pins the callback count against that, so the
assertion still proves the fast path ran the callback every time without being
coupled to a loop bound that no longer exists.
Left alone deliberately: the four sites asserting a tolerance rather than zero —
`CellViewDedupTests` and `PortalProjectionTests`. Their ceilings already absorb
this noise and none has flaked; changing a bound in either direction is a
separate decision from fixing a measurement. Worth noting that
`PortalProjectionTests`' ceiling exists explicitly to tolerate "a
tiered-JIT/ArrayPool bookkeeping transition ... to the first measured batch",
which is exactly what the probe removes, so it could probably be tightened to
zero now — recorded in the issue rather than done here.
Solution build 0 warnings / 0 errors; App suite 3,941 passed / 3 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Commit 2 deleted the GL rendering backend's implementations; this step
removes the package references and shader vocabulary they leave behind,
so nothing in the App project still spells Silk.NET.OpenGL.
Silk.NET.OpenGL and Silk.NET.OpenGL.Extensions.ARB are dropped from
AcDream.App.csproj. Chorizite.Core stays — the audit is NOT clean: its
Render.Enums (TextureFormat, BufferUsage) and Lib.BoundingBox types are
used directly and extensively across the Wb texture/mesh pipeline,
independent of the deleted GL IUniformBuffer implementers the package
comment used to cite. The stale comment is corrected in place.
IMeshPipelineDevice.Gl is removed along with the GL? gl parameter
threaded through WbMeshAdapter's four constructors, WorldRenderComposition's
CreateMeshAdapter, and VulkanMeshPipelineDevice's Gl => null
implementation — nothing read any of them once the legacy per-mesh
upload bodies were gone (confirmed by grep: the sole non-doc-comment hit
was a test assertion). While in WbMeshAdapter.Dispose(), found and fixed
a real bug along the way: its teardown still pattern-matched the deleted
GL GpuFrameFlightController to decide whether to wait for submitted work,
which VulkanFrameFlightController replaced at slice V6a without this site
being updated — so the wait had been silently dead on every Vulkan run
since then. Retargeted to VulkanFrameFlightController, which carries the
same WaitForSubmittedWork().
The GL pixel-format vocabulary (Silk.NET.OpenGL.PixelFormat/PixelType) that
WorldTextureArray/TextureFormatExtensions/TextureAtlasManager used for
upload validation is replaced by AcDream.Content's existing Silk.NET-free
UploadPixelFormat/UploadPixelType enums (added at MP1a to keep the bake
tool GL-free); two new members (Rgb, Red, Float) extend that enum with
their GL ABI constants to cover the full vocabulary WorldTextureArray
needs, since MP1a's original set only covered what the extractor itself
emits. ObjectMeshManager's App-boundary cast
`(Silk.NET.OpenGL.PixelFormat?)batch.UploadPixelFormat` becomes a direct
pass-through now that both sides share the type.
GpuBindingModel.StorageTextureTable (the GL-only binding=9 emulation of
the Vulkan texture table) is deleted and StorageBindingCount drops from
10 to 9; the descriptor-set-layout code that builds from that count
(VulkanPipelineLayouts, VulkanFrameBindings) is untouched and just
allocates one fewer always-dummy-seeded, always-unused binding.
Several fully dead GL-only classes came along for the ride, confirmed by
zero construction sites: SilkFramebufferViewportTarget
(NullFramebufferViewportTarget is the sole production
IFramebufferViewportTarget), SilkRenderGlStateReader
(NullRenderGlStateReader.Instance is the sole IRenderGlStateReader),
RuntimeRenderFrameClearPhase (VulkanRenderFrameClearPhase is the sole
IRenderFrameClearPhase, expressing the same atmosphere-clear logic as a
pass load-op instead), and GpuFrameTimer plus FrameProfiler's
GL-owning FrameBoundary(GL) overload and BeginGpuFrame/EndGpuFrame
bracket (RecordGpuSample is the only GPU-timing path any backend uses
now — the ACDREAM_WB_DIAG nested-query exclusion these existed for no
longer applies, since WbDrawDispatcher's own diagnostic GPU sampling
already moved to the device's Vulkan timer pool). GpuFrameFlightController
itself stays (never constructed with a real fence API in production, but
its retirement-ledger/serial-ring logic is backend-neutral and still
covered by its own unit tests) — only its GL-specific parts (the public
GL constructor overload, SilkGpuFenceApi) are deleted, since removing the
whole class would mean restructuring the frozen Slice-8 composition
shape's GpuFrameFlightController? threading, which is out of this
commit's scope. TextureParameters.cs and BufferUsageExtensions.cs
(zero callers each) are deleted outright.
common.glsl is deleted: nothing in the actual Vulkan .spv build reads
it. tools/ShaderCompiler/Program.cs compiles each .vert/.frag pair
directly and tools/ShaderCompiler/VulkanGlslPreamble.cs injects its own
complete self-contained preamble per file; common.glsl's textual
concatenation was exclusively Shader.cs's GL-only mechanism, deleted at
Commit 2. The five shader files that named it in comments
(mesh_modern.vert, particle.vert, particle.frag, sky.frag,
terrain_modern.frag) are corrected to point at VulkanGlslPreamble.cs
instead. mesh.vert/mesh.frag — the pre-N.5 legacy shader pair the
mandatory modern path already made unreachable, with zero C# consumers
and no compiled .spv — are deleted too. Regenerated via
tools/compile-shaders.ps1: 9/9 remaining shader pairs compile
(previously 9/10, with mesh the sole failure — the VulkanShaderManifestTests
doc comment's "nine of ten are not Vulkan-expressible" was already
stale before this commit).
Test fallout: dead-subject test methods/files are deleted rather than
patched (TextRendererFailureSafetyTests.cs, ClipFrameUploadTests.cs,
GpuResourceRetirementTransactionTests.cs's GL queue tests, one
WorldRenderDiagnosticsTests source-order test, one
RenderFrameResourceControllerTests clear-phase-order test); tests whose
subject moved or was renamed are updated in place rather than deleted
(GpuContractTests, VulkanCapabilityGateTests, MeshPipelineDeviceSeamTests'
pinned seven-member surface now reads six, ParticleBindlessInstanceTests'
cross-dialect check now covers the one surviving dialect,
WbMeshAdapterTests' misleadingly-named null-gl test — gpuDevice was
always the parameter that actually threw).
Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors,
with the Silk.NET.OpenGL/.Extensions.ARB package references physically
removed from the csproj (not just unreferenced in code).
Tests: full-solution `dotnet test` green across every project.
Zero remaining `using Silk.NET.OpenGL` anywhere in src/ or tests/.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Vulkan is the sole, user-signed-off backend (V10 landed) and step 1
already removed ImGui/Studio/DevTools. This step deletes the GL
rendering backend itself: every Gpu/Gl/** implementation, the Wb
ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/
BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache,
RenderBootstrap, and RenderFrameGlStateController.
GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/
OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses
its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and
RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone —
there is nothing left to select between. The five world-draw dual-arm
renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer,
ParticleRenderer, SkyRenderer) and the composition roots
(WorldRenderComposition, HostInputCameraComposition,
LivePresentationComposition, FrameRootComposition) collapse to their
RHI-only arm. GL-only diagnostic properties with a live external reader
(DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op
rather than disappearing, since the reader is out of this commit's
scope.
A few GL-flavored mechanisms turned out to be backend-neutral once
isolated: GlConstructionCleanupLedger is renamed
ResourceConstructionCleanupLedger (exception-chain walking has nothing
to do with GL), and GlfwNativePlatformProbe moved out of the otherwise
GL-only GraphicalCapabilityRecord.cs into
GraphicalWindowBackendSelection.cs before the rest of that file was
deleted.
Test files with no surviving subject are deleted outright
(GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests,
PortalDepthShaderParityTests, TextureCacheBindlessTests,
TextRendererFailureSafetyTests, ClipFrameUploadTests, every
Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests);
others get their dead GL-only members trimmed while their live
assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now
reads GpuBindingModel.StorageClipRegions, the same binding index under
its new backend-neutral name; GpuResourceRetirementTransactionTests
drops its OpenGLGraphicsDevice-subclassing test double and the two GL
queue tests it existed for). EnvCellRendererTests' construction helper
now builds a real ObjectMeshManager via VulkanMeshPipelineDevice
instead of passing null through a null-forgiving operator, since the
RHI constructor never tolerated a null mesh manager and the old GL
constructor (which did) is gone.
Deferred to the next two steps, deliberately not touched here: the
Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl
(WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale
csproj comment (the package itself is still load-bearing —
TextureFormat and friends are used well beyond the deleted
ManagedGLUniformBuffer), and the CI/gate scripts.
Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors.
Tests: full-solution `dotnet test` green across every project
(App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all
others 100%); the 2 App.Tests names that flake under full-suite
parallel execution (#250-family, documented pre-existing) pass in
isolation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two changes, one measurement. The V6m smoke pair put GL versus Vulkan at
Holtburg at 18.52% of the frame differing at tolerance 2 with MSAA off. The same
stop on the same instrument now measures 9.05%, and the two populations these
address are gone from the difference map rather than merely smaller.
1. THE WORLD ATLASES WERE SAMPLED WITHOUT ANISOTROPY ON VULKAN, AND WITH THE
DEVICE MAXIMUM ON GL.
RhiWorldTextureArray -- the backend-neutral shared object/material atlas, and
the only IWorldTextureArray the Vulkan arm ever constructs -- registered its
clamp and repeat slots with GpuSamplerDescription.WorldClamp/WorldRepeat as
written, which carry MaxAnisotropy 1. The GL arm asks for the driver's own
GL_MAX_TEXTURE_MAX_ANISOTROPY twice over: ManagedGLTextureArray sets
GL_TEXTURE_MAX_ANISOTROPY on the image, and the two sampler objects its resident
bindless handles are built from (OpenGLGraphicsDevice.WrapSampler/ClampSampler)
set it again, which is the one that actually wins.
V6i-2 knew it was asking for 1 and said so in a comment -- "the world arm that
draws through these arrays is the next slice, and it is the one that can gate a
filtering change visually." That slice was V6j, the gate is V7, and this is it.
Retail settles the question rather than the GL arm settling it.
RenderDeviceD3D::SetDefaultD3DStates (0x005a3800) loops all sixteen sampler
stages and issues SetSamplerState(stage, 0xA, this->m_D3DCaps.MaxAnisotropy) at
0x005a4230. 0xA is D3DSAMP_MAXANISOTROPY and the argument is the device's
reported cap, not a setting -- so "as much anisotropy as this device has" is
retail's own rule, the GL arm is faithful to it, and asking for 1 diverged from
retail as well as from the shipping backend. No divergence-register row is owed
in either direction: this retires a Vulkan-only gap and lands on retail's value.
The fix asks for a ceiling rather than reading a limit back, because the pinned
RHI contract (plan section 3.3) carries no anisotropy field and is frozen. It
does not need one: VulkanGpuSampler already clamps MaxAnisotropy to
VkPhysicalDeviceLimits.maxSamplerAnisotropy, Vulkan guarantees that limit is at
least 16 wherever the samplerAnisotropy feature is supported -- which this
backend requires -- and 16 is where every desktop driver caps. The request and
the GL arm's read therefore land on the same number.
What it was worth, from the difference map at the same stop: the roof shingles
of both Holtburg cottages, which had been dense hatching across the whole
surface, and the stone courses of the near building are now black. Measured as
high-frequency energy (mean absolute neighbour difference, GL versus Vulkan) the
right-hand roof went from visibly blurred to a ratio of 0.999 and the wall to
1.023; every other textured region in the frame is between 0.99 and 1.02.
Grazing-angle surfaces are where anisotropy is the whole difference, which is
why a roof was the loudest thing in the frame.
2. THE SKY HAS TWO CLOCKS AND ONLY ONE OF THEM WAS PINNABLE.
ACDREAM_DAY_GROUP and the route's AcdreamCycleTimeOfDay presses pin the Dereth
date, which chooses the day group, the keyframe and the sun angle. The cloud
sheet does not read that clock: SkyRenderer accumulates TexVelocityX/Y against
DateTime.UtcNow minus its own construction time, by design, because retail's
clouds drift with real time regardless of the date. Two launches minutes apart
therefore cannot agree about where the clouds are no matter what the route does,
and the V6m smoke measured the cost -- 89% of its 18.52% sat in the top 240 rows.
ACDREAM_SKY_PHASE_SECONDS (RuntimeOptions.SkyAnimationPhaseSeconds ->
SkyRenderer.AnimationPhaseSecondsOverride) replaces that elapsed-seconds value
with a fixed one. Unset -- the default, and every ordinary run -- keeps the wall
clock, so nothing a user or the offline gate sees changes. The differential gate
forces it on both launches alongside MSAA and the day group; the offline gate
keeps its top-280 mask, because a same-commit GL pair still has the sun to
disagree about.
This is instrument determinism on the same footing as ACDREAM_DAY_GROUP, not a
workaround: it is one input to a UV offset, it is off by default, and no shipping
path reads it. The alternative on the table was -MaskTopPixels, which would have
permanently blinded the campaign's strictest instrument to the entire sky -- one
of the five surfaces the offline gate already cannot see. Rows 0-32 of the
Holtburg pair went from 23,090 differing pixels to 1,211, and what remains up
there is roof and portal rather than cloud.
WHAT THE SAME PAIR STILL SHOWS, unattributed and carried to the next commit: the
distant treeline, the player and the NPCs, and the animated portal. The portal is
phase and expected. The treeline is not filtering -- sharpness now matches within
5% and a shift search finds no sub-pixel offset -- and the two runs entered the
world at different last-logout positions (0xC95B0001 versus 0x09040008), so the
far-tier streaming history differed. That is the next thing to prove or refute.
Gates. Release build green. App tests 4,133 passed / 3 skipped against the
4,132/3 baseline (one new: the sky-phase parse). GL offline pixel gate against
the pre-change tree: 2.31e-05, 13 pixels of 563,200, inside the documented 9-31
band -- GL did not move. One offline Vulkan run with VK_LAYER_KHRONOS_validation
proven inserted by the loader: zero validation errors, zero warnings. Full
three-stop differential recorded at artifacts/v7-diff-c1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
V6i-2 cut IMeshPipelineDevice at the measured surface and proved the mesh
pipeline could be CONSTRUCTED without naming a backend. It said plainly what it
did not claim: "the mesh pipeline does not RUN on Vulkan. Its upload bodies are
still raw GL — GlobalMeshBuffer, the VAO/IBO construction, the layer transfers."
This moves them, and gives the interface its second implementation.
GlobalMeshBuffer takes GL?. The two backing stores were already IGpuBuffer
(V4b); what still needed a context was the vertex array and the attribute
pointers, which have no RHI verb because Vulkan bakes vertex input into the
pipeline. So a backend with none builds the stores and nothing else, publishes
0 for VAO/VBO/IBO, and publishes VertexStore/IndexStore — the same buffers,
named the way a pass encoder binds them. HasStores is the backend-neutral form
of the VAO != 0 readiness test the raw-GL draw paths make. Two bodies fork on
the context and nothing else does: InitBuffers skips the vertex array, and
CommitMigration skips the rebind — on the encoder arm the field swap IS the
atomic publication, because the next pass reads whatever the field then holds.
The store deletion likewise splits: GL keeps its immediate DeleteRetired,
because the arena's own flight gate has already proven no submitted frame can
reference the store, while the other arm has no second deferral to skip and
Dispose is its retirement-queued release.
ObjectMeshManager's RequireGl narrowed to the LEGACY per-mesh upload. Its three
call sites were one modern-path constructor argument and two bodies whose every
GL statement sits inside `if (!_useModernRendering)`. The constructor now hands
the arena the nullable context; the two bodies resolve one lazily inside the
legacy branch. That branch is unreachable in every shipping configuration —
missing bindless or draw-parameters throws at startup under the N.5 ship
amendment — so the accessor survives as the guard on dead code rather than as a
blocker, and it is deleted with that code.
VulkanMeshPipelineDevice is the second implementation, and it is four
properties and two no-ops. Two things about it are worth stating rather than
leaving to be inferred. HasBindless and HasOpenGL43 answer TRUE: their names are
GL-shaped because the seam was cut from a GL device, but what they gate is the
MODERN path — one shared arena, table texture indexing, multi-draw indirect —
which Vulkan supplies unconditionally and the capability gate rejects a device
for lacking, so answering false would disable the only path that exists.
HasPendingWork answers false because the GL device's queue exists to defer work
onto the thread holding the context, and Vulkan resource work is recorded into
the frame's command buffer or routed through the retirement queue.
WbMeshAdapter selects between them once, in the one place the mesh pipeline
still names a backend. The GL arm is unchanged, including the queue-drain
guarantee its construction rollback asserts.
So composition builds the mesh pipeline on BOTH arms, and NullWbMeshAdapter is
deleted — it existed for exactly the gap this closes, and the landblock spawn
ledger now registers against the real adapter. Streaming's publication into GPU
state stops being a no-op there: the Vulkan run below builds real render data,
including the [up-null] zero-vertex caching path.
Gates. Release build green. App tests 4,112 passed / 3 skipped, against a 4,109
baseline plus the three added here. Strict GL offline pixel gate against
579e0b7f: 4.44e-05 (25 differing pixels of 563,200), inside the documented 9-31
px control band and 22x under the 0.001 threshold. One offline Vulkan run with
VK_LAYER_KHRONOS_validation proven inserted by the loader (VK_LOADER_DEBUG=layer
reports `Insert instance layer "VK_LAYER_KHRONOS_validation"`): zero validation
errors, zero warnings, a captured frame, and no [shutdown] diagnostic on either
stream.
What this does NOT claim: nothing draws the world on Vulkan yet. The three
world renderers' submission arms, the two pass executors, and the pass-structure
merge are the next commit's.
No divergence-register row: no retail-facing behaviour changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan §5.5.10 recorded the blocker as a fact about types: "WbMeshAdapter owns an
OpenGLGraphicsDevice, so it is not constructible on Vulkan until slice V4t" —
which is the entire reason NullWbMeshAdapter exists. §5.5.12 item 6 then measured
how wide that dependency really is, and the answer is seven members out of a
760-line class: a GL context, the retirement queue, the shared instance VBO, and
two capability flags.
IMeshPipelineDevice is exactly that surface. OpenGLGraphicsDevice declares it and
every member already existed under a GL-specific name, so the shipping backend
executes not one changed statement — these are aliases, not behaviour.
Two casts moved, and they are what actually blocked construction:
- ObjectMeshManager downcast IGpuDevice to GlGpuDevice in its CONSTRUCTOR, so a
Vulkan-composed pipeline threw before running a statement. V4t put it there
because the class registered bindless handles itself; commit 2 moved that into
the array, leaving the field a pass-through for the raw-GL renderers' handle
table. The cast now lives on that one property and names the backend it was
composed against instead of reporting a failed cast.
- The atlas array factory is selected by IWorldTextureArrayFactory.For, which is
the one place the texture stack branches on a backend.
MeshPipelineDeviceSeamTests proves the decoupling rather than describing it: it
builds ObjectMeshManager against a device whose Gl is null, asserts it constructs,
asserts construction built no GL object, asserts the handle table refuses by name,
and asserts the factory picks the RHI arm. A reflection test pins the seam's
member set so a later slice cannot quietly widen it back out — the whole value
here is that it is narrow.
What this does NOT claim: the mesh pipeline does not RUN on Vulkan. Its upload
bodies are still raw GL — GlobalMeshBuffer, the VAO/IBO construction, the layer
transfers — and they now fail through one RequireGl() accessor that names the
slice that owns porting them, instead of failing at construction. WbMeshAdapter
still creates an OpenGLGraphicsDevice in its GL constructor, because there is no
second implementation to create yet. Those bodies are items 3–5 of §5.5.12's
remainder list, along with RetailPViewPassExecutor and the three world renderers'
submission arms.
§5.5.13 reports the whole of V6i-2 and the slice table gains its V6i row.
Gates: Release build; App tests 4,109 / 3 skips (the 4,086 baseline plus 23 across
the three commits); complete Release suite 9,172 / 5; strict GL offline pixel gate
vs 0ca802cd 1.60e-05 (9 px of 563,200 — the low end of the documented 9–31 px
control band, and fewer than a same-commit control has measured); GL connected
tools/run-repeat-connected-gate.ps1 -Runs 3 at 3/3 RENDERED on the desktop witness
and 3/3 on the client capture; one Vulkan composition-host run with
VK_LAYER_KHRONOS_validation proven inserted by the loader at zero errors, zero
warnings, no [shutdown] diagnostic, and a captured frame.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan §5.5.11 recorded what V4t deliberately left behind: it moved the table
ENTRY of every world texture to the device and kept CREATION with the caches,
because "creating world textures through IGpuTexture is real remaining work and
it belongs with the Vulkan world arm, which is the first thing that cannot use a
GL handle at all." §5.5.12 item 1 handed it forward and named the missing piece
exactly — "an ITextureArray implementation over IGpuTexture, not a codec",
because V6b's BlockCompressionCodec and BlockCompressionMipChain already supply
the BC chains. This is that work.
IWorldTextureArray is the seam, and the slot is what crosses it. Before this
commit ObjectMeshManager read BindlessWrapHandle/BindlessClampHandle off the
concrete GL array and interned them into the device table itself. A 64-bit
ARB_bindless_texture handle has no Vulkan spelling, so the array now answers the
question the caller was really asking — ResolveSlot(wrapping) — and each arm gets
there its own way: ManagedGLTextureArray makes the same idempotent interning call
one level down, and RhiWorldTextureArray returns a pair it registered at
construction. ReleaseTextureSlots replaces the snapshot dictionary the manager
kept for the same reason, and still runs only once physical retirement completes.
Which implementation exists is decided ONCE, by the IWorldTextureArrayFactory
composition builds — plan §3.1's no-runtime-fork rule. Everything above the seam
(capacity policy, slot allocation, ref counting, layer retirement, empty-atlas
eviction, and the whole of ObjectMeshManager's atlas policy) is written once and
branches on nothing.
Three things the RHI array does differently, each because the backends genuinely
differ rather than by choice: BC mip chains are CPU-built through
BlockCompressionMipChain, since Vulkan cannot blit into a compressed image, while
RGBA8 uses the device's blit; filtering lives in an immutable sampler rather than
a texture parameter, so both address modes are registered up front exactly as the
GL array holds two resident handles; and RGB8/A8/Rgba32f are refused at creation
with the reason named. A8 is the interesting refusal — the GL array serves it by
swizzling R into A, and a Vulkan swizzle lives in the image VIEW, which the pinned
GpuTextureDescription does not describe. A silent substitution would render wrong
and look like a shader bug.
TerrainAtlas gains the second construction path V6i drafted and reverted. The
decode is factored out and shared, so both arms read the same DATs, in the same
order, with the same resize-to-max policy; only the upload forks.
ICompositeTextureArrayBackend gains its RHI arm, which is four small methods
because that seam was already a seam.
The Vulkan arm is EXERCISED, not merely present. That is the whole reason the
V6i draft was reverted rather than landed — "built then reverted because nothing
exercised it" — and it is the same failure §5.5.12 measured twice in the
descriptor layouts. So the composition host now builds the real terrain atlas
through IGpuDevice.CreateTexture on the arm with no GL context, and creates and
releases one shared array of each format family plus one composite array at
startup. Creation only; nothing draws them. Releasing them in the same statement
covers one thing a retained bundle would not — that both slot pairs come back and
the images route through the retirement queue.
Gates: Release build; App tests 4,104 / 3 skips; strict GL offline pixel gate vs
0ca802cd 3.20e-05 (18 px of 563,200, inside the documented 9–31 px control band);
GL connected tools/run-repeat-connected-gate.ps1 -Runs 3 at 3/3 RENDERED on the
desktop witness AND 3/3 on the client capture; one Vulkan composition-host run
with VK_LAYER_KHRONOS_validation proven inserted by the loader at zero errors,
zero warnings, no [shutdown] diagnostic, and a captured frame. That run built
terrain-atlas 512x512x33 with 10 mip levels, terrain-alpha-atlas 512x512x8, RGBA8
64x64x32 (slots 3/4, 174,720 mip bytes blitted), BC1 64x64x32 (slots 5/6, 696 mip
bytes encoded) and composite 32x32x8 (slot 7).
One whole-suite run failed Issue181WallPressEquilibriumTests once; it passed
alone and did not recur in five further runs. Seven test classes mutate the same
process-global CameraDiagnostics switches with no xUnit collection isolation, and
this diff touches no camera, visibility or physics code. A separate run of the
UNCHANGED parent tree failed a different zero-allocation test, which is `#250`'s
documented class. Both are filed rather than attributed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rest of V4t. The composite, particle and shared-atlas texture paths now
hand out the device's GpuTextureSlot instead of a raw 64-bit
ARB_bindless_texture handle, and GroupKey, CachedBatch and ObjectRenderBatch
carry that slot. WbDrawDispatcher, EnvCellRenderer and ParticleRenderer retire
their interim GlBindlessHandleTable instances and share the device's one
table, exactly as V4t-1 did for terrain. Nothing about world submission
changes otherwise: these three renderers are still raw GL, still bind binding
9 themselves, and still draw the same geometry in the same order.
**What produces a slot now.** CompositeTextureArrayCache's GL backend interns
each array's handle when it makes it resident and retires the entry when it
makes it non-resident, so the pair is created and destroyed together and the
cache above it never learns a device exists — the fake backend its tests use
mints a stand-in slot. TextureCache.AcquireParticleTexture does the same for
the one-layer particle arrays it owns, including on its rollback path.
ObjectMeshManager registers each shared atlas's wrap/clamp handles at batch
upload; registration is idempotent by handle, so the many batches sharing an
atlas share its entry.
**Slot release is stricter than what it replaces, not looser.** The interim
tables never released anything — the class comment said so — and they grew
without bound. The device's table has a fixed 16,384-slot capacity, so an
unreleased entry is now a leak with an end. Every producer therefore retires
its entry: the composite backend at MakeNonResident, the particle backend at
MakeNonResident, and ObjectMeshManager when a retiring atlas's PHYSICAL
retirement completes — the point at which its handles are already non-resident
and its texture already deleted. That last one needs the handles snapshotted
at eviction, because ManagedGLTextureArray.Dispose zeroes its own copies as
its first act. Teardown deliberately does not release: the device is being torn
down alongside its callers, so there is nothing left to recycle a slot into,
and deferring work through a possibly-disposed retirement queue would turn a
clean shutdown into a throw.
**The default value became load-bearing, and that is the one real hazard here.**
BindlessTextureLocation could say "not resolved" with handle 0, because no
texture has handle 0. A slot index has no spare value — default(GpuTextureSlot)
is real slot 0 — so a positional record would have turned every
budget-rejected or still-uploading composite into a silent read of whichever
texture registered first. That is the magenta-placeholder failure shape one
layer down. The type is now a struct storing the slot one-based, so default IS
Unresolved, with a test pinning both halves: default is unresolved, and a
location naming slot 0 is resolved and distinguishable from it. Elsewhere the
sentinel is already exact — GpuTextureSlot.Unassigned is 0xFFFFFFFF, which is
common.glsl's ACDREAM_TEXTURE_NONE — so the classify path's "no texture yet"
test and the particle billboard's untextured branch are unchanged in meaning.
**GroupKey ordering is preserved because the key never ordered anything.**
Handle→slot is a bijection (the device interns one slot per resident handle),
so the same (entity, batch) pairs bucket together as before. The key reaches
equality, hashing and the scene-digest fingerprints — never a comparator:
opaque and translucent groups sort by cull mode then camera distance, the
delayed-alpha path by viewer distance then submission ordinal, and group
enumeration follows the persistent dictionary's insertion order, which a
changed hash does not disturb. The digests hash the slot index where they
hashed the handle; both sides of the render-shadow comparison compute them the
same way, so the value changing is invisible to it. Read
CompareOpaqueSubmissionOrder, CompareTransparentSubmissionOrder and
AlphaFingerprintComparer before doubting this — sort-order drift is a
pixel-visible regression class this project has hit, and it is why the check
was made before the retype rather than after.
**One visibility change, forced rather than chosen.** BindlessTextureLocation
was public and now holds an internal contract type, so it is internal;
ObjectRenderBatch.TextureSlot is internal on an otherwise public class for the
same reason. Nothing outside this assembly and its InternalsVisibleTo test
assemblies named either.
**SkyRenderer keeps its interim table**, and the report should say why: the
sky's textures are minted by SkyRenderer itself from TextureCache's raw GL
texture names, which this slice does not retype, so it would be the one
consumer registering handles it produced — a different shape from the world
stack. The offline gate also masks the sky band, so the one automated
instrument here cannot see a sky regression. V4f owns that renderer.
**Gates.** GL offline pixel gate vs cb2a70b8, measured twice: 31 and 22
differing pixels of 563,200 (5.50e-05, 3.91e-05). The first is above the
plan's documented 15-23 px band, so a control was measured rather than
assumed: two same-commit captures at this tree differ by 19 px, and — the
decisive number — a capture at V4t-1 and a capture at this commit differ by
9 px, fewer than the same-commit control. Maximum channel delta is 41-52 in
every pair including the controls, i.e. the differing pixels are drawn from
one flickering population, not from moved geometry. tools/run-repeat-connected-gate.ps1
-Runs 3: 3/3 RENDERED on both the desktop witness and the client capture. One
Vulkan composition-host run with VK_LAYER_KHRONOS_validation proven inserted
by the loader: zero errors, zero warnings, converged ownership ledger. App
tests 4,077 / 3 skips and the complete Release suite 9,140 / 5 — both the
4,075 and 9,138 baselines plus the two tests added here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two renderers that draw everything in the world - WbDrawDispatcher for
entities and EnvCellRenderer for dungeon shells - now record through
IGpuPassEncoder instead of calling GL directly. They share mesh_modern and its
binding layout, which is why they had to move together.
What moved. Every per-frame upload became an IGpuFrame.AllocateRing slice:
instance transforms, batch metadata, clip slots, global lights, per-instance
light sets, indoor flags, opacity, selection lighting, and the indirect command
array. That retires both renderers' DynamicBufferSet pools outright. Those pools
existed so a second Draw within one frame could not overwrite an earlier draw's
still-pending data; the frame ring gives that structurally, because every
allocation within a frame is distinct memory that lives until the frame retires.
DynamicBufferSetCount now reports 0 for both, which is the truth rather than a
silent change - they own no such pool any more.
The imperative Enable/Disable/BlendFunc/DepthMask brackets around the two
multi-draw passes became pipeline variants: five for the dispatcher (opaque,
opaque+alpha-to-coverage, and the three retail blends) and three for the cell
shells. Cull mode and front face stay dynamic per MDI run, exactly where
ApplyCullMode and SetCullMode set them, because core Vulkan 1.3 makes those
dynamic and blend and alpha-to-coverage not. ApplyRetailBlend is gone: its three
cases are now three pipelines, including the inverse-alpha one that
GpuBlendMode.InverseAlpha was added for. uViewProjection, uDrawIDOffset,
uLightingMode, uRenderPass and uLightDebug became fields of the shared
GpuPushConstants block. Issue #52's per-pass batch offset is unchanged - the
draw index still resets per indirect call, and Vulkan's gl_DrawID resets
identically.
Depth compare is baked as GL_LESS, not the contract's LessOrEqual default. The
world frame runs under GL_LESS (RenderFrameGlStateController.RestoreFrameDefaults)
and neither renderer ever called glDepthFunc, so both inherited it; baking
LessOrEqual would have changed which of two coplanar retail surfaces wins.
Two uniform writes were dropped rather than ported, and both are no-ops today:
uFilterByCell and uHighlightColor are declared in neither mesh_modern stage, so
they resolved to location -1. Saying so here rather than letting them vanish.
GPU timing moved to IGpuPassEncoder.BeginTimerScope. The [WB-DIAG] median/p95
window is still fed and still measures opaque + transparent time for the
dispatch, but the sample now comes from IGpuTimerPool.TryResolve - the most
recent retired result - instead of a hand-rolled 3-deep query ring read at N-3.
A sample can therefore repeat when the GPU has not finished a newer query,
where the old code dropped it. The pool also owns the #125 "never read a query
that was never begun" guard now. Diagnostic-only, and flagged rather than left
to be discovered.
Three things deliberately did NOT move, per the campaign doc's section 5.3.
The interim GlBindlessHandleTable stays; both renderers still intern raw
bindless handles and now bind that table through the encoder as an ordinary
IGpuBuffer at binding 9. Retiring it is slice V4t, because the handles are
produced by the texture caches and carried through GroupKey and CachedBatch.
ClipFrame's region buffer (binding 2) and the SceneLighting UBO stay globally
bound by raw GL, because terrain and the viewport/portal renderers read the same
bindings and are raw GL until V4d/V4g. EnvCellRenderer's glMemoryBarrier stays a
raw call: it has no RHI verb, and it guards incoherent shader writes that
acdream does not make, so it was already a no-op against client-side uploads.
RetailAlphaQueue, the GroupKey bucketing, the front-to-back and translucent sort
orders, and every other piece of CPU fidelity logic are untouched. The deferred
alpha payload is still prepared exactly once per sorted alpha scope: a ring
allocation cannot outlive its frame as a ref struct, but its buffer, offset and
size can be stored, so DrawPreparedAlphaBatch binds the same bytes many times
without recopying them.
Two supporting changes outside the two renderers, both flagged.
GlGpuDevice.BeginPass no longer binds framebuffer 0 for a null colour target; it
leaves the binding alone and only binds an explicitly named target. A null target
means "whatever the spine bound", which is what GpuPassDescription's own remarks
describe when they say clears and framebuffer management stay with the spine
until V4h. Forcing 0 would have been fatal here and invisible to this gate:
PrivateEntityViewportRenderer binds its offscreen FBO and then calls
WbDrawDispatcher.Draw, as does PortalTunnelPresentation, so the paperdoll and
creature-appraisal viewports would have rendered to the backbuffer and left their
textures empty - and the offline gate does not cover those viewports. This is the
same class of fix as the ambient-capability save/restore in GlGpuPassEncoder.
GlGpuDevice.CreatePipeline now splices the slice-V2 shared preamble
(Shaders/common.glsl) into every pipeline, reusing Shader.InjectPreamble - widened
from private to internal - so a pipeline-compiled program and a Shader-compiled
one are built from byte-identical sources. mesh_modern requires it: the preamble
declares the binding-9 table and defines ACDREAM_TEXTURE_HANDLE, without which
the world shaders do not compile. Shaders that reference none of it gain an
unused SSBO declaration and two macros; every shader in the tree is #version 430
core, so that is always legal.
Both renderers keep their trailing raw-GL disable block after the pass closes.
The encoder's Dispose restores the capability state that was ambient on ENTRY,
which is not the state these renderers used to leave behind - terrain, sky and
particles are still raw GL and still inherit what the previous renderer left, so
the exit state is reasserted explicitly. It goes at V4h with the last raw-GL
renderer.
A defect caught in review and fixed before the gate: each IGpuPipeline owns its
own vertex array, and vertex attribute pointers plus the index binding are
vertex-array state, so switching blend variants mid-pass silently dropped the
mesh source while the storage bindings survived. Every pipeline switch now goes
through one helper that re-binds the arena.
Gates. Release build green with TreatWarningsAsErrors. App tests 3,844 passed /
3 skipped, stable over four consecutive runs, against a 3,843 baseline plus the
InverseAlpha contract test. Offline pixel gate against 111e7236: 20 differing
pixels of 563,200 compared (fraction 3.55e-05), against a same-commit control
captured immediately afterwards of 17 - indistinguishable from capture noise and
28x under the 0.001 threshold. The gate run's client log has zero exceptions and
an empty stderr.
Coverage gap, stated rather than assumed: the offline gate's scene is a fixed
outdoor view, so it exercises WbDrawDispatcher heavily and EnvCellRenderer not at
all. Dungeon interiors, the paperdoll and appraisal viewports, and portal transit
need a user visual check before this slice is considered proven.
No divergence-register row: this slice changes no retail-facing behaviour.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Moves the mesh/EnvCell draw path's per-batch texture representation from a
64-bit ARB_bindless_texture handle to a small integer table index, entirely
on the still-shipping GL backend, with zero pixel change. This is the CPU-side
half of the eventual Vulkan descriptor-array indexing model: a table index is
the backend-neutral form (Vulkan indexes a descriptor array with it directly),
while a raw bindless handle is GL-only. Landing the data-model change now, on
GL, under a strict self-differential pixel gate, keeps it separate from V4c's
much larger RHI-plumbing change (see docs/plans/2026-07-27-vulkan-campaign.md
section 5.2 for why the table cannot be device-owned yet).
Mechanism: mesh_modern.vert's BatchData struct carries `textureIndex` (a slot)
instead of `textureHandle` (uvec2); the vertex shader looks the slot up in a
new binding=9 storage buffer (GpuBindingModel.StorageTextureTable) and passes
the reconstructed uvec2 handle to the fragment shader exactly as before, so
mesh_modern.frag needed no change at all beyond the UBO-set macro below. The
16-byte std430 stride is unchanged (GpuBindingModel.GpuBatchDataStrideBytes);
textureLayer/flags keep their offsets, so every existing CPU writer's layout
is untouched.
The handle->slot table (GlBindlessHandleTable, new, pure C#) is owned
separately by WbDrawDispatcher and EnvCellRenderer rather than shared through
a single TextureCache-owned instance: EnvCellRenderer never had a TextureCache
dependency, and nothing requires index agreement between renderers since each
rebinds its own binding=9 buffer immediately before its own draw call. This
avoided threading a new constructor parameter through EnvCellRenderer (and its
six test call sites) for no behavioral benefit. TextureCache and
CompositeTextureArrayCache turned out to need no changes at all: they only
ever produce raw ulong handles, and that production path is unaffected -
the new indirection is entirely a WbDrawDispatcher/EnvCellRenderer-side
concern, added exactly where each already assembles its per-batch GPU struct
(ToInput, the copy-back loop, PrepareDeferredAlphaDraws for the
RetailAlphaQueue path, and EnvCellRenderer's ModernBatchData construction).
The table itself is a single non-ring buffer (unlike the per-frame
triple-buffered SSBOs) because a genuinely new handle is rare - new dat
surfaces/composite overrides, not every frame - so it flushes only when
GlBindlessHandleTable.Dirty is set, mirroring how the existing texture caches
already upload infrequently.
Shader-side, introduced Rendering/Shaders/common.glsl as the shared preamble
GL has no #include for: Shader.cs gained an `includeCommonPreamble` overload
that splices the file's text in after the leading #version/#extension block
(GLSL requires #version first). It declares the binding=9 table plus the
ACDREAM_TEXTURE_HANDLE(idx) lookup macro, and a scaffolding ACDREAM_UBO_SET
macro (a no-op under GL today, redefined to `set = 1,` when the Vulkan
toolchain compiles this same source at V6+, per the campaign doc's set-1 UBO
note) applied to both SceneLighting UBO declarations now so no later slice
needs to touch them again.
Tests: WbDrawDispatcherIndirectBuilderTests updated for the renamed
IndirectGroupInput/BatchDataPublic fields; new ModernBatchDataLayoutTests
(mirrors ClipFrameLayoutTests' role, but for EnvCellRenderer's GPU struct) and
GlBindlessHandleTableTests (pure-CPU allocator behavior, including the
zero-handle case, which is registered like any other handle rather than
special-cased, since that's what reproduces the pre-V2 sampling result
bit-for-bit).
Gate: dotnet build -c Release green, dotnet test
tests/AcDream.App.Tests -c Release green (3843 passed / 3 skipped, +9 over
the 3834/3 baseline), and tools/run-offline-pixel-gate.ps1 passed with a
2.84e-05 differing-pixel fraction against the parent commit - within the
documented ~33x same-commit noise margin. No divergence-register row: this
introduces no retail behavior deviation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Source composite warmup from the canonical published destination neighborhood, including quiesced static and live projections, instead of rescanning the retained Far-tier world after every membership edge.
This lifecycle correction applies to both draw paths, preserving ef1d263337 as the sole G4 visual rollback.
Co-authored-by: OpenAI Codex <codex@openai.com>
Scan published destination candidates under a separate bounded budget from expensive mesh and composite preparation so large retained worlds cannot exhaust the reveal window before candidate discovery completes.
The correction is valid on both the retained and pre-cutover draw paths, so the G4 visual rollback remains the single commit ef1d263337.
Co-authored-by: OpenAI Codex <codex@openai.com>
Retain appearance classification per projection while refreshing transforms, clip slots, lights, selection lighting, and group instance payloads each frame. Incomplete resources and active animation remain on the live classification path, and only an exact full packed comparison acknowledges scene dirtiness.
The production dispatcher forces transparent deferral off when no top-level VAO was resolved, even if the frame alpha queue is active. Carry the same per-draw VAO sentinel into the compare-only packed classifier so its empty-submission state is exact rather than sampling only the later queue state.
Build a compare-only dispatcher classifier from RenderFrameView and compare complete opaque, alpha, selection, clip, light, texture, transform, and draw payloads against the accepted path. Preserve retail's stable equal-CYpt ordering with explicit draw-local submission ordinals so material-group history cannot affect alpha ties.
Separate loaded spatial residency from the world presentation gate so destination live objects acquire mesh owners behind portal space while drawing and simulation stay quiesced. Prevent unowned CPU-cache hits from recreating stale GPU staging work.
Co-authored-by: Erik Nilsson <erikn@users.noreply.github.com>
Retain composite candidate progress across live membership generations and run a stable follow-up pass after churn, preventing ACE object-stream updates from resetting portal readiness forever.
Co-authored-by: Erik Nilsson <erikn@users.noreply.github.com>
An adversarial performance review found our own instruments cannot
measure the project's own performance gates:
- FrameProfiler aggregated CPU/GPU/alloc/stage samples into ~5-second
windows and reset the ring buffers after each report, so route-wide
p50/p95/p99 distributions across a whole soak could not be
reconstructed after the fact. ACDREAM_FRAME_HISTORY=<path> now opts
into a separate per-frame history (one record per frame, ~72
bytes/record, accumulated in memory with zero frame-thread I/O) that
a shutdown-only Dispose() writes as CSV. The aggregated [frame-prof]
report format and its existing metrics are unchanged.
- The canonical checkpoint JSON tracked cache residency (entry/byte
counts) but never LOH size/fragmentation, process-wide allocated
bytes, or cache hit/miss/eviction traffic — a committed audit JSON
showed 65% LOH fragmentation that no tracked instrument recorded,
and "does a revisit portal hit or miss the caches" was unanswerable
from an artifact alone. WorldLifecycleResourceSnapshot now carries
loh_size_bytes/loh_fragmentation_bytes (GCMemoryInfo.GenerationInfo
index 3), process_total_allocated_bytes (GC.GetTotalAllocatedBytes),
and Interlocked hit/miss/eviction counters for the CPU mesh cache,
decoded-texture cache, and the four bounded DAT-object caches
(portal/cell/highRes/language, aggregated).
- run-connected-r6-soak.ps1 unconditionally forced
ACDREAM_UNCAPPED_RENDER=1 with no capped mode, while its sibling
lifecycle-gate script correctly gated it behind a switch. Added
-Uncapped (default capped, matching the sibling script's pattern),
fixed the stationary dwell (12s -> 26s, past the 25s
LiveEntityLivenessController deadline the adjacent comment already
cited), and now write an env-disclosure.json into the automation
artifact directory before every launch listing every ACDREAM_* var
the script sets plus -Uncapped, since the prior audit could only see
ACDREAM_DUMP_MOVE_TRUTH and nothing else was ever recorded anywhere.
Cache counters are wired via the existing composition path
(ObjectMeshManager already owns the CPU mesh cache and the mesh
extractor directly; content.Dats is threaded into
WorldLifecycleResourceSnapshotSource the same way every other
composition consumer receives it). The DAT-object cache lives behind
IDatReaderWriter, a third-party interface from the DatReaderWriter
package that cannot be extended; RuntimeDatCollection (the one
production implementation) exposes the aggregate stats directly and a
pattern match reads them, degrading to zero for any test double —
no new static registry was introduced (GpuMemoryTracker remains the
one precedented process-wide static).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 1da2c33c875b41fa383dd79694ee2765f0e21896)
PrepareRenderBatches rebuilt the full EnvCell visibility snapshot every
frame any indoor/building root was resolved: a Parallel.ForEach dispatch
over every GpuReady EnvCell landblock (with per-landblock locks), a fresh
outer dictionary plus one fresh inner dictionary per visible cell, a new
snapshot object, and a complete transparency rescan - all while standing
perfectly still. The NeedsPrepare flag existed since Phase A8 as the
intended rebuild gate but was never read by production code.
This wires the gate on the snapshot's actual inputs:
- landblock commits/removals (the existing NeedsPrepare flag),
- the visible-cell filter (content-compared; the caller reuses one
scratch HashSet across frames),
- the trim window (center/radius),
- mesh render-data availability - new
ObjectMeshManager.RenderDataAvailabilityVersion, bumped at publish,
pending-release hide, release completion, and teardown, because the
snapshot bakes per-cell transparency from TryGetRenderData and a
late-arriving transparent shell must reclassify its cell,
- the camera: eye position under a 1 mm ABSOLUTE epsilon (swallows the
documented ~36 um rest jitter, dirties on any real movement; the VP
translation row scales with AC's ~5e4 world coordinates where a
relative tolerance would mask sub-meter motion) plus rows 1-3 of
view*projection (position-independent rotation x projection) under
relative 1e-5.
Skipping is pool-safe: RenderCore re-anchors _poolIndex to the active
snapshot's PostPreparePoolIndex on every call, so consecutive Renders
without an intervening Prepare reuse scratch lists past the snapshot's
owned region exactly as within-frame passes already do. The empty-filter
branch is now also idempotent instead of allocating a fresh empty
snapshot per frame. Render still receives the current frame's
view-projection every frame (the U.4 stale-matrix rule) - only the
snapshot rebuild is gated.
Pixels must be identical; needs the standard user visual pass (dungeon +
town-near-buildings) before the change is considered accepted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace scheduler-quantized software sleeps with a reusable Windows high-resolution deadline timer, expose pacing in the frame profiler, and make shutdown wake every persistent mesh worker without losing the shared signal.
Preserve retail alpha order while using a stable radix, skip duplicate deferred-alpha SSBO packing, pack light sets, cache static selection descriptors, and retire historical material groups at the whole-frame boundary. The fixed dense-Caul sample improved from roughly 9-12 ms CPU to 5.3-6.2 ms without reducing visual quality.
Release build succeeds with zero warnings and all 6,300 tests pass with five intentional skips. Three independent retail, architecture, and adversarial reviews are clean; the post-review connected route remains pending because local ACE is offline.
Co-authored-by: OpenAI Codex <codex@openai.com>