diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index d9702b3c..c910eabd 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -9415,6 +9415,8 @@ public sealed class GameWindow : IDisposable { var slice = sliceCtx.Slice; bool scissor = BeginDoorwayScissor(true, slice.NdcAabb); + if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeClipRouteEnabled) + EmitClipRouteScissorProbe(scissor, slice.NdcAabb); _gl!.BindBufferBase(BufferTargetARB.UniformBuffer, ClipFrame.TerrainClipUboBinding, _clipFrame!.TerrainUbo); @@ -9600,6 +9602,30 @@ public sealed class GameWindow : IDisposable _glStateStable = 0; } + // §4 flap [clip-route-scis] probe (2026-06-10, throwaway): the ACTUAL GL scissor state + // the landscape pass (sky + terrain + outdoor entities + the player) draws under, read + // back right after BeginDoorwayScissor. The whole pass is scissored to slice.NdcAabb — + // if the box reads doorway-sized here, the full-world flap is the scissor by + // construction, no RenderDoc needed. Print-on-change. + private string? _lastClipRouteScisSig; + private long _clipRouteScisSeq; + + private void EmitClipRouteScissorProbe(bool applied, System.Numerics.Vector4 ndcAabb) + { + var gl = _gl; + if (gl is null) return; + Span sbox = stackalloc int[4]; + gl.GetInteger(GetPName.ScissorBox, sbox); + bool enabled = gl.IsEnabled(EnableCap.ScissorTest); + string sig = System.FormattableString.Invariant( + $"applied={(applied ? 1 : 0)} scis={(enabled ? 1 : 0)} box=({sbox[0]},{sbox[1]},{sbox[2]},{sbox[3]}) ndc=({ndcAabb.X:F3},{ndcAabb.Y:F3},{ndcAabb.Z:F3},{ndcAabb.W:F3})"); + _clipRouteScisSeq++; + if (sig == _lastClipRouteScisSig) + return; + _lastClipRouteScisSig = sig; + Console.WriteLine($"[clip-route-scis] n={_clipRouteScisSeq} {sig}"); + } + private void EnableClipDistances() { for (int i = 0; i < ClipFrame.MaxPlanes; i++) diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index 5bf7bd45..c2933870 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -208,11 +208,15 @@ public sealed class RetailPViewRenderer if (clipAssembly.OutsideViewSlices.Length == 0) return; + int probeSliceIndex = 0; foreach (var slice in clipAssembly.OutsideViewSlices) { _clipFrame.SetTerrainClip(slice.Planes); UploadClipFrame(ctx.SetTerrainClipUbo); _entities.SetClipRouting(clipAssembly.CellIdToSlot, slice.Slot, outdoorVisible: true); + if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeClipRouteEnabled) + EmitClipRouteProbe(clipAssembly, slice, probeSliceIndex); + probeSliceIndex++; ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(slice, partition.Outdoor)); } @@ -222,6 +226,91 @@ public sealed class RetailPViewRenderer UseIndoorMembershipOnlyRouting(); } + // §4 flap [clip-route] probe state (2026-06-10, throwaway): print-on-change signature + + // monotonic sequence so held-flap vs healthy frames diff cleanly in one capture. + private string? _lastClipRouteSig; + private long _clipRouteSeq; + private readonly List _clipRouteCellKeys = new(); + + // §4 flap apparatus (2026-06-10): the decisive probe between the surviving suspects + // (handoff 2026-06-09 §1). Emits the EXACT clip inputs the landscape pass draws under: + // the outside slice's slot + NDC AABB + planes (CPU side), the region-SSBO bytes decoded + // at that slot (what mesh_modern.vert reads for routed instances), the terrain-UBO head + // (what terrain/sky gate against), and the CellIdToSlot routing table. Fires AFTER + // SetTerrainClip + UploadClipFrame + SetClipRouting, BEFORE DrawLandscapeSlice — so the + // printed bytes are exactly what this slice's draws consume. + private void EmitClipRouteProbe(ClipFrameAssembly clipAssembly, ClipViewSlice slice, int sliceIndex) + { + var sb = new System.Text.StringBuilder(256); + sb.Append(System.FormattableString.Invariant( + $"slice={sliceIndex}/{clipAssembly.OutsideViewSlices.Length} slot={slice.Slot}")); + sb.Append(System.FormattableString.Invariant( + $" ndc=({slice.NdcAabb.X:F3},{slice.NdcAabb.Y:F3},{slice.NdcAabb.Z:F3},{slice.NdcAabb.W:F3})")); + sb.Append(System.FormattableString.Invariant($" planes={slice.Planes.Length}[")); + for (int i = 0; i < slice.Planes.Length; i++) + { + var p = slice.Planes[i]; + if (i > 0) sb.Append(' '); + sb.Append(System.FormattableString.Invariant($"({p.X:F3},{p.Y:F3},{p.Z:F3},{p.W:F3})")); + } + + // CellIdToSlot sorted by cell id so dictionary enumeration order can't fake a change. + sb.Append("] cells={"); + _clipRouteCellKeys.Clear(); + foreach (uint key in clipAssembly.CellIdToSlot.Keys) + _clipRouteCellKeys.Add(key); + _clipRouteCellKeys.Sort(); + for (int i = 0; i < _clipRouteCellKeys.Count; i++) + { + if (i > 0) sb.Append(','); + sb.Append(System.FormattableString.Invariant( + $"0x{_clipRouteCellKeys[i]:X8}:{clipAssembly.CellIdToSlot[_clipRouteCellKeys[i]]}")); + } + sb.Append('}'); + + // Region-SSBO content decoded at the routed slot, from the packed bytes UploadClipFrame + // just uploaded — slot stride 144: count uint at +0, planes[8] at +16. + var rb = _clipFrame.RegionBytesForTest; + int off = slice.Slot * ClipFrame.CellClipStrideBytes; + if (off >= 0 && off + ClipFrame.CellClipStrideBytes <= rb.Length) + { + uint ssboCount = System.BitConverter.ToUInt32(rb.Slice(off, 4)); + sb.Append(System.FormattableString.Invariant($" ssbo[{slice.Slot}]: n={ssboCount}")); + int planeN = (int)System.Math.Min(ssboCount, (uint)ClipFrame.MaxPlanes); + for (int i = 0; i < planeN; i++) + { + int po = off + ClipFrame.CellClipPlanesOffset + i * 16; + float px = System.BitConverter.ToSingle(rb.Slice(po, 4)); + float py = System.BitConverter.ToSingle(rb.Slice(po + 4, 4)); + float pz = System.BitConverter.ToSingle(rb.Slice(po + 8, 4)); + float pw = System.BitConverter.ToSingle(rb.Slice(po + 12, 4)); + sb.Append(System.FormattableString.Invariant($" ({px:F3},{py:F3},{pz:F3},{pw:F3})")); + } + } + else + { + sb.Append(System.FormattableString.Invariant( + $" ssbo[{slice.Slot}]: OUT-OF-RANGE len={rb.Length}")); + } + + // Terrain-UBO head as uploaded (std140: int count at +0, planes[8] at +16). + var tb = _clipFrame.TerrainBytesForTest; + int uboCount = System.BitConverter.ToInt32(tb.Slice(0, 4)); + float u0 = System.BitConverter.ToSingle(tb.Slice(16, 4)); + float u1 = System.BitConverter.ToSingle(tb.Slice(20, 4)); + float u2 = System.BitConverter.ToSingle(tb.Slice(24, 4)); + float u3 = System.BitConverter.ToSingle(tb.Slice(28, 4)); + sb.Append(System.FormattableString.Invariant( + $" ubo: n={uboCount} p0=({u0:F3},{u1:F3},{u2:F3},{u3:F3})")); + + string sig = sb.ToString(); + _clipRouteSeq++; + if (sig == _lastClipRouteSig) + return; + _lastClipRouteSig = sig; + Console.WriteLine($"[clip-route] n={_clipRouteSeq} {sig}"); + } + private void DrawExitPortalMasks( IRetailPViewCellDrawCallbacks ctx, PortalVisibilityFrame pvFrame, diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index c2f3fbba..2f5a16f9 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -343,6 +343,52 @@ public sealed unsafe class WbDrawDispatcher : IDisposable _outdoorVisible = false; } + // §4 flap [clip-route-disp] probe state (2026-06-10, throwaway): print-on-change + // signature + monotonic sequence + reusable histogram. See RenderingDiagnostics + // .ProbeClipRouteEnabled for the full probe contract. + private string? _lastClipRouteDispSig; + private long _clipRouteDispSeq; + private readonly SortedDictionary _clipRouteHist = new(); + + // §4 flap apparatus (2026-06-10): per-slot instance histogram as staged for binding=3. + // grp.Slots is laid out 1:1 with grp.Matrices (binding=0), so this IS the slot content + // the GPU reads per instance — if outdoor instances land on the wrong slot (or vanish + // into cullEnt) when the building flood merges, this line shows it directly. + private void EmitClipRouteDispatchProbe(int culledEntities) + { + _clipRouteHist.Clear(); + int total = 0; + foreach (var grp in _groups.Values) + { + var slots = grp.Slots; + for (int i = 0; i < slots.Count; i++) + { + _clipRouteHist.TryGetValue(slots[i], out int c); + _clipRouteHist[slots[i]] = c + 1; + total++; + } + } + + var sb = new System.Text.StringBuilder(128); + sb.Append(System.FormattableString.Invariant( + $"outdoorSlot={_outdoorSlot} outdoorVis={(_outdoorVisible ? 'Y' : 'n')} inst={total} cullEnt={culledEntities} slots={{")); + bool first = true; + foreach (var kv in _clipRouteHist) + { + if (!first) sb.Append(','); + first = false; + sb.Append(System.FormattableString.Invariant($"{kv.Key}:{kv.Value}")); + } + sb.Append('}'); + + string sig = sb.ToString(); + _clipRouteDispSeq++; + if (sig == _lastClipRouteDispSig) + return; + _lastClipRouteDispSig = sig; + Console.WriteLine($"[clip-route-disp] n={_clipRouteDispSeq} {sig}"); + } + // Phase U.4 CULL sentinel returned by ResolveEntitySlot: the entity's instances // are dropped entirely (not emitted into the binding=0 instance buffer NOR the // binding=3 slot buffer), matching the existing frustum / visible-cell cull. @@ -752,6 +798,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable uint? populateEntityId = null; uint populateLandblockId = 0; + // §4 flap [clip-route-disp] probe (2026-06-10, throwaway): entities dropped by + // ResolveSlotForFrame's CULL sentinel this Draw. One increment per culled entity — + // cheap enough to count unconditionally; emission below is probe-gated. + int probeCulledEntities = 0; + // Tier 1 cache (#53) — fast-path one-shot tracker. The cache stores a // FLAT list of batches across all MeshRefs of an entity, so a single // ApplyCacheHit call already drew every batch. _walkScratch yields @@ -844,6 +895,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable (_currentEntitySlot, _currentEntityCulled) = ResolveSlotForFrame( _clipRoutingActive, entity.ServerGuid, entity.ParentCellId, _cellIdToSlot, _outdoorSlot, _outdoorVisible); + if (_currentEntityCulled) + probeCulledEntities++; } prevTupleEntityId = entity.Id; @@ -1067,6 +1120,15 @@ public sealed unsafe class WbDrawDispatcher : IDisposable // null) or when no entities walked at all. FinalFlushPopulate(populateEntityId, populateLandblockId, _cache, _populateScratch); + // §4 flap [clip-route-disp] probe (2026-06-10, throwaway): the per-slot instance + // histogram exactly as it will be uploaded to binding=3 (grp.Slots) plus the + // culled-entity count. Routed draws only (the landscape pass under DrawInside) so the + // unrouted per-cell bucket draws don't oscillate the print-on-change signature. + // Emitted BEFORE the anyVao / totalInstances early-outs so an all-culled frame still + // reports (inst=0). + if (RenderingDiagnostics.ProbeClipRouteEnabled && _clipRoutingActive) + EmitClipRouteDispatchProbe(probeCulledEntities); + // Nothing visible — skip the GL pass entirely. if (anyVao == 0) { diff --git a/src/AcDream.Core/Rendering/RenderingDiagnostics.cs b/src/AcDream.Core/Rendering/RenderingDiagnostics.cs index 911b20ce..46eb3c64 100644 --- a/src/AcDream.Core/Rendering/RenderingDiagnostics.cs +++ b/src/AcDream.Core/Rendering/RenderingDiagnostics.cs @@ -155,6 +155,23 @@ public static class RenderingDiagnostics public static bool ProbeGlStateEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_PROBE_GLSTATE") == "1"; + /// + /// §4 outdoor full-world flap apparatus (2026-06-10) — the decisive probe between the two + /// surviving suspects (handoff 2026-06-09 §1): (a) per-instance clip-slot routing under + /// outdoor roots, (b) terrain/sky UBO content at draw time — plus the landscape-pass scissor + /// box as a third ground truth. When true: RetailPViewRenderer.DrawLandscapeThroughOutsideView + /// emits one [clip-route] line (print-on-change) with the outside slice's slot + NDC + /// AABB + planes, the CellIdToSlot routing table, the region-SSBO bytes decoded at the routed + /// slot, and the terrain-UBO head as uploaded; WbDrawDispatcher.Draw emits one + /// [clip-route-disp] line (print-on-change, routed draws only) with the per-slot + /// instance histogram exactly as uploaded to binding=3 plus the culled-entity count; and + /// GameWindow.DrawRetailPViewLandscapeSlice emits one [clip-route-scis] line + /// (print-on-change) with the ACTUAL GL scissor enable + box the landscape pass draws under. + /// Throwaway apparatus — strip once §4 ships. Initial state from ACDREAM_PROBE_CLIPROUTE=1. + /// + public static bool ProbeClipRouteEnabled { get; set; } = + Environment.GetEnvironmentVariable("ACDREAM_PROBE_CLIPROUTE") == "1"; + /// /// Bounded-propagation port apparatus (2026-06-08). When true, PortalVisibilityBuilder.Build emits /// one [portal-churn] summary line per call: per-cell pop count (re-pops = churn), total re-enqueues,