using System.Numerics;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Sky;
using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Walk;
using AcDream.Core.Rendering;
using AcDream.Core.Vfx;
using AcDream.Core.World;
namespace AcDream.App.Rendering;
internal interface IEnvCellImmediateDrawSink
{
void DrawImmediate(
uint cellId,
EnvCellTransparentRoute route,
bool detailSurfaceActive);
}
// S3 chunk 4 fix round 2 (L2): RetailPViewFramebufferSize / IRetailPViewFramebufferSource
// / SilkRetailPViewFramebufferSource are deleted — their only consumer was
// the RHI surface's NDC-to-pixel scissor conversion, and that scissor
// mechanism itself is deleted in the same round (no producer of a narrowed
// rectangle remains anywhere in the walk).
internal sealed class RetailPViewCellSource : IRetailPViewCellSource
{
private readonly CellVisibility _cells;
public RetailPViewCellSource(CellVisibility cells) =>
_cells = cells ?? throw new ArgumentNullException(nameof(cells));
public LoadedCell? Find(uint cellId) =>
_cells.TryGetCell(cellId, out LoadedCell? cell) ? cell : null;
}
///
/// Concrete GL implementation of the named passes ordered by
/// . It owns reusable pass-local particle
/// classifications but borrows every renderer and world source.
/// The order it implements is retail PView::DrawCells @ 0x005A4840:
/// landscape, delayed-alpha flush, optional interior depth clear, exit masks,
/// cell shells/objects, then surviving dynamics. Landscape sky/terrain/weather
/// placement follows LScape::draw @ 0x00506330.
///
///
/// Campaign V slice V6j: backend-neutral. The order it implements is retail's and
/// is written once; the four places it touches graphics state directly are owned
/// by .
///
internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSink
{
private readonly IWorldPassSurface _surface;
private readonly IRenderFrameGlState _frameGlState;
private readonly ClipFrame _clipFrame;
private readonly TerrainModernRenderer? _terrain;
private readonly EnvCellRenderer _envCells;
private readonly WbDrawDispatcher _entities;
private readonly SkyRenderer? _sky;
private readonly ParticleSystem? _particles;
private readonly ParticleRenderer? _particleRenderer;
private readonly PortalDepthMaskRenderer? _portalDepthMask;
private readonly RetailAlphaQueue _alpha;
private readonly WorldRenderDiagnostics _diagnostics;
private readonly TerrainDrawDiagnosticsController _terrainDiagnostics;
private readonly HashSet _noSceneParticleEntityIds = [];
private readonly EnvCellAlphaDrawSource _envCellClipAlphaSource;
private readonly EnvCellAlphaDrawSource _envCellBlendAlphaSource;
internal delegate void RenderImmediateEnvCellRoute(
uint cellId,
EnvCellTransparentRoute route,
bool detailSurfaceActive);
public RetailPViewPassExecutor(
IWorldPassSurface surface,
IRenderFrameGlState frameGlState,
ClipFrame clipFrame,
TerrainModernRenderer? terrain,
EnvCellRenderer envCells,
WbDrawDispatcher entities,
SkyRenderer? sky,
ParticleSystem? particles,
ParticleRenderer? particleRenderer,
PortalDepthMaskRenderer? portalDepthMask,
RetailAlphaQueue alpha,
WorldRenderDiagnostics diagnostics,
TerrainDrawDiagnosticsController terrainDiagnostics)
{
_surface = surface ?? throw new ArgumentNullException(nameof(surface));
_frameGlState = frameGlState
?? throw new ArgumentNullException(nameof(frameGlState));
_clipFrame = clipFrame ?? throw new ArgumentNullException(nameof(clipFrame));
_terrain = terrain;
_envCells = envCells ?? throw new ArgumentNullException(nameof(envCells));
_entities = entities ?? throw new ArgumentNullException(nameof(entities));
_sky = sky;
_particles = particles;
_particleRenderer = particleRenderer;
_portalDepthMask = portalDepthMask;
_alpha = alpha ?? throw new ArgumentNullException(nameof(alpha));
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
_terrainDiagnostics = terrainDiagnostics
?? throw new ArgumentNullException(nameof(terrainDiagnostics));
_envCellClipAlphaSource = new EnvCellAlphaDrawSource(
_envCells.RenderTransparentOrdered,
EnvCellTransparentRoute.Clip);
_envCellBlendAlphaSource = new EnvCellAlphaDrawSource(
_envCells.RenderTransparentOrdered,
EnvCellTransparentRoute.Alpha);
}
public void BeginFrame()
{
// Campaign OVERHAUL S2 chunk 6: no per-frame particle-owner
// classification to reset any more — particle draws are cell-scoped
// and read live from ParticleSystem's own retained cell index.
}
/// Campaign FW3.2b-2: the shared dispatcher, for
/// 's WalkFrameDriver
/// construction — the driver's ctor takes a
/// directly (it calls SubmitOrderedStream itself; see that
/// class's own doc comment).
internal WbDrawDispatcher Dispatcher => _entities;
/// Campaign FW3.2b-2: forwards to
/// — the frame/
/// encoder pair the walk driver submits its stream flushes into.
internal (IGpuFrame Frame, IGpuPassEncoder Encoder) RequireWalkSubmission() =>
_entities.RequireWalkSubmission();
/// Campaign FW3.2b-2: forwards to
/// — the real
/// viewport size for WalkProductionFrameContext.
internal (int Width, int Height)? WalkAttachmentExtent =>
_entities.WalkAttachmentExtent;
public void AbortFrame()
{
List? failures = null;
TryAbort(_frameGlState.RestoreFrameDefaults);
TryAbort(_noSceneParticleEntityIds.Clear);
if (failures is { Count: > 0 })
throw new AggregateException("Retail PView pass abort failed.", failures);
void TryAbort(Action operation)
{
try
{
operation();
}
catch (Exception error)
{
(failures ??= []).Add(error);
}
}
}
public ClipFrameAssembly BeginWalkClipFrame(
bool outdoorRoot,
ClipFrameAssembly reuseAssembly) =>
ClipFrameAssembler.BeginWalkFrame(_clipFrame, outdoorRoot, reuseAssembly);
public void PrepareClipFrame() =>
_surface.PrepareClipFrame();
public void PrepareCellBatches(
RetailPViewFrameInput frame,
HashSet visibleCellIds) =>
_envCells.PrepareRenderBatches(
frame.ViewProjection,
frame.CameraWorldPosition,
filter: visibleCellIds,
centerLbX: frame.RenderCenterLbX,
centerLbY: frame.RenderCenterLbY,
renderRadius: frame.RenderRadius);
public void DrawOpaqueCellShells(HashSet cellIds) =>
_envCells.Render(WbRenderPass.Opaque, cellIds);
public bool CellHasTransparentShell(uint cellId) =>
_envCells.CellHasTransparent(cellId);
public void DrawTransparentCellShellsOrdered(IReadOnlyList cellIds) =>
_envCells.RenderTransparentOrdered(cellIds);
///
/// S4-c2 fix round 1 (M6, contract C4): retail DrawEnvCell
/// @0x0059f1c2 installs the environment detail surface (may be null)
/// before DrawMesh @0x0059f212 and clears it @0x0059f21a — detail
/// ON makes eligible subsets take Row 1 (immediate, WITH detail —
/// 's own doc
/// comment); detail OFF still routes each real subset by the exact
/// TextureBatchData.RetailSurfaceMask retained on
/// ObjectRenderBatch. A pure Base1ClipMap mask 0x08 reaches CLIP;
/// alpha-family 0x02 reaches ALPHA; a table-Immediate mask stays at the
/// cell turn. The queue coalesces only to one token per (cell,list), the
/// accepted AP-238 granularity residual.
///
internal void SubmitOrDrawTransparentCellShell(uint cellId)
{
bool detailSurfaceActive = _envCells.TransparentDetailEnabled;
EnvCellTransparentRoute routes = _envCells.GetTransparentRoutes(
cellId,
detailSurfaceActive);
DispatchTransparentCellShell(
cellId,
routes,
detailSurfaceActive,
_alpha,
_envCellClipAlphaSource,
_envCellBlendAlphaSource,
this);
}
private readonly List _singleCellListScratch = new(1);
private void DrawImmediateEnvCellRoute(
uint cellId,
EnvCellTransparentRoute route,
bool detailSurfaceActive)
{
_singleCellListScratch.Clear();
_singleCellListScratch.Add(cellId);
_envCells.RenderTransparentOrdered(
_singleCellListScratch,
route,
detailSurfaceActive);
}
void IEnvCellImmediateDrawSink.DrawImmediate(
uint cellId,
EnvCellTransparentRoute route,
bool detailSurfaceActive) =>
DrawImmediateEnvCellRoute(cellId, route, detailSurfaceActive);
///
/// Production dispatch for one cell after
/// scanned its real transparent batches. Separate source identities keep
/// CLIP and ALPHA replay filters attached to their tokens. Immediate
/// subsets draw at the cell turn; each deferred list receives at most one
/// token for this cell.
///
internal static void DispatchTransparentCellShell(
uint cellId,
EnvCellTransparentRoute routes,
bool detailSurfaceActive,
RetailAlphaQueue queue,
EnvCellAlphaDrawSource clipSource,
EnvCellAlphaDrawSource alphaSource,
IEnvCellImmediateDrawSink renderImmediate)
{
if ((routes & EnvCellTransparentRoute.Immediate) != 0)
{
renderImmediate.DrawImmediate(
cellId,
EnvCellTransparentRoute.Immediate,
detailSurfaceActive);
}
if ((routes & EnvCellTransparentRoute.Clip) != 0)
{
int token = clipSource.AddPendingCellId(cellId);
queue.TryAppend(RetailAlphaList.Clip, clipSource, token, overrideClipmap: true);
}
if ((routes & EnvCellTransparentRoute.Alpha) != 0)
{
int token = alphaSource.AddPendingCellId(cellId);
queue.TryAppend(RetailAlphaList.Alpha, alphaSource, token, overrideClipmap: false);
}
}
/// Delegate adapter retained for focused route/allocation tests;
/// production calls the interface overload above with this, so no
/// per-cell delegate conversion or retained callback enters the owner.
internal static void DispatchTransparentCellShell(
uint cellId,
EnvCellTransparentRoute routes,
bool detailSurfaceActive,
RetailAlphaQueue queue,
EnvCellAlphaDrawSource clipSource,
EnvCellAlphaDrawSource alphaSource,
RenderImmediateEnvCellRoute renderImmediate)
{
if ((routes & EnvCellTransparentRoute.Immediate) != 0)
{
renderImmediate(
cellId,
EnvCellTransparentRoute.Immediate,
detailSurfaceActive);
}
if ((routes & EnvCellTransparentRoute.Clip) != 0)
{
int token = clipSource.AddPendingCellId(cellId);
queue.TryAppend(RetailAlphaList.Clip, clipSource, token, overrideClipmap: true);
}
if ((routes & EnvCellTransparentRoute.Alpha) != 0)
{
int token = alphaSource.AddPendingCellId(cellId);
queue.TryAppend(RetailAlphaList.Alpha, alphaSource, token, overrideClipmap: false);
}
}
///
/// The queue-facing half of :
/// retains the cell ids submitted to one fixed list in append order and replays
/// a drained slice through
/// (production wiring) — one token per cell, never per subset (M6's
/// documented granularity residual). 's
/// "only adjacent same-source entries batch" invariant means a particle
/// or ordinary GfxObj instance appended BETWEEN two cell tokens keeps
/// its own position in the combined drain — this source never
/// re-orders across another entry. Depends on a delegate rather than
/// the concrete type (marked
/// , not ) so
/// EnvCellAlphaDrawSourceTests can pin the queue mechanics
/// without standing up a GPU-backed renderer.
///
internal delegate void RenderEnvCellsByRoute(
IReadOnlyList cellIds,
EnvCellTransparentRoute route,
bool detailSurfaceActive);
internal sealed class EnvCellAlphaDrawSource(
RenderEnvCellsByRoute renderTransparentOrdered,
EnvCellTransparentRoute route)
: IRetailAlphaDrawSource
{
private readonly List _pendingCellIds = new();
private readonly List _preparedCellIds = new();
private readonly List _drawScratch = new();
/// Returns this cell's token — its index into
/// at the moment it was added.
internal int AddPendingCellId(uint cellId)
{
int token = _pendingCellIds.Count;
_pendingCellIds.Add(cellId);
return token;
}
public void PrepareAlphaDraws(ReadOnlySpan tokens)
{
_preparedCellIds.Clear();
for (int i = 0; i < tokens.Length; i++)
_preparedCellIds.Add(_pendingCellIds[tokens[i]]);
}
public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
{
if (drawCount <= 0)
return;
_drawScratch.Clear();
for (int i = 0; i < drawCount; i++)
_drawScratch.Add(_preparedCellIds[firstPreparedDraw + i]);
renderTransparentOrdered(_drawScratch, route, detailSurfaceActive: false);
}
public void ResetAlphaSubmissions()
{
_pendingCellIds.Clear();
_preparedCellIds.Clear();
_drawScratch.Clear();
}
internal int PendingCount => _pendingCellIds.Count;
}
///
/// S3 chunk 1 fix round 2 (§11.6 H1): retail draws the weather pass
/// EXACTLY ONCE per frame — GameSky::Draw @0x00506ff0 with
/// arg2==1 runs AFTER LScape::draw's landblock loop
/// finishes (@0x00506396), not once per active landscape view. This call
/// is UNCLIPPED and sets no scissor — S3 chunk 4 (§10.2) deleted the
/// former per-outside-view-slice loop (the walk's own screen-space
/// terrain-clip writer, its per-frame clip-routing reset call, and the
/// old DrawLandscapeSliceLate leaf, one call per active landscape
/// view) that used to run before this
/// call and re-submit the rain mesh once per doorway aperture; this is
/// now the ONLY weather call site, matching retail's ONE unclipped
/// GameSky::Draw(sky,1).
///
/// S3 chunk 4 (§10.2): the rain PARTICLE emitters
/// (ParticleRenderPass.SkyPostScene) moved here too, as ONE
/// unclipped submission — retail inserts emitters into one unclipped
/// alpha list (no per-doorway clipSlot), the same rule as the
/// mesh draw above.
///
///
/// Gated exactly as before —
/// , retail's
/// SmartBox::is_player_outside @0x00451e80 check ANDed with the
/// two render toggles — via _sky?.RenderWeather(...) (null-
/// conditional, matching every other _sky?.RenderSky(...) call
/// site in this codebase): a missing sky asset silently skips the GL
/// draw.
///
///
/// S3 chunk 4 (O3): the transcript "OC" print that used to live here
/// moved to WalkFrameDriver.OnWeatherTurn, fired by
/// RetailFrameWalk.DrawLandscape at Collect time (retail's real
/// turn — between the landscape and the flood for an interior root, at
/// the end of the walk for an outdoor root) instead of wherever the GPU
/// draw happens to run at Replay.
///
///
/// S3 chunk 4 fix round 1 (K2): the caller () now gates
/// this call on WalkFrameDriver.WeatherTurnFired — the walk's own
/// record of "a Landscape turn ran with the weather gate open" — rather
/// than re-deriving the gate independently, so this method's own
/// check is a
/// provably redundant safety net (the caller reads the same
/// 's toggles the flag was computed from).
///
///
/// S3 chunk 4 fix round 1 (K8): the weather MESH draws before the rain
/// PARTICLE emitters below purely as this method's own call order —
/// retail's single GameSky::Draw(sky,1) call imposes no ordering
/// between acdream's two substitutes (the mesh and the particle
/// system), so this order is a bookkeeping choice, not a retail fact.
///
///
public void DrawWeatherOnce(RetailPViewFrameInput frame)
{
if (!ShouldDrawWeatherOnce(frame.RenderSky, frame.RenderWeather, frame.PlayerCellId))
return;
_sky?.RenderWeather(
frame.Camera,
frame.CameraWorldPosition,
frame.DayFraction,
frame.ActiveDayGroup,
frame.SkyKeyframe,
frame.EnvironOverrideActive);
if (_particles is not null && _particleRenderer is not null)
{
_particleRenderer.Draw(
frame.Camera,
frame.CameraWorldPosition,
ParticleRenderPass.SkyPostScene);
}
}
///
/// S3 chunk 1 fix round 2 (§11.6 H1): the weather pass's gate, extracted
/// as a pure predicate — retail's SmartBox::is_player_outside
/// @0x00451e80 check, (player objcell_id & 0xFFFF) < 0x100,
/// ANDed with the two render toggles. Internal (not private) so a test
/// can pin "no weather while the player stands indoors" and "no weather
/// with either toggle off" without a live GL/DAT
/// — itself still needs one to actually
/// draw, but this predicate alone decides whether it would even try (and
/// therefore whether the OC print fires).
///
internal static bool ShouldDrawWeatherOnce(
bool renderSky, bool renderWeather, uint playerCellId)
=> renderSky && renderWeather && (playerCellId & 0xFFFFu) < 0x100u;
public void DrawLandscapeStaticParticles(
RetailPViewFrameInput frame,
uint cellId)
{
// One unclipped submission per cell per frame. Retail never clips a
// particle to a portal view — its polys join the one alpha list during
// the owner cell's walk turn and the depth test at the flush decides
// occlusion (FlushAlphaList @0x0059D2E0). The former per-slice call
// with the slice's clip slot both hardware-cut effects at aperture
// boundaries and double-submitted owners visible in two slices.
// Retail CPhysicsObj::add_particle_shadow_to_cell (0x00514a70): an
// emitter owns one shadow in its OWN current cell, so this is a cell
// lookup, not an owner union — a hidden/suspended owner's emitter
// still draws here.
if (_particles is not null && _particleRenderer is not null)
{
_particleRenderer.DrawForCell(
frame.Camera,
frame.CameraWorldPosition,
ParticleRenderPass.Scene,
cellId,
clipSlot: 0);
}
}
public void ClearInteriorDepth()
{
_surface.ClearInteriorDepth();
}
/// Returns the number of exit-seal fans ATTEMPTED this turn —
/// S4-c1 fix round 1 F2 corrected this from "matches what reached the
/// GPU": retail increments portalsDrawnCount BEFORE
/// polyClipFinish runs (0x59BD70-0x59BD74 precedes 0x59BDB0), so
/// the counter records guard-passing ATTEMPTS, not successful GPU fans.
/// A portal whose polygon has fewer than 3 vertices is counted here even
/// though the GPU renderer's own internal <3 guard
/// (DrawDepthFan) draws nothing for it
/// (RetailPViewPassExecutorTests.DrawExitPortalMask_CountsAnUnclippableTwoVertexPolygon_ButDrawsNothing
/// pins exactly this).
public int DrawExitPortalMask(
RetailPViewFrameInput frame,
uint cellId,
ReadOnlySpan clipPlanes) =>
DrawPortalDepthWrite(
cellId,
clipPlanes,
frame,
forceFarZ: frame.RootCell.IsOutdoorNode);
public void DrawUnattachedSceneParticles(
RetailPViewFrameInput frame,
bool outdoorCells)
{
if (_particles is null || _particleRenderer is null)
return;
// Retail draws an unattached emitter once, during its owner CELL's
// walk turn, with NO portal-view clip (CPhysicsObj::ShouldDrawParticles
// @0x0050FE60 gates by cell in-view + distance; occlusion is the depth
// test at FlushAlphaList @0x0059D2E0). Outdoor-cell emitters submit in
// the landscape stage, interior-cell emitters in the final world stage.
// The former once-per-OutsideView-slice submission with that slice's
// hardware clip slot made effects vanish by view direction (zero
// outside slices in view = zero submissions) — invented behavior.
_particleRenderer.DrawForOwners(
frame.Camera,
frame.CameraWorldPosition,
ParticleRenderPass.Scene,
_noSceneParticleEntityIds,
includeUnattached: true,
clipSlot: 0,
unattachedCellScope: outdoorCells
? UnattachedEmitterCellScope.OutdoorCells
: UnattachedEmitterCellScope.InteriorCells);
}
/// PView::DrawCells @0x005a4840's own
/// FlushAlphaList(0f) @0x005a4872 (OH1 contract §7 site 3).
public void FlushLandscapeAlpha() =>
_alpha.Flush(RetailAlphaFlushSite.LandscapeFlush, 0f);
/// RenderDeviceD3D::DrawBuilding @0x0059f2a0's own
/// FlushAlphaList(0f) @0x0059f30b (OH1 contract §7 site 1) — the
/// building alpha barrier. Distinct call from
/// even though both drain under the same 0f threshold (S4-c2: the two
/// sites were sharing one undifferentiated call before the two-list
/// FIFO cutover, when no site label existed to distinguish them).
internal void FlushBuildingAlpha() =>
_alpha.Flush(RetailAlphaFlushSite.DrawBuilding, 0f);
/// RenderDeviceD3D::DrawBlock @0x005a17c0's (per-land-cell
/// loop head @0x005a18d0) own FlushAlphaList(::flush) @0x005a1a07,
/// the immutable global 0.75f pressure valve (OH1 contract §7 site
/// 2).
internal void FlushSortCellExitAlpha() =>
_alpha.Flush(RetailAlphaFlushSite.SortCellExit, 0.75f);
public void DrawCellParticles(
RetailPViewFrameInput frame,
uint cellId)
{
if (_particles is null || _particleRenderer is null)
return;
// Retail never clips cell particles to a portal view: the owner
// cell's walls own occlusion via the depth test at the alpha flush.
// CPhysicsObj::add_particle_shadow_to_cell (0x00514a70) draws an
// emitter in its OWN current cell, so this is a cell lookup, not an
// owner union — a hidden/suspended owner's emitter still draws here.
_particleRenderer.DrawForCell(
frame.Camera,
frame.CameraWorldPosition,
ParticleRenderPass.Scene,
cellId,
clipSlot: 0);
}
public void EmitDiagnostics(
RetailPViewFrameInput frame,
RetailPViewFrameResult result) =>
_diagnostics.EmitRetailPViewDiagnostics(
RenderingDiagnostics.ProbeVisibilityEnabled,
RenderingDiagnostics.ProbeFlapEnabled,
result,
frame.RootCell,
frame.ViewerCellId,
frame.PlayerCellId,
frame.CameraWorldPosition,
frame.PlayerViewPosition,
frame.CameraCellResolution);
/// Retail D3DPolyRender::DrawPortalPolyInternal
/// @0x0059BC90. Main interior roots stamp true depth (seal); outdoor and
/// look-in apertures stamp far depth (punch). The renderer owns that
/// choice. Returns the number of portals ATTEMPTED (not necessarily
/// drawn) this turn — S4-c1 fix round 1 F2 retired the ">=3 vertices"
/// pre-filter that used to gate the count: every portal with
/// OtherCellId == 0xFFFF that survives the ±12 boundary guard
/// ()
/// is counted here, BEFORE the vertex-count check that decides whether
/// its fan actually draws — matching retail's own count-BEFORE-clip
/// order (0x59BD70-0x59BD74 precedes 0x59BDB0). This is the same
/// enumeration WalkFrameDriver.OnInteriorFloodDrawTurn counts
/// through DrawExitSeals's return value.
private int DrawPortalDepthWrite(
uint cellId,
ReadOnlySpan clipPlanes,
RetailPViewFrameInput frame,
bool forceFarZ,
int? onlyPortalIndex = null)
{
if (_portalDepthMask is null)
return 0;
LoadedCell? cell = frame.Cells.Find(cellId);
if (cell is null)
return 0;
int submitted = 0;
Span world = stackalloc Vector3[32];
for (int index = 0; index < cell.Portals.Count; index++)
{
if (onlyPortalIndex.HasValue && index != onlyPortalIndex.Value)
continue;
if (cell.Portals[index].OtherCellId != 0xFFFF)
continue;
if (index >= cell.PortalPolygons.Count)
break;
Vector3[] localVertices = cell.PortalPolygons[index];
// S4-c1 C1 (fix round 1 F1): DrawPortalPolyInternal's
// degenerate-input guard
// (WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard's
// own doc comment) — tested on the LOCAL portal-polygon
// vertices, BEFORE the world-transform loop below. A hit drops
// the whole polygon: no transform, no fan submission, no
// `submitted` increment (retail's reject -> transform -> clip
// -> count order). No length pre-filter runs before this any
// more (fix round 1 F2 — see the count comment below).
if (WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(localVertices))
continue;
int count = Math.Min(localVertices.Length, world.Length);
for (int vertex = 0; vertex < count; vertex++)
{
// FW3.3: fans draw at the dat aperture verbatim (the
// ShellDrawLiftZ retirement — shells draw unlifted too).
world[vertex] = Vector3.Transform(
localVertices[vertex],
cell.WorldTransform);
}
_diagnostics.EmitSeamMask(
RenderingDiagnostics.ProbeSeamDrawEnabled,
RenderingDiagnostics.SeamDrawTargetCells,
cellId,
index,
forceFarZ,
world[..count]);
// S4-c1 fix round 1 F2: retail increments portalsDrawnCount
// (0x59BD70-0x59BD74) BEFORE polyClipFinish runs (0x59BDB0) —
// the counter records accepted ATTEMPTS, not successful GPU
// fans (oh1-depth-lifecycle.md's arbitration table). A polygon
// with fewer than 3 vertices is counted here even though
// DrawDepthFan below draws nothing — its own `< 3` guard is
// this port's stand-in for retail's post-clip `var_4 >= 3`
// check. Round 0 counted AFTER that guard (a `< 3` continue
// ahead of both the boundary guard and this increment), which
// silently dropped the count for such a polygon — never
// observed on authored dat data (every real portal polygon has
// >= 3 vertices) but wrong order all the same.
submitted++;
_portalDepthMask.DrawDepthFan(
world[..count],
frame.ViewProjection,
clipPlanes,
forceFarZ);
}
return submitted;
}
// S3 chunk 4 fix round 1 (K4): the sky's own doorway-scissor bracket and
// the EnableClipDistances wrapper around it are deleted — their only
// caller was DrawWalkSky's per-outside-view-slice loop
// (RetailPViewPassExecutor.WalkLeaf.cs), itself deleted by the same fix
// (retail draws the sky ONCE, unclipped, exactly like the terrain and
// the weather — see DrawWalkSky's own doc comment).
//
// S3 chunk 4 fix round 2 (L2): round 1's own comment here asserted that
// the interior depth clear still had a live scissor to end at that
// point — a mechanism that no longer existed ANYWHERE by then (K4 had
// already deleted the sky's own scissor bracket, the last production
// producer of a narrowed rectangle). The interface's own scissor
// begin/end pair and their RhiWorldPassSurface bodies are deleted outright
// in this round; the interior depth clear no longer ends anything
// because nothing narrows a rectangle any more — the pass encoder sets
// the full-attachment scissor once, at pass begin
// (VulkanGpuPassEncoder.cs:87), and it stays that way for the life of
// the pass.
//
// S3 review fix round 1 (F3): round 2's own comment above claimed "the
// KEEP clips (exit seals, punch fans) bracket their own draws with
// Enable/DisableClipDistances below" — FALSE. Neither
// DrawExitPortalMask/DrawPortalDepthWrite nor
// RetailPViewPassExecutor.WalkLeaf.cs's DrawWalkPunchFan ever called
// Enable/DisableClipDistances; every one of this file's six
// DisableClipDistances() call sites sat around the UNCLIPPED particle
// draws (DrawWeatherOnce, DrawLandscapeStaticParticles x2,
// DrawUnattachedSceneParticles, DrawCellParticles x2) — bodies that were
// already no-ops on the only backend
// (RhiWorldPassSurface.EnableClipDistances/DisableClipDistances, see
// WorldPassSurface.cs). A no-op call bracketing an unclipped draw is not
// a clip bracket at all; it asserted a mechanism this file never had.
// Deleted outright with this correction, along with the private
// DisableClipDistances() wrapper (a method that does nothing on the
// only backend is a false mechanism too). IWorldPassSurface.
// EnableClipDistances/DisableClipDistances stay on the INTERFACE —
// WorldScenePassExecutor (the separate flat-world path) still calls
// them — only this file's now-pointless use of them is gone.
}