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
|
|
@ -171,6 +171,7 @@ public sealed class GameWindow : IDisposable
|
|||
// each frame on an indoor root (null on the outdoor root).
|
||||
private AcDream.App.Rendering.InteriorRenderer? _interiorRenderer;
|
||||
private AcDream.App.Rendering.RetailPViewRenderer? _retailPViewRenderer;
|
||||
private AcDream.App.Rendering.PortalDepthMaskRenderer? _portalDepthMask;
|
||||
private AcDream.App.Rendering.InteriorEntityPartition.Result? _interiorPartition;
|
||||
|
||||
// Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain
|
||||
|
|
@ -1845,6 +1846,10 @@ public sealed class GameWindow : IDisposable
|
|||
_clipFrame ??= ClipFrame.NoClip();
|
||||
_retailPViewRenderer = new AcDream.App.Rendering.RetailPViewRenderer(
|
||||
_gl, _clipFrame, _envCellRenderer!, _wbDrawDispatcher!);
|
||||
|
||||
// T1: invisible portal depth writes (seal/punch) — retail
|
||||
// DrawPortalPolyInternal (Ghidra 0x0059bc90).
|
||||
_portalDepthMask = new AcDream.App.Rendering.PortalDepthMaskRenderer(_gl);
|
||||
}
|
||||
|
||||
// Phase G.1 sky renderer — its own shader (sky.vert / sky.frag)
|
||||
|
|
@ -7632,24 +7637,26 @@ public sealed class GameWindow : IDisposable
|
|||
renderSky,
|
||||
kf,
|
||||
environOverrideActive),
|
||||
// The depth clear is a doorway "look-in" trick: clear depth inside a door/window
|
||||
// region so the cell seen THROUGH it draws over the terrain drawn through that
|
||||
// region (the indoor root looking out). For the OUTDOOR-node root the only
|
||||
// OutsideView slice is the FULL-SCREEN base terrain, so clearing its depth wipes the
|
||||
// entire depth buffer AFTER terrain/exteriors/player drew — the flooded building
|
||||
// interiors (cellars) would then paint over everything (cellar in front of the
|
||||
// player; building interiors through the ground). Outdoors the interiors must
|
||||
// depth-test against terrain+exteriors and appear only through real door openings,
|
||||
// so issue NO depth clear. Interior roots keep the doorway clear (unchanged).
|
||||
ClearDepthSlice = clipRoot.IsOutdoorNode
|
||||
// T1: retail's depth discipline (PView::DrawCells, Ghidra 0x005a4840).
|
||||
// INTERIOR roots: one FULL depth clear between the outside stage and
|
||||
// the interior stage, then SEALS re-stamp every outside-leading
|
||||
// portal's TRUE depth (#108's protective mechanism). OUTDOOR roots:
|
||||
// no clear (the world's depth must survive) — instead each flooded
|
||||
// building's entry aperture gets a far-Z PUNCH so its interior shows
|
||||
// through the doorway. Both are safe ONLY because dynamics draw LAST
|
||||
// (DrawDynamicsLast) — the first BR-2 attempt punched after dynamics
|
||||
// and erased the player (reverted 88be519).
|
||||
ClearDepthForInterior = clipRoot.IsOutdoorNode
|
||||
? null
|
||||
: slice =>
|
||||
: () =>
|
||||
{
|
||||
bool zc = BeginDoorwayScissor(true, slice.NdcAabb);
|
||||
_gl.Disable(EnableCap.ScissorTest);
|
||||
_gl.DepthMask(true); // depth clears honor glDepthMask (c4df241 lesson)
|
||||
_gl.Clear(ClearBufferMask.DepthBufferBit);
|
||||
if (zc)
|
||||
_gl.Disable(EnableCap.ScissorTest);
|
||||
},
|
||||
DrawExitPortalMasks = sliceCtx =>
|
||||
DrawRetailPViewPortalDepthWrite(sliceCtx, envCellViewProj,
|
||||
forceFarZ: clipRoot.IsOutdoorNode),
|
||||
DrawCellParticles = sliceCtx =>
|
||||
DrawRetailPViewCellParticles(sliceCtx, camera, camPos),
|
||||
EmitDiagnostics = result =>
|
||||
|
|
@ -7703,45 +7710,32 @@ public sealed class GameWindow : IDisposable
|
|||
|| pviewResult.ClipAssembly.OutsideViewSlices.Length > 0)
|
||||
? "pviewScoped"
|
||||
: sigSceneParticles;
|
||||
sigOutdoorSceneryDrawn = pviewResult.Partition.Outdoor.Count > 0
|
||||
sigOutdoorSceneryDrawn = pviewResult.Partition.OutdoorStatic.Count > 0
|
||||
&& pviewResult.ClipAssembly.OutsideViewSlices.Length > 0;
|
||||
|
||||
// Render unification: DrawInside draws the Outdoor bucket (through the landscape
|
||||
// slice) and the per-cell ByCell buckets, but NOT LiveDynamic — server entities with
|
||||
// no resolved ParentCellId (the transient just-spawned / unpositioned case the old
|
||||
// outdoor branch drew at the bottom of its block). Preserve that draw for the
|
||||
// outdoor-node root so no live entity blinks out outdoors (spec section 10 regression
|
||||
// guard). DrawInside's tail clears entity clip routing and disables clip distances, so
|
||||
// visibleCellIds:null draws them unclipped — identical to the old outdoor path.
|
||||
if (clipRoot.IsOutdoorNode
|
||||
&& _interiorRenderer is not null
|
||||
&& pviewResult.Partition.LiveDynamic.Count > 0)
|
||||
{
|
||||
_interiorRenderer.DrawEntityBucket(
|
||||
camera, frustum, playerLb, animatedIds,
|
||||
pviewResult.Partition.LiveDynamic, visibleCellIds: null);
|
||||
sigLiveDynamicDrawnCount = pviewResult.Partition.LiveDynamic.Count;
|
||||
}
|
||||
// T1: DrawInside now draws ALL dynamics itself in its single
|
||||
// last entity pass (DrawDynamicsLast) — the old LiveDynamic
|
||||
// top-up draw is gone.
|
||||
sigLiveDynamicDrawnCount = pviewResult.Partition.Dynamics.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool liveDynamicsDrawn = false;
|
||||
|
||||
if (_interiorRenderer is not null)
|
||||
{
|
||||
_outdoorRootNoCells.Clear();
|
||||
var outdoorPartition = AcDream.App.Rendering.InteriorEntityPartition.Partition(
|
||||
_outdoorRootNoCells, _worldState.LandblockEntries);
|
||||
sigOutdoorRootObjectCount = outdoorPartition.Outdoor.Count;
|
||||
sigOutdoorRootObjectCount = outdoorPartition.OutdoorStatic.Count;
|
||||
|
||||
if (outdoorPartition.Outdoor.Count > 0)
|
||||
// T1: static world first (shells + scenery)…
|
||||
if (outdoorPartition.OutdoorStatic.Count > 0)
|
||||
{
|
||||
_interiorRenderer.DrawEntityBucket(
|
||||
camera,
|
||||
frustum,
|
||||
playerLb,
|
||||
animatedIds,
|
||||
outdoorPartition.Outdoor,
|
||||
outdoorPartition.OutdoorStatic,
|
||||
visibleCellIds: null);
|
||||
}
|
||||
|
||||
|
|
@ -7795,6 +7789,12 @@ public sealed class GameWindow : IDisposable
|
|||
MaxSeedDistance = 48f,
|
||||
LandblockEntries = _worldState.LandblockEntries,
|
||||
SetTerrainClipUbo = uboId => _terrain?.SetClipUbo(uboId),
|
||||
// T1: look-in — PUNCH building entry apertures to far-Z so
|
||||
// the flooded interior shows through the doorway. Safe:
|
||||
// dynamics draw after this whole block.
|
||||
DrawExitPortalMasks = sliceCtx =>
|
||||
DrawRetailPViewPortalDepthWrite(sliceCtx, envCellViewProj,
|
||||
forceFarZ: true),
|
||||
});
|
||||
|
||||
if (portalResult is not null)
|
||||
|
|
@ -7804,21 +7804,20 @@ public sealed class GameWindow : IDisposable
|
|||
sigExteriorClipAssembly = portalResult.ClipAssembly;
|
||||
sigExteriorDrawableCells = portalResult.DrawableCells;
|
||||
sigExteriorPartition = portalResult.Partition;
|
||||
liveDynamicsDrawn = portalResult.Partition.LiveDynamic.Count > 0;
|
||||
if (liveDynamicsDrawn)
|
||||
sigLiveDynamicDrawnCount = portalResult.Partition.LiveDynamic.Count;
|
||||
}
|
||||
}
|
||||
|
||||
if (!liveDynamicsDrawn && outdoorPartition.LiveDynamic.Count > 0)
|
||||
// T1: …then ALL dynamics last (after the look-in punched +
|
||||
// drew interiors), depth-tested, never hard-clipped.
|
||||
if (outdoorPartition.Dynamics.Count > 0)
|
||||
{
|
||||
sigLiveDynamicDrawnCount = outdoorPartition.LiveDynamic.Count;
|
||||
sigLiveDynamicDrawnCount = outdoorPartition.Dynamics.Count;
|
||||
_interiorRenderer.DrawEntityBucket(
|
||||
camera,
|
||||
frustum,
|
||||
playerLb,
|
||||
animatedIds,
|
||||
outdoorPartition.LiveDynamic,
|
||||
outdoorPartition.Dynamics,
|
||||
visibleCellIds: null);
|
||||
}
|
||||
}
|
||||
|
|
@ -9333,7 +9332,7 @@ public sealed class GameWindow : IDisposable
|
|||
if (partition is not null)
|
||||
{
|
||||
int shellTotal = 0, shellMesh = 0;
|
||||
foreach (var e in partition.Outdoor)
|
||||
foreach (var e in partition.OutdoorStatic)
|
||||
if (e.IsBuildingShell) { shellTotal++; if (e.MeshRefs.Count > 0) shellMesh++; }
|
||||
sb.Append(" bshell=").Append(shellTotal).Append('/').Append(shellMesh);
|
||||
}
|
||||
|
|
@ -9457,8 +9456,8 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
if (keys.Count > MaxCells)
|
||||
sb.Append(",...");
|
||||
sb.Append("] out=").Append(partition.Outdoor.Count)
|
||||
.Append(" live=").Append(partition.LiveDynamic.Count);
|
||||
sb.Append("] out=").Append(partition.OutdoorStatic.Count)
|
||||
.Append(" live=").Append(partition.Dynamics.Count);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
|
@ -9550,6 +9549,51 @@ public sealed class GameWindow : IDisposable
|
|||
DisableClipDistances();
|
||||
}
|
||||
|
||||
// T1: retail's invisible portal depth writes on every outside-leading
|
||||
// portal (other_cell_id==0xFFFF) of this cell, clipped to the slice's view
|
||||
// region — D3DPolyRender::DrawPortalPolyInternal (Ghidra 0x0059bc90),
|
||||
// dispatched by PView::DrawCells (Ghidra 0x005a4840). forceFarZ is
|
||||
// retail's maxZ1(true)/maxZ2(false) selector:
|
||||
// • INTERIOR root (false → SEAL, true depth): after the full depth clear,
|
||||
// stamp the door plane so interior geometry beyond the door z-fails
|
||||
// inside the aperture and the terrain drawn through the outside view
|
||||
// keeps its pixels (#108's protective mechanism).
|
||||
// • OUTDOOR root / look-in (true → PUNCH, far depth): erase the world's
|
||||
// depth inside a flooded building's entry aperture so the interior
|
||||
// drawn next shows THROUGH the doorway.
|
||||
// Both are safe ONLY because dynamics draw last (DrawDynamicsLast) — the
|
||||
// first BR-2 attempt punched after dynamics and erased the player
|
||||
// (reverted 88be519). Wiring only — the draw lives in
|
||||
// PortalDepthMaskRenderer.
|
||||
private void DrawRetailPViewPortalDepthWrite(
|
||||
AcDream.App.Rendering.RetailPViewCellSliceContext sliceCtx,
|
||||
System.Numerics.Matrix4x4 viewProjection,
|
||||
bool forceFarZ)
|
||||
{
|
||||
if (_portalDepthMask is null)
|
||||
return;
|
||||
if (!_cellVisibility.TryGetCell(sliceCtx.CellId, out var cell) || cell is null)
|
||||
return;
|
||||
|
||||
Span<System.Numerics.Vector3> world = stackalloc System.Numerics.Vector3[32];
|
||||
for (int i = 0; i < cell.Portals.Count; i++)
|
||||
{
|
||||
if (cell.Portals[i].OtherCellId != 0xFFFF)
|
||||
continue; // depth writes apply to portals leading OUTSIDE only
|
||||
if (i >= cell.PortalPolygons.Count)
|
||||
break;
|
||||
var localVerts = cell.PortalPolygons[i];
|
||||
if (localVerts.Length < 3)
|
||||
continue;
|
||||
|
||||
int n = System.Math.Min(localVerts.Length, world.Length);
|
||||
for (int v = 0; v < n; v++)
|
||||
world[v] = System.Numerics.Vector3.Transform(localVerts[v], cell.WorldTransform);
|
||||
|
||||
_portalDepthMask.DrawDepthFan(world[..n], viewProjection, sliceCtx.Slice.Planes, forceFarZ);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawRetailPViewCellParticles(
|
||||
AcDream.App.Rendering.RetailPViewCellSliceContext sliceCtx,
|
||||
ICamera camera,
|
||||
|
|
@ -11773,6 +11817,7 @@ public sealed class GameWindow : IDisposable
|
|||
_audioEngine?.Dispose(); // Phase E.2: stop all voices, close AL context
|
||||
_wbDrawDispatcher?.Dispose();
|
||||
_envCellRenderer?.Dispose(); // Phase A8
|
||||
_portalDepthMask?.Dispose(); // T1
|
||||
_clipFrame?.Dispose(); // Phase U.3
|
||||
_skyRenderer?.Dispose(); // depends on sampler cache; dispose first
|
||||
_samplerCache?.Dispose();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue