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.Core.Rendering; using AcDream.Core.Vfx; using AcDream.Core.World; using Silk.NET.Windowing; namespace AcDream.App.Rendering; internal readonly record struct RetailPViewFramebufferSize(int Width, int Height); internal interface IRetailPViewFramebufferSource { RetailPViewFramebufferSize Capture(); } internal sealed class SilkRetailPViewFramebufferSource(IWindow window) : IRetailPViewFramebufferSource { private readonly IWindow _window = window ?? throw new ArgumentNullException(nameof(window)); public RetailPViewFramebufferSize Capture() { var size = _window.FramebufferSize; return new RetailPViewFramebufferSize(size.X, size.Y); } } 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; } internal sealed class RetailPViewParticleClassifications { private readonly HashSet _outdoor = []; private readonly HashSet _visible = []; private readonly HashSet _dynamics = []; public IReadOnlySet Outdoor => _outdoor; public HashSet Visible => _visible; public HashSet Dynamics => _dynamics; public void BeginFrame() { _outdoor.Clear(); _visible.Clear(); _dynamics.Clear(); } public void ReplaceOutdoor(IReadOnlyList owners) { _outdoor.Clear(); foreach (WorldEntity owner in owners) _outdoor.Add(owner.Id); } public void ReplaceOutdoor(IReadOnlySet ownerIds) { _outdoor.Clear(); _outdoor.UnionWith(ownerIds); } } /// /// 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. /// internal interface IOutdoorSceneParticleOwnerSource { IReadOnlySet OutdoorSceneParticleEntityIds { get; } } /// /// 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 : IOutdoorSceneParticleOwnerSource { 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 RetailPViewParticleClassifications _particleClassifications = new(); private readonly HashSet _noSceneParticleEntityIds = []; // ACDREAM_PROBE_WALK_ROOT / cathedral FW4: observation-only counter for // the walk-owned far-Z portal punches. internal int WalkLookInPunchCountThisFrame { get; private set; } /// /// Borrowed until the next late landscape pass. The outdoor-root post-world /// particle pass consumes this synchronously before another PView frame. /// public IReadOnlySet OutdoorSceneParticleEntityIds => _particleClassifications.Outdoor; 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)); } public void BeginFrame() { _particleClassifications.BeginFrame(); if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled) { AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase = "pre"; WalkLookInPunchCountThisFrame = 0; } } /// 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(() => _envCells.SetClipRouting(null)); TryAbort(_entities.ClearClipRouting); TryAbort(_particleClassifications.BeginFrame); 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(int terrainUploadCount) => _surface.PrepareClipFrame(terrainUploadCount); public void SetTerrainClip(ReadOnlySpan planes) => _surface.SetTerrainClip(planes); public void ClearClipRouting() => _entities.ClearClipRouting(); public void UseIndoorMembershipOnlyRouting() { // Retail viewcone-checks meshes and draws whole cell shells. This clears // any terrain-slice routing before the indoor membership passes. _envCells.SetClipRouting(null); _entities.ClearClipRouting(); } 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 void SetCellShellClipRouting(IReadOnlyDictionary? routing) => _envCells.SetClipRouting(routing); public bool CellHasTransparentShell(uint cellId) => _envCells.CellHasTransparent(cellId); public void DrawTransparentCellShellsOrdered(IReadOnlyList cellIds) => _envCells.RenderTransparentOrdered(cellIds); public void DrawLandscapeSliceLate( RetailPViewFrameInput frame, RetailPViewLandscapeLateSliceContext context) { ClipViewSlice slice = context.Slice; bool scissor = BeginDoorwayScissor(slice.NdcAabb); _surface.BindTerrainClip(); DisableClipDistances(); if (context.Dynamics.Count > 0) { var dynamicsEntry = ( frame.PlayerLandblockId ?? 0u, Vector3.Zero, Vector3.Zero, context.Dynamics, (IReadOnlyDictionary?)null); _entities.Draw( frame.Camera, new[] { dynamicsEntry }, frame.Frustum, neverCullLandblockId: frame.PlayerLandblockId, visibleCellIds: null, animatedEntityIds: frame.AnimatedEntityIds); } // Late-stage particle owners submit ONCE per frame through // DrawLandscapeStaticParticles after the slice loop (retail: one // unclipped alpha-list insertion per emitter), not per slice here. EnableClipDistances(); // Retail GameSky::Draw @0x00506ff0 gates the WEATHER pass (arg2==1) // on SmartBox::is_player_outside @0x00451e80 — Ghidra-arbitrated: // (player objcell_id & 0xFFFF) < 0x100. The sky pass always draws; // the rain draws ONLY while the PLAYER stands in an outdoor cell. // That one boolean is retail's entire rain confinement (the // owner-reported indoor/seam rain at the cathedral: rain on the // ledges, none the instant the player crosses into an interior // cell) — no depth or view-clip mechanism is involved. bool playerOutside = (frame.PlayerCellId & 0xFFFFu) < 0x100u; if (frame.RenderSky && frame.RenderWeather && playerOutside) { _sky?.RenderWeather( frame.Camera, frame.CameraWorldPosition, frame.DayFraction, frame.ActiveDayGroup, frame.SkyKeyframe, frame.EnvironOverrideActive); DisableClipDistances(); if (_particles is not null && _particleRenderer is not null) { _particleRenderer.Draw( frame.Camera, frame.CameraWorldPosition, ParticleRenderPass.SkyPostScene, clipSlot: checked((uint)slice.Slot)); } } else { DisableClipDistances(); } if (scissor) _surface.EndScissor(); _entities.ClearClipRouting(); DisableClipDistances(); } public void DrawLandscapeStaticParticles( RetailPViewFrameInput frame, RetailPViewLandscapeStaticParticleContext context) { // One unclipped submission per owner 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. DisableClipDistances(); _particleClassifications.ReplaceOutdoor(context.ParticleOwnerIds); ProbeWalkParticleRoute("static-owners", _particleClassifications.Outdoor); if (_particleClassifications.Outdoor.Count > 0 && _particles is not null && _particleRenderer is not null) { _particleRenderer.DrawForOwners( frame.Camera, frame.CameraWorldPosition, ParticleRenderPass.Scene, _particleClassifications.Outdoor, clipSlot: 0); } _entities.ClearClipRouting(); DisableClipDistances(); } public void ClearInteriorDepth() { if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled) AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase = "post"; _surface.ClearInteriorDepth(); } public void DrawExitPortalMask( RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => DrawPortalDepthWrite(context, 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. DisableClipDistances(); ProbeWalkParticleRoute( outdoorCells ? "unattached-outdoor" : "unattached-interior", null); _particleRenderer.DrawForOwners( frame.Camera, frame.CameraWorldPosition, ParticleRenderPass.Scene, _noSceneParticleEntityIds, includeUnattached: true, clipSlot: 0, unattachedCellScope: outdoorCells ? UnattachedEmitterCellScope.OutdoorCells : UnattachedEmitterCellScope.InteriorCells); } /// ACDREAM_PROBE_WALK_ROOT companion (throwaway, dies with the /// cathedral falls investigation): logs each particle submission route /// with its owner count + the pre/post-depth-clear phase tag. private static void ProbeWalkParticleRoute(string route, IReadOnlySet? owners) { if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled) return; string ids = ""; if (owners is not null) { var sb = new System.Text.StringBuilder(); int shown = 0; foreach (uint id in owners) { if (shown++ == 6) { sb.Append(",…"); break; } if (sb.Length > 0) sb.Append(','); sb.Append(id.ToString("x")); } ids = $" n={owners.Count} ids=[{sb}]"; // Phase I cathedral instrumentation: the REAL falls owners // (outdoor landblock statics 0xCF418000-13 per the DAT-geometry // report) — print EVERY submission that carries one, with the // exact watched ids, unconditionally (never rate-limited; these // are the money lines the six fix rounds never had). System.Text.StringBuilder? falls = null; foreach (uint id in owners) { if (id >= 0xCF418000u && id <= 0xCF418014u) { falls ??= new System.Text.StringBuilder(); if (falls.Length > 0) falls.Append(','); falls.Append(id.ToString("x")); } } if (falls is not null) { Console.WriteLine( $"[falls] route={route} phase=" + $"{AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase} " + $"ids=[{falls}]"); } } Console.WriteLine( $"[walk-part] {route}{ids} phase={AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase}"); } public void FlushLandscapeAlpha() => _alpha.Flush(); public void DrawCellParticles( RetailPViewFrameInput frame, RetailPViewCellSliceContext context) { if (_particles is null || _particleRenderer is null || context.ParticleOwnerIds.Count == 0) { return; } HashSet visible = _particleClassifications.Visible; visible.Clear(); visible.UnionWith(context.ParticleOwnerIds); if (visible.Count == 0) return; DisableClipDistances(); ProbeWalkParticleRoute("cell-owners", visible); // Retail never clips cell particles to a portal view: the owner // cell's walls own occlusion via the depth test at the alpha flush. _particleRenderer.DrawForOwners( frame.Camera, frame.CameraWorldPosition, ParticleRenderPass.Scene, visible, clipSlot: 0); DisableClipDistances(); } 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); private void DrawPortalDepthWrite( RetailPViewCellSliceContext context, RetailPViewFrameInput frame, bool forceFarZ, int? onlyPortalIndex = null) { // 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. if (_portalDepthMask is null) return; LoadedCell? cell = frame.Cells.Find(context.CellId); if (cell is null) return; 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]; if (localVertices.Length < 3) 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, context.CellId, index, forceFarZ, world[..count]); _portalDepthMask.DrawDepthFan( world[..count], frame.ViewProjection, context.Slice.Planes, forceFarZ); } } private bool BeginDoorwayScissor(Vector4 ndcAabb) => _surface.BeginScissor(ndcAabb); private void EnableClipDistances() => _surface.EnableClipDistances(); private void DisableClipDistances() => _surface.DisableClipDistances(); }