T1 (fused BR-2/3): retail frame order - dynamics last, punch+seal, shell chop deleted
The complete retail drawing order in one installment (per the amended plan: every installment is a COMPLETE retail behavior - the half-ported punch of88be519is re-landed here WITH the ordering that makes it correct): static world (sky/terrain/weather/shells/scenery) -> aperture depth writes (interior SEAL at true depth / outdoor+look-in PUNCH to far-Z; PortalDepthMaskRenderer, DrawPortalPolyInternal Ghidra 0x0059bc90) -> interior cells WHOLE, far-to-near, drawn once (DrawCells Loop 2, Ghidra 0x005a4840; use_built_mesh pc:427905) -> per-cell STATIC object lists -> ALL dynamics LAST (DrawDynamicsLast), depth-tested, never hard-clipped InteriorEntityPartition: new contract - every ServerGuid != 0 entity goes to Dynamics regardless of cell (indoor/outdoor/unresolved/hidden); ByCell carries only dat-baked indoor statics of visible cells; Outdoor renamed OutdoorStatic. Fixes the audit's livedynamic-invisible-under-interior-roots divergence as a side effect (live entities are never dropped by the visibility set; culling is T3's viewcone). DELETED (retail has no counterpart): the gl_ClipDistance shell chop (927fd8fenable +9ce335eoutdoor scoping + UseShellClipRouting + the per-slice shell loop + clipShells param) - retail never clips cell geometry; aperture exactness = punch/seal + z-buffer + this order. The old per-slice scissored AABB depth clear is replaced by retail's single gated full clear (ClearDepthForInterior). The interior-root LiveDynamic top-up draw and the look-in's dynamics involvement are gone (one last pass, no double-draws). Closes at the T5 gate (expected): #114 (chop deleted), the char-eaten-by- doorway regression (ordering), outdoor interiors-through-doorways (punch); #108's render half (seal) - its membership half stays re-attributed. Suites: build green, App 226 green (partition tests rewritten to the T1 contract), Core 1398 + 4 pre-existing #99-era + 1 skip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1e5db94f0e
commit
579c8b06bc
4 changed files with 234 additions and 200 deletions
|
|
@ -23,7 +23,6 @@ public sealed class RetailPViewRenderer
|
|||
new(0, new Vector4(-1f, -1f, 1f, 1f), Array.Empty<Vector4>());
|
||||
|
||||
private readonly HashSet<uint> _oneCell = new(1);
|
||||
private readonly Dictionary<uint, int> _oneCellSlot = new(1);
|
||||
|
||||
// R-A2: per-building flood grouping, reused across frames (inner lists cleared each frame).
|
||||
private readonly Dictionary<uint, List<LoadedCell>> _buildingGroups = new();
|
||||
|
|
@ -90,20 +89,21 @@ public sealed class RetailPViewRenderer
|
|||
|
||||
ctx.EmitDiagnostics?.Invoke(result);
|
||||
|
||||
// T1 (fused BR-2/3): retail's frame order — static world, then the
|
||||
// aperture depth writes, then interior cells WHOLE far→near, then
|
||||
// per-cell statics, then ALL dynamics last (retail draws objects after
|
||||
// cells: PView::DrawCells Ghidra 0x005a4840; DrawBuilding 0x0059f2a0).
|
||||
// The geometric shell chop (gl_ClipDistance crop, 927fd8f/9ce335e) is
|
||||
// DELETED — retail never clips cell geometry; aperture exactness comes
|
||||
// from the punch/seal depth writes + the z-buffer, and the dynamics-
|
||||
// last order is what makes the punch safe (the first BR-2 attempt
|
||||
// punched after dynamics and erased the player, reverted 88be519).
|
||||
DrawLandscapeThroughOutsideView(ctx, clipAssembly, partition);
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
DrawExitPortalMasks(ctx, pvFrame, clipAssembly, drawableCells);
|
||||
// #113 fix scope (#114): GL-clip the shells only for the OUTDOOR root —
|
||||
// the case the flood replay validated (tight, stable door-aperture
|
||||
// regions) and the one that produced the phantom staircase. The first
|
||||
// user gate (2026-06-11) showed INDOOR clip regions are not yet
|
||||
// draw-quality (chopped stairs / vanishing inner walls at exits /
|
||||
// see-through to neighbour rooms at the meeting hall) — indoor roots
|
||||
// stay unclipped (yesterday's user-accepted state) until #114 brings
|
||||
// the indoor regions to retail's pixel-exact crop.
|
||||
DrawEnvCellShells(ctx, pvFrame, clipAssembly, drawableCells,
|
||||
clipShells: ctx.RootCell.IsOutdoorNode);
|
||||
DrawEnvCellShells(pvFrame);
|
||||
DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition);
|
||||
DrawDynamicsLast(ctx, partition);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -201,10 +201,13 @@ public sealed class RetailPViewRenderer
|
|||
|
||||
ctx.EmitDiagnostics?.Invoke(result);
|
||||
|
||||
// T1: look-in order — punch the apertures, then interior cells WHOLE,
|
||||
// then the looked-into building's per-cell statics. Dynamics are NOT
|
||||
// drawn here: they belong exclusively to the frame's single last
|
||||
// entity pass (the outdoor root's DrawDynamicsLast), which prevents
|
||||
// double-draws of entities inside looked-into buildings.
|
||||
DrawExitPortalMasks(ctx, pvFrame, clipAssembly, drawableCells);
|
||||
// DrawPortal is the from-outside look-in path — same validated outdoor
|
||||
// regime as the outdoor root (see #114 scope note in DrawInside).
|
||||
DrawEnvCellShells(ctx, pvFrame, clipAssembly, drawableCells, clipShells: true);
|
||||
DrawEnvCellShells(pvFrame);
|
||||
DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition);
|
||||
RestoreNoClip(ctx.SetTerrainClipUbo);
|
||||
|
||||
|
|
@ -228,11 +231,18 @@ public sealed class RetailPViewRenderer
|
|||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeClipRouteEnabled)
|
||||
EmitClipRouteProbe(clipAssembly, slice, probeSliceIndex);
|
||||
probeSliceIndex++;
|
||||
ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(slice, partition.Outdoor));
|
||||
ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(slice, partition.OutdoorStatic));
|
||||
}
|
||||
|
||||
foreach (var slice in clipAssembly.OutsideViewSlices)
|
||||
ctx.ClearDepthSlice?.Invoke(slice);
|
||||
// T1: retail clears the FULL depth buffer ONCE between the outside
|
||||
// stage and the interior stage (PView::DrawCells, Ghidra 0x005a4840 —
|
||||
// Clear gated on portalsDrawnCount; exact gate semantics is a plan
|
||||
// open question, staged as "any outside slice drawn"), then re-stamps
|
||||
// every outside-leading portal's TRUE depth (the seals,
|
||||
// DrawExitPortalMasks). Replaces the old per-slice scissored AABB
|
||||
// clear (wrong shape, no seal after it).
|
||||
if (clipAssembly.OutsideViewSlices.Length > 0)
|
||||
ctx.ClearDepthForInterior?.Invoke();
|
||||
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
}
|
||||
|
|
@ -342,68 +352,43 @@ public sealed class RetailPViewRenderer
|
|||
}
|
||||
}
|
||||
|
||||
private void DrawEnvCellShells(
|
||||
IRetailPViewCellDrawCallbacks ctx,
|
||||
PortalVisibilityFrame pvFrame,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
HashSet<uint> drawableCells, // param kept this task; removed in Task 4
|
||||
bool clipShells)
|
||||
private void DrawEnvCellShells(PortalVisibilityFrame pvFrame)
|
||||
{
|
||||
// Retail DrawCells Loop 2: every visible cell's shell, reverse cell_draw_list
|
||||
// (far→near), per portal_view slice. No drawableCells filter — a cell without a
|
||||
// clip-slot falls through GetCellSlicesOrNoClip to NoClipSlice and draws unclipped
|
||||
// (sealed; per-slice trim returns in Task 4).
|
||||
//
|
||||
// #113 (2026-06-10): the per-slice clip MUST actually clip. Retail clips drawn
|
||||
// CELL geometry to the accumulated portal view — Render::set_view (:343750)
|
||||
// installs the view polygon's edge planes and DrawEnvCell submits every cell
|
||||
// polygon with planeMask=0xffffffff (:427922) through ACRender::polyClipFinish.
|
||||
// Our equivalent (UseShellClipRouting → mesh_modern.vert gl_ClipDistance) was
|
||||
// routed but INERT: gl_ClipDistance writes are ignored unless GL_CLIP_DISTANCEi
|
||||
// is enabled, and no caller enabled it for this pass — so flooded interior cells
|
||||
// drew WHOLE, painting interior geometry across exterior walls (the Holtburg
|
||||
// meeting-hall phantom staircase, AAB3 0x100 stair cell coincident with the
|
||||
// shell's west wall). Self-contained per feedback_render_self_contained_gl_state;
|
||||
// no early-outs between enable and disable. Slot-0 slices (SSBO count=0) still
|
||||
// pass-all — the assembler's >8-plane scissor fallback remains unimplemented
|
||||
// (rare; Issue113MeetingHallFloodTests pins 0 such slices at the hall).
|
||||
// Characters/statics stay unclipped (DrawCellObjectLists): retail's mesh path is
|
||||
// viewcone-check + BoundingType handling, and hard-clipping slices characters at
|
||||
// doorways (the original UseIndoorMembershipOnlyRouting observation).
|
||||
//
|
||||
// clipShells (#114 scope, 2026-06-11): true only for outdoor-eye roots.
|
||||
// The first user gate showed indoor clip regions are not draw-quality
|
||||
// yet (chopped stairs / vanishing walls at exits) — indoor roots draw
|
||||
// unclipped until #114 lands pixel-exact indoor regions.
|
||||
if (clipShells)
|
||||
for (int i = 0; i < ClipFrame.MaxPlanes; i++)
|
||||
_gl.Enable(Silk.NET.OpenGL.EnableCap.ClipDistance0 + i);
|
||||
|
||||
// T1 (fused BR-2/3): retail DrawCells Loop 2 — every visible cell's
|
||||
// shell drawn WHOLE, reverse cell_draw_list (far→near), drawn once.
|
||||
// Retail NEVER clips cell geometry: the production path is the
|
||||
// prebuilt mesh (DrawEnvCell use_built_mesh, pc:427905; the
|
||||
// planeMask=0xffffffff legacy submit means skip-all-edges), and
|
||||
// aperture exactness comes from the punch/seal depth writes + the
|
||||
// z-buffer + this order. The former gl_ClipDistance chop
|
||||
// (927fd8f/9ce335e, #114) is deleted with this rewrite.
|
||||
// Per-cell opaque+transparent keeps the far→near transparent
|
||||
// compositing the per-cell loop already provided.
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
foreach (var entry in IndoorDrawPlan.ShellPass(pvFrame))
|
||||
{
|
||||
uint cellId = entry.CellId;
|
||||
_oneCell.Clear();
|
||||
_oneCell.Add(cellId);
|
||||
|
||||
var slices = GetCellSlicesOrNoClip(clipAssembly, cellId);
|
||||
|
||||
// BR-2 phantom-site probe: which cells draw their shell with a
|
||||
// pass-all slice (NoClipSlice fallback or assembler slot-0)?
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbePhantomEnabled)
|
||||
EmitPhantomShellProbe(cellId, slices, clipShells,
|
||||
hadSlot: clipAssembly.CellIdToViewSlices.ContainsKey(cellId));
|
||||
|
||||
foreach (var slice in slices)
|
||||
{
|
||||
UseShellClipRouting(cellId, slice);
|
||||
_envCells.Render(WbRenderPass.Opaque, _oneCell);
|
||||
_envCells.Render(WbRenderPass.Transparent, _oneCell);
|
||||
}
|
||||
_oneCell.Add(entry.CellId);
|
||||
_envCells.Render(WbRenderPass.Opaque, _oneCell);
|
||||
_envCells.Render(WbRenderPass.Transparent, _oneCell);
|
||||
}
|
||||
}
|
||||
|
||||
if (clipShells)
|
||||
for (int i = 0; i < ClipFrame.MaxPlanes; i++)
|
||||
_gl.Disable(Silk.NET.OpenGL.EnableCap.ClipDistance0 + i);
|
||||
// T1: the frame's single LAST entity pass — ALL server-spawned dynamics
|
||||
// (player, NPCs, doors, items), indoor or out, drawn after the static
|
||||
// world + punches + interior cells. Depth-tested, never hard-clipped
|
||||
// (retail draws objects per cell AFTER cells and viewcone-culls them —
|
||||
// PView::DrawCells epilogue Ghidra 0x005a4840; the sphere-vs-view cull is
|
||||
// T3). Drawing dynamics last is what makes the aperture punch safe.
|
||||
private void DrawDynamicsLast(
|
||||
IRetailPViewCellDrawContext ctx,
|
||||
InteriorEntityPartition.Result partition)
|
||||
{
|
||||
if (partition.Dynamics.Count == 0)
|
||||
return;
|
||||
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
DrawEntityBucket(ctx, partition.Dynamics, visibleCellIds: null);
|
||||
}
|
||||
|
||||
private void DrawCellObjectLists(
|
||||
|
|
@ -413,6 +398,11 @@ public sealed class RetailPViewRenderer
|
|||
HashSet<uint> drawableCells,
|
||||
InteriorEntityPartition.Result partition)
|
||||
{
|
||||
// T1: per-cell STATIC object lists only (dat-baked 0x40 statics) —
|
||||
// dynamics moved to DrawDynamicsLast. Far→near with the cells, after
|
||||
// the shells (retail DrawCells epilogue: PortalList = cell's views →
|
||||
// DrawObjCell, Ghidra 0x005a4840). Unclipped; per-view sphere culling
|
||||
// (viewconeCheck) is T3.
|
||||
for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--)
|
||||
{
|
||||
uint cellId = pvFrame.OrderedVisibleCells[i];
|
||||
|
|
@ -425,8 +415,8 @@ public sealed class RetailPViewRenderer
|
|||
_oneCell.Clear();
|
||||
_oneCell.Add(cellId);
|
||||
|
||||
// BR-2 phantom-site probe: entity buckets draw unclipped +
|
||||
// un-viewcone'd by design — log the per-cell exposure.
|
||||
// BR-2 phantom-site probe: static buckets draw unclipped +
|
||||
// un-viewcone'd until T3 — log the per-cell exposure.
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbePhantomEnabled)
|
||||
EmitPhantomObjsProbe(cellId, bucket.Count);
|
||||
|
||||
|
|
@ -440,29 +430,11 @@ public sealed class RetailPViewRenderer
|
|||
|
||||
// BR-2 phantom-site probe state: print-on-change per cell so the log stays
|
||||
// diffable while the condition persists. Throwaway apparatus — strip when
|
||||
// the #113 phantom residual closes (plan §BR-2).
|
||||
private readonly Dictionary<uint, string> _phantomShellSig = new();
|
||||
// the #113 phantom residual closes. (The [phantom-shell] half died with
|
||||
// the T1 chop deletion — shells draw whole, there is no slice state left
|
||||
// to report.)
|
||||
private readonly Dictionary<uint, int> _phantomObjsSig = new();
|
||||
|
||||
private void EmitPhantomShellProbe(uint cellId, ClipViewSlice[] slices, bool clipShells, bool hadSlot)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder(96);
|
||||
sb.Append(clipShells ? "clip=on" : "clip=OFF");
|
||||
sb.Append(hadSlot ? " slot=yes" : " slot=NONE(pass-all)");
|
||||
sb.Append(" slices=[");
|
||||
for (int i = 0; i < slices.Length; i++)
|
||||
{
|
||||
if (i > 0) sb.Append(',');
|
||||
sb.Append(slices[i].Slot).Append(':').Append(slices[i].Planes.Length).Append("pl");
|
||||
if (slices[i].Slot == 0) sb.Append("(PASS-ALL)");
|
||||
}
|
||||
sb.Append(']');
|
||||
var sig = sb.ToString();
|
||||
if (_phantomShellSig.TryGetValue(cellId, out var prev) && prev == sig) return;
|
||||
_phantomShellSig[cellId] = sig;
|
||||
Console.WriteLine($"[phantom-shell] cell=0x{cellId:X8} {sig}");
|
||||
}
|
||||
|
||||
private void EmitPhantomObjsProbe(uint cellId, int bucketCount)
|
||||
{
|
||||
if (_phantomObjsSig.TryGetValue(cellId, out var prev) && prev == bucketCount) return;
|
||||
|
|
@ -483,25 +455,14 @@ public sealed class RetailPViewRenderer
|
|||
|
||||
private void UseIndoorMembershipOnlyRouting()
|
||||
{
|
||||
// For MESHES (characters, statics) retail's DrawMesh performs portal-view
|
||||
// visibility checks (Render::viewconeCheck on the drawing sphere) rather
|
||||
// than hard per-poly clipping — feeding the 2D views into gl_ClipDistance
|
||||
// slices characters at stair/door boundaries, which retail does not do.
|
||||
// CELL SHELL geometry is different: retail clips it to the portal view
|
||||
// (planeMask=0xffffffff per cell polygon, decomp :427922 + :343750) —
|
||||
// DrawEnvCellShells enables exactly that (#113).
|
||||
// T1: NOTHING in the world passes hard-clips geometry anymore — retail
|
||||
// viewcone-CHECKS meshes (sphere vs view planes, T3) and never clips
|
||||
// cell shells (DrawEnvCell draws the whole prebuilt mesh, pc:427905).
|
||||
// This clears any clip routing left by the landscape slices.
|
||||
_envCells.SetClipRouting(null);
|
||||
_entities.ClearClipRouting();
|
||||
}
|
||||
|
||||
private void UseShellClipRouting(uint cellId, ClipViewSlice slice)
|
||||
{
|
||||
_oneCellSlot.Clear();
|
||||
_oneCellSlot[cellId] = slice.Slot;
|
||||
_envCells.SetClipRouting(_oneCellSlot);
|
||||
_entities.ClearClipRouting();
|
||||
}
|
||||
|
||||
private void DrawEntityBucket(
|
||||
IRetailPViewCellDrawContext ctx,
|
||||
IReadOnlyList<WorldEntity> bucket,
|
||||
|
|
@ -575,7 +536,11 @@ public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext
|
|||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries { get; init; }
|
||||
public required Action<uint> SetTerrainClipUbo { get; init; }
|
||||
public required Action<RetailPViewLandscapeSliceContext> DrawLandscapeSlice { get; init; }
|
||||
public Action<ClipViewSlice>? ClearDepthSlice { get; init; }
|
||||
/// <summary>T1: one full-buffer depth clear between the outside stage and the
|
||||
/// interior stage (retail PView::DrawCells, Ghidra 0x005a4840). Null for outdoor
|
||||
/// roots — outdoors the interiors must depth-test against terrain + exteriors and
|
||||
/// appear only through punched apertures.</summary>
|
||||
public Action? ClearDepthForInterior { get; init; }
|
||||
public Action<RetailPViewCellSliceContext>? DrawExitPortalMasks { get; init; }
|
||||
public Action<RetailPViewCellSliceContext>? DrawCellParticles { get; init; }
|
||||
public Action<RetailPViewFrameResult>? EmitDiagnostics { get; init; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue