acdream/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs
Erik 0aa166aa09 fix(render): complete S4 chunk 2 final alpha parity round
Final allowed fix round for S4-c2 on cc8e5677a. This lands every item in
the campaign packet section 12 without adding a flush site, shader, distance,
overflow recovery draw, or graphical-client run.

R2-1 particle row 5: ParticleRenderer now constructs and owns the actual
particle-mesh-opaque pipeline using the existing particle_mesh shaders and
layout, Blend=None, depth test/write enabled with WorldCompare, dynamic
per-batch cull, clockwise front face, and no alpha-to-coverage. The production
dispatch selects it for an opaque-classified mesh particle whose clamped
material alpha is 1.0. Nonopaque mesh pipelines remain depth-write-off. The
production route keeps cached reserve/immediate delegates and the warmed
append/immediate paths allocate 0 B.

R2-2 EnvCell exact per-subset routing: ObjectMeshManager carries Content's
TextureBatchData.RetailSurfaceMask onto ObjectRenderBatch at the real upload
boundary. EnvCellRenderer scans the active prepared cell snapshot and feeds
each real transparent batch's exact mask through RetailAlphaMeshRouter. Pure
0x08 Base1ClipMap reaches CLIP, 0x02 alpha-family reaches ALPHA, and table
Immediate subsets draw at the cell turn. Separate fixed-route draw sources
coalesce to at most one token per (cell,list) and filtered replay draws only
that list's subsets; a mixed cell contributes to both lists without duplicate
replay. Detail-on routes eligible subsets immediately with the detail pass.
The warmed dispatch/source allocation pins measure 0 B; the production scan/filter is covered behaviorally and uses only retained scratch/enumerators (static allocation audit).

R2-3 capacity cleanup: RetailAlphaQueue registers a source before the 3,000
entry capacity return. A source whose first append is rejected is therefore
reset by flush, EndFrame, or abort, but its rejected payload is never prepared
or drawn.

R2-4 production proof and prose: both actual Wb submit sites are exercised;
the particle tests call the production dispatcher and inspect the constructed
owner's production pipeline/selector; EnvCell tests upload real Content batch
masks through ObjectMeshManager and drain real filtered MDI calls. The A1
positive proof executes WorldSceneRenderer's real outdoor frame owner through
RetailPViewRenderer.DrawInside and RetailAlphaQueue.EndFrame and observes
[DrawBuilding x N, RenderNormalMode] with no LandscapeFlush. The packet and
register now state the varying retail first-for-list truth and the exact
per-subset EnvCell/AP-238, visible AP-239 compositing, and AP-240 feeder scope.
Physical active register counts remain AP=159 and AD=92.

Final clean-state gates (actual output):
- Release solution build: Build succeeded; 0 Warning(s); 0 Error(s).
- Hermetic solution filter: every project green, 16,728 passed / 0 failed /
  0 skipped total; AcDream.App.Tests 6,875/6,875.
- InstalledDat: 255 passed / 10 failed / 1 skipped / 266 total, exactly the
  allowed identities: TowerAscent_StaircaseStaysConeVisible_EveryStep;
  MainGameUiAndChatInput_MediaBearingChildrenNowBuildAsRealWidgets (#383);
  EveryAuthoredInvisibleWidget_StartsHiddenAcrossAllLayouts (#383);
  Oh_doorway_still_first_frame_diff (#458); and the six
  AlphaFlushCounts_{CathedralArrival,CathedralLeak,CathedralStairArch,
  FoundryDeep,HoltburgDoorwayStill,TerraceEdge}_MatchesRetailFrame2. All six
  AlphaFlushSites_* pass in the same lane.
- VulkanShaderDescriptorContractTests + VulkanShaderManifestTests +
  RenderPackSpirvValidatorTests: 32 passed / 0 failed / 0 skipped.
- Corrected queue/router/walk/driver/particle/Wb/EnvCell/PView production
  filter: 211 passed / 0 failed / 0 skipped.
- Explicit warmed production allocation pins: 2 passed; both measure 0 B.
- Register: physical AP-238/AP-239/AP-240/AD-120/~~AP-34~~ rows each count
  exactly 1; active physical rows AP=159 and AD=92.
- git diff --check: PASS.

Production mutation checks (each applied, run to the named first failure, and
exactly reversed before the final gates):

1. Restoring the particle row-5 throw fails
   OpaqueClassifiedMeshBatch_WithNoMaterialAlpha_DrawsImmediateOnOpaqueDepthState
   first with InvalidOperationException: mutation: row 5 unreachable.

2. Deleting row 5's immediate callback fails that same production-dispatch
   test's first collection assertion: expected [(Mesh, 11, True)], actual [].

3. Constructing the actual owner pipeline with depthWrite:false fails
   ImmediateOpaqueMesh_UsesProductionParticleMeshOpaquePipelineDescription
   first at Assert.True(description.Depth.Write): expected true, actual false.

4. Mapping the production selector back to _meshAlphaPipeline fails that same
   test first at Assert.Same: expected particle-mesh-opaque, actual
   particle-mesh-alpha.

5. Dropping RetailSurfaceMask at the real ObjectMeshManager upload boundary
   fails the mixed-cell production scan first: expected Clip | Alpha, actual
   Immediate.

6. Hardcoding the uploaded EnvCell scan to MaskAlphaFamily fails the mixed-cell
   production scan first: expected Clip | Alpha, actual Alpha; the pure-mask
   pin also reports expected Clip, actual Alpha.

7. Inverting the production detail predicate fails the detail-on production
   pin first: expected Immediate, actual Clip.

8. Removing both EnvCell replay filters fails the mixed production drain's real
   MDI assertions: each call expected DrawCount 1, actual DrawCount 2.

9. Moving RegisterSource below the full-capacity return fails every
   RejectedFirstUseSource_IsCleanedWithoutPrepareOrDraw row (flush, EndFrame,
   abort) at the first ResetCount assertion: expected 1, actual 0.

10. Hardcoding detailSurfaceActive=false at the real Wb dispatch fails both
    production submit-site tests at their first queue-count assertion:
    expected 0, actual 1.

11. Restoring RetailPViewRenderer's removed outdoor LandscapeFlush call fails
    OutdoorProductionPView_DrainsBuildingThenRenderNormalModeWithoutLandscapeFlush
    first at the real drain sequence: expected [DrawBuilding,
    RenderNormalMode], actual [DrawBuilding, LandscapeFlush,
    RenderNormalMode].

12. Deleting WorldSceneRenderer's final EndFrame owner drain fails that same
    full-path A1 test first: expected [DrawBuilding, RenderNormalMode], actual
    [DrawBuilding].

Not done/deferred: none. No retail conflict or infeasible contract item was
found. No graphical client was launched.

Co-Authored-By: Codex <noreply@openai.com>
2026-09-04 11:53:24 +02:00

728 lines
32 KiB
C#

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;
}
/// <summary>
/// Concrete GL implementation of the named passes ordered by
/// <see cref="RetailPViewRenderer"/>. It owns reusable pass-local particle
/// classifications but borrows every renderer and world source.
/// The order it implements is retail <c>PView::DrawCells @ 0x005A4840</c>:
/// landscape, delayed-alpha flush, optional interior depth clear, exit masks,
/// cell shells/objects, then surviving dynamics. Landscape sky/terrain/weather
/// placement follows <c>LScape::draw @ 0x00506330</c>.
/// </summary>
/// <summary>
/// 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 <see cref="IWorldPassSurface"/>.
/// </summary>
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<uint> _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.
}
/// <summary>Campaign FW3.2b-2: the shared dispatcher, for
/// <see cref="RetailPViewRenderer"/>'s <c>WalkFrameDriver</c>
/// construction — the driver's ctor takes a <see cref="WbDrawDispatcher"/>
/// directly (it calls <c>SubmitOrderedStream</c> itself; see that
/// class's own doc comment).</summary>
internal WbDrawDispatcher Dispatcher => _entities;
/// <summary>Campaign FW3.2b-2: forwards to
/// <see cref="WbDrawDispatcher.RequireWalkSubmission"/> — the frame/
/// encoder pair the walk driver submits its stream flushes into.</summary>
internal (IGpuFrame Frame, IGpuPassEncoder Encoder) RequireWalkSubmission() =>
_entities.RequireWalkSubmission();
/// <summary>Campaign FW3.2b-2: forwards to
/// <see cref="WbDrawDispatcher.WalkAttachmentExtent"/> — the real
/// viewport size for <c>WalkProductionFrameContext</c>.</summary>
internal (int Width, int Height)? WalkAttachmentExtent =>
_entities.WalkAttachmentExtent;
public void AbortFrame()
{
List<Exception>? 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<uint> visibleCellIds) =>
_envCells.PrepareRenderBatches(
frame.ViewProjection,
frame.CameraWorldPosition,
filter: visibleCellIds,
centerLbX: frame.RenderCenterLbX,
centerLbY: frame.RenderCenterLbY,
renderRadius: frame.RenderRadius);
public void DrawOpaqueCellShells(HashSet<uint> cellIds) =>
_envCells.Render(WbRenderPass.Opaque, cellIds);
public bool CellHasTransparentShell(uint cellId) =>
_envCells.CellHasTransparent(cellId);
public void DrawTransparentCellShellsOrdered(IReadOnlyList<uint> cellIds) =>
_envCells.RenderTransparentOrdered(cellIds);
/// <summary>
/// S4-c2 fix round 1 (M6, contract C4): retail <c>DrawEnvCell</c>
/// @0x0059f1c2 installs the environment detail surface (may be null)
/// before <c>DrawMesh</c> @0x0059f212 and clears it @0x0059f21a — detail
/// ON makes eligible subsets take Row 1 (immediate, WITH detail —
/// <see cref="EnvCellRenderer.TransparentDetailEnabled"/>'s own doc
/// comment); detail OFF still routes each real subset by the exact
/// <c>TextureBatchData.RetailSurfaceMask</c> retained on
/// <c>ObjectRenderBatch</c>. 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.
/// </summary>
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<uint> _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);
/// <summary>
/// Production dispatch for one cell after <see cref="EnvCellRenderer"/>
/// 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.
/// </summary>
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);
}
}
/// <summary>Delegate adapter retained for focused route/allocation tests;
/// production calls the interface overload above with <c>this</c>, so no
/// per-cell delegate conversion or retained callback enters the owner.</summary>
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);
}
}
/// <summary>
/// The queue-facing half of <see cref="SubmitOrDrawTransparentCellShell"/>:
/// retains the cell ids submitted to one fixed list in append order and replays
/// a drained slice through <see cref="EnvCellRenderer.RenderTransparentOrdered"/>
/// (production wiring) — one token per cell, never per subset (M6's
/// documented granularity residual). <see cref="IRetailAlphaDrawSource.PrepareAlphaDraws"/>'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 <see cref="EnvCellRenderer"/> type (marked
/// <see langword="internal"/>, not <see langword="private"/>) so
/// <c>EnvCellAlphaDrawSourceTests</c> can pin the queue mechanics
/// without standing up a GPU-backed renderer.
/// </summary>
internal delegate void RenderEnvCellsByRoute(
IReadOnlyList<uint> cellIds,
EnvCellTransparentRoute route,
bool detailSurfaceActive);
internal sealed class EnvCellAlphaDrawSource(
RenderEnvCellsByRoute renderTransparentOrdered,
EnvCellTransparentRoute route)
: IRetailAlphaDrawSource
{
private readonly List<uint> _pendingCellIds = new();
private readonly List<uint> _preparedCellIds = new();
private readonly List<uint> _drawScratch = new();
/// <summary>Returns this cell's token — its index into
/// <see cref="_pendingCellIds"/> at the moment it was added.</summary>
internal int AddPendingCellId(uint cellId)
{
int token = _pendingCellIds.Count;
_pendingCellIds.Add(cellId);
return token;
}
public void PrepareAlphaDraws(ReadOnlySpan<int> 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;
}
/// <summary>
/// S3 chunk 1 fix round 2 (§11.6 H1): retail draws the weather pass
/// EXACTLY ONCE per frame — <c>GameSky::Draw</c> @0x00506ff0 with
/// <c>arg2==1</c> runs AFTER <c>LScape::draw</c>'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 <c>DrawLandscapeSliceLate</c> 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
/// <c>GameSky::Draw(sky,1)</c>.
/// <para>
/// S3 chunk 4 (§10.2): the rain PARTICLE emitters
/// (<c>ParticleRenderPass.SkyPostScene</c>) moved here too, as ONE
/// unclipped submission — retail inserts emitters into one unclipped
/// alpha list (no per-doorway <c>clipSlot</c>), the same rule as the
/// mesh draw above.
/// </para>
/// <para>
/// Gated exactly as before —
/// <see cref="ShouldDrawWeatherOnce(bool,bool,uint)"/>, retail's
/// <c>SmartBox::is_player_outside</c> @0x00451e80 check ANDed with the
/// two render toggles — via <c>_sky?.RenderWeather(...)</c> (null-
/// conditional, matching every other <c>_sky?.RenderSky(...)</c> call
/// site in this codebase): a missing sky asset silently skips the GL
/// draw.
/// </para>
/// <para>
/// S3 chunk 4 (O3): the transcript "OC" print that used to live here
/// moved to <c>WalkFrameDriver.OnWeatherTurn</c>, fired by
/// <c>RetailFrameWalk.DrawLandscape</c> 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.
/// </para>
/// <para>
/// S3 chunk 4 fix round 1 (K2): the caller (<see
/// cref="RetailPViewRenderer.DrawLandscapeDynamicsPhase"/>) now gates
/// this call on <c>WalkFrameDriver.WeatherTurnFired</c> — 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
/// <see cref="ShouldDrawWeatherOnce(bool,bool,uint)"/> check is a
/// provably redundant safety net (the caller reads the same
/// <paramref name="frame"/>'s toggles the flag was computed from).
/// </para>
/// <para>
/// 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 <c>GameSky::Draw(sky,1)</c> 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.
/// </para>
/// </summary>
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);
}
}
/// <summary>
/// S3 chunk 1 fix round 2 (§11.6 H1): the weather pass's gate, extracted
/// as a pure predicate — retail's <c>SmartBox::is_player_outside</c>
/// @0x00451e80 check, <c>(player objcell_id &amp; 0xFFFF) &lt; 0x100</c>,
/// 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 <see cref="SkyRenderer"/>
/// — <see cref="DrawWeatherOnce"/> itself still needs one to actually
/// draw, but this predicate alone decides whether it would even try (and
/// therefore whether the OC print fires).
/// </summary>
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();
}
/// <summary>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 <c>portalsDrawnCount</c> BEFORE
/// <c>polyClipFinish</c> 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 <c>&lt;3</c> guard
/// (<c>DrawDepthFan</c>) draws nothing for it
/// (RetailPViewPassExecutorTests.DrawExitPortalMask_CountsAnUnclippableTwoVertexPolygon_ButDrawsNothing
/// pins exactly this).</summary>
public int DrawExitPortalMask(
RetailPViewFrameInput frame,
uint cellId,
ReadOnlySpan<Vector4> 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);
}
/// <summary><c>PView::DrawCells</c> @0x005a4840's own
/// <c>FlushAlphaList(0f)</c> @0x005a4872 (OH1 contract §7 site 3).</summary>
public void FlushLandscapeAlpha() =>
_alpha.Flush(RetailAlphaFlushSite.LandscapeFlush, 0f);
/// <summary><c>RenderDeviceD3D::DrawBuilding</c> @0x0059f2a0's own
/// <c>FlushAlphaList(0f)</c> @0x0059f30b (OH1 contract §7 site 1) — the
/// building alpha barrier. Distinct call from <see cref="FlushLandscapeAlpha"/>
/// 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).</summary>
internal void FlushBuildingAlpha() =>
_alpha.Flush(RetailAlphaFlushSite.DrawBuilding, 0f);
/// <summary><c>RenderDeviceD3D::DrawBlock</c> @0x005a17c0's (per-land-cell
/// loop head @0x005a18d0) own <c>FlushAlphaList(::flush)</c> @0x005a1a07,
/// the immutable global 0.75f pressure valve (OH1 contract §7 site
/// 2).</summary>
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);
/// <summary>Retail <c>D3DPolyRender::DrawPortalPolyInternal</c>
/// @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
/// <c>OtherCellId == 0xFFFF</c> that survives the ±12 boundary guard
/// (<see cref="WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard"/>)
/// 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 <c>WalkFrameDriver.OnInteriorFloodDrawTurn</c> counts
/// through <c>DrawExitSeals</c>'s return value.</summary>
private int DrawPortalDepthWrite(
uint cellId,
ReadOnlySpan<Vector4> 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<Vector3> 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.
}