using System.Numerics; 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.OpenGL; 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); } } /// /// 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; } } internal readonly record struct RenderFrameEntityDrawRequest( RenderFrameView View, RenderFrameCandidateRoute Route, int RouteIndex, uint CellId, uint TupleLandblockId); internal interface IRenderFrameEntityPassExecutor { void BeginEntityFrame(in RenderFrameView view); bool DrawEntityRoute( ICamera camera, in RenderFrameView view, RenderFrameCandidateRoute route, int routeIndex, uint cellId, uint tupleLandblockId); void CompleteEntityFrame(in RenderFrameView view); void AbortEntityFrame(); } internal sealed class RetailPViewPassExecutor : IRetailPViewPassExecutor, IRenderFrameEntityPassExecutor, IOutdoorSceneParticleOwnerSource { private readonly GL _gl; private readonly IRenderFrameGlState _frameGlState; private readonly IRetailPViewFramebufferSource _framebuffer; 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 = []; /// /// 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( GL gl, IRenderFrameGlState frameGlState, IRetailPViewFramebufferSource framebuffer, ClipFrame clipFrame, TerrainModernRenderer? terrain, EnvCellRenderer envCells, WbDrawDispatcher entities, SkyRenderer? sky, ParticleSystem? particles, ParticleRenderer? particleRenderer, PortalDepthMaskRenderer? portalDepthMask, RetailAlphaQueue alpha, WorldRenderDiagnostics diagnostics, TerrainDrawDiagnosticsController terrainDiagnostics) { _gl = gl ?? throw new ArgumentNullException(nameof(gl)); _frameGlState = frameGlState ?? throw new ArgumentNullException(nameof(frameGlState)); _framebuffer = framebuffer ?? throw new ArgumentNullException(nameof(framebuffer)); _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(); } public void BeginEntityFrame(in RenderFrameView view) => _entities.BeginPackedProductionFrame(in view); public bool DrawEntityRoute( ICamera camera, in RenderFrameView view, RenderFrameCandidateRoute route, int routeIndex, uint cellId, uint tupleLandblockId) => _entities.DrawPackedProductionRoute( camera, in view, route, routeIndex, cellId, tupleLandblockId); public void CompleteEntityFrame(in RenderFrameView view) => _entities.CompletePackedProductionFrame(in view); public void AbortEntityFrame() => _entities.AbortPackedProductionFrame(); public void AbortFrame() { List? failures = null; TryAbort(_frameGlState.RestoreFrameDefaults); 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 AssembleClipFrame( PortalVisibilityFrame portalFrame, ClipFrameAssembly reuseAssembly) => ClipFrameAssembler.Assemble(_clipFrame, portalFrame, reuseAssembly); public void PrepareClipFrame(int terrainUploadCount) { // Allocate every terrain record before issuing the first draw. BufferData // must not replace the arena while an earlier slice can reference it. _clipFrame.ReserveTerrainUploads(_gl, terrainUploadCount); _clipFrame.UploadRegions(_gl); _entities.SetClipRegionSsbo(_clipFrame.RegionSsbo); _envCells.SetClipRegionSsbo(_clipFrame.RegionSsbo); UploadTerrainClip(); } public void SetTerrainClip(ReadOnlySpan planes) { _clipFrame.SetTerrainClip(planes); UploadTerrainClip(); } 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 bool CellHasTransparentShell(uint cellId) => _envCells.CellHasTransparent(cellId); public void DrawTransparentCellShells(HashSet cellIds) => _envCells.Render(WbRenderPass.Transparent, cellIds); public void DrawTransparentCellShellsOrdered(IReadOnlyList cellIds) => _envCells.RenderTransparentOrdered(cellIds); public void DrawEntityBucket( RetailPViewFrameInput frame, IReadOnlyList entities, HashSet? visibleCellIds) { uint landblockId = frame.PlayerLandblockId ?? 0u; var entry = ( landblockId, Vector3.Zero, Vector3.Zero, entities, (IReadOnlyDictionary?)null); _entities.Draw( frame.Camera, new[] { entry }, frame.Frustum, neverCullLandblockId: frame.PlayerLandblockId, visibleCellIds: visibleCellIds, animatedEntityIds: frame.AnimatedEntityIds); } public void EmitClipRouteProbe( ClipFrameAssembly clipAssembly, ClipViewSlice slice, int sliceIndex) => _diagnostics.EmitClipRouteProbe( RenderingDiagnostics.ProbeClipRouteEnabled, _clipFrame, clipAssembly, slice, sliceIndex); public void EmitOutStageOwner( WorldEntity entity, Vector3 sphereCenter, float sphereRadius, int sliceIndex, bool passed) => _diagnostics.EmitOutStageOwner( RenderingDiagnostics.ProbeOutStageEnabled, RenderingDiagnostics.DumpEntitySourceIds, entity, sphereCenter, sphereRadius, sliceIndex, passed); public void EmitOutStageRouting( int sliceIndex, IReadOnlyList entities, ViewconeCuller viewcone) => _diagnostics.EmitOutStageRouting( RenderingDiagnostics.ProbeOutStageEnabled, sliceIndex, entities, viewcone); public void EmitPhantomObjects(uint cellId, int survivorCount) => _diagnostics.EmitPhantomObjects( RenderingDiagnostics.ProbePhantomEnabled, cellId, survivorCount); public void DrawLandscapeSlice( RetailPViewFrameInput frame, RetailPViewLandscapeSliceContext context) { ClipViewSlice slice = context.Slice; bool scissor = BeginDoorwayScissor(slice.NdcAabb); _diagnostics.EmitClipRouteScissorProbe( RenderingDiagnostics.ProbeClipRouteEnabled, scissor, slice.NdcAabb); _clipFrame.BindTerrainClip(_gl); EnableClipDistances(); if (frame.RenderSky) { _sky?.RenderSky( frame.Camera, frame.CameraWorldPosition, frame.DayFraction, frame.ActiveDayGroup, frame.SkyKeyframe, frame.EnvironOverrideActive); } DisableClipDistances(); if (frame.RenderSky && _particles is not null && _particleRenderer is not null) { _particleRenderer.Draw( frame.Camera, frame.CameraWorldPosition, ParticleRenderPass.SkyPreScene); } EnableClipDistances(); _terrainDiagnostics.Begin(); _terrain?.Draw( frame.Camera, frame.Frustum, neverCullLandblockId: frame.PlayerLandblockId, clipPlanes: slice.Planes, ndcClipAabb: slice.NdcAabb); _terrainDiagnostics.Complete(); DisableClipDistances(); if (context.EntityDraw is RenderFrameEntityDrawRequest request) { RenderFrameView drawView = request.View; _entities.DrawPackedProductionRoute( frame.Camera, in drawView, request.Route, request.RouteIndex, request.CellId, request.TupleLandblockId); } else if (context.OutdoorEntities.Count > 0) { var sceneryEntry = ( frame.PlayerLandblockId ?? 0u, Vector3.Zero, Vector3.Zero, context.OutdoorEntities, (IReadOnlyDictionary?)null); _entities.Draw( frame.Camera, new[] { sceneryEntry }, frame.Frustum, neverCullLandblockId: frame.PlayerLandblockId, visibleCellIds: null, animatedEntityIds: frame.AnimatedEntityIds); } if (scissor) _gl.Disable(EnableCap.ScissorTest); DisableClipDistances(); } public void DrawLandscapeSliceLate( RetailPViewFrameInput frame, RetailPViewLandscapeLateSliceContext context) { ClipViewSlice slice = context.Slice; bool scissor = BeginDoorwayScissor(slice.NdcAabb); _clipFrame.BindTerrainClip(_gl); DisableClipDistances(); if (context.EntityDraw is RenderFrameEntityDrawRequest request) { RenderFrameView drawView = request.View; _entities.DrawPackedProductionRoute( frame.Camera, in drawView, request.Route, request.RouteIndex, request.CellId, request.TupleLandblockId); } else 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); } _particleClassifications.ReplaceOutdoor(context.ParticleOwners); _diagnostics.EmitOutStageParticles( RenderingDiagnostics.ProbeOutStageEnabled, _particles, _particleClassifications.Outdoor); if (!frame.RootCell.IsOutdoorNode && _particleClassifications.Outdoor.Count > 0 && _particles is not null && _particleRenderer is not null) { _particleRenderer.DrawForOwners( frame.Camera, frame.CameraWorldPosition, ParticleRenderPass.Scene, _particleClassifications.Outdoor); } EnableClipDistances(); if (frame.RenderSky && frame.RenderWeather) { _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); } } else { DisableClipDistances(); } if (scissor) _gl.Disable(EnableCap.ScissorTest); DisableClipDistances(); } public void ClearInteriorDepth() { _gl.Disable(EnableCap.ScissorTest); _gl.DepthMask(true); _gl.Clear(ClearBufferMask.DepthBufferBit); } public void DrawExitPortalMask( RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => DrawPortalDepthWrite(context, frame, forceFarZ: frame.RootCell.IsOutdoorNode); public void DrawLookInPortalPunch( RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => DrawPortalDepthWrite(context, frame, forceFarZ: true); public void DrawUnattachedSceneParticles(RetailPViewFrameInput frame) { if (_particles is null || _particleRenderer is null) return; DisableClipDistances(); _particleRenderer.DrawForOwners( frame.Camera, frame.CameraWorldPosition, ParticleRenderPass.Scene, _noSceneParticleEntityIds, includeUnattached: true); } public void FlushLandscapeAlpha() => _alpha.Flush(); public void DrawCellParticles( RetailPViewFrameInput frame, RetailPViewCellSliceContext context) { if (_particles is null || _particleRenderer is null || context.CellEntities.Count == 0) { return; } HashSet visible = _particleClassifications.Visible; visible.Clear(); foreach (WorldEntity entity in context.CellEntities) visible.Add(entity.Id); if (visible.Count == 0) return; DisableClipDistances(); _particleRenderer.DrawForOwners( frame.Camera, frame.CameraWorldPosition, ParticleRenderPass.Scene, visible); DisableClipDistances(); } public void DrawDynamicsParticles( RetailPViewFrameInput frame, IReadOnlyList survivors) { if (_particles is null || _particleRenderer is null || survivors.Count == 0) return; HashSet dynamics = _particleClassifications.Dynamics; dynamics.Clear(); foreach (WorldEntity entity in survivors) dynamics.Add(entity.Id); if (dynamics.Count == 0) return; DisableClipDistances(); _particleRenderer.DrawForOwners( frame.Camera, frame.CameraWorldPosition, ParticleRenderPass.Scene, dynamics); DisableClipDistances(); } public void EmitDiagnostics( RetailPViewFrameInput frame, RetailPViewFrameResult result) => _diagnostics.EmitRetailPViewDiagnostics( RenderingDiagnostics.ProbeViewerEnabled, RenderingDiagnostics.ProbeVisibilityEnabled, RenderingDiagnostics.ProbeFlapEnabled, result, frame.RootCell, frame.ViewerCellId, frame.PlayerCellId, frame.CameraWorldPosition, frame.PlayerViewPosition, frame.CameraView, frame.CameraCellResolution); private void UploadTerrainClip() { TerrainClipBufferBinding binding = _clipFrame.UploadTerrainClip(_gl); _terrain?.SetClipUbo(binding); } private void DrawPortalDepthWrite( RetailPViewCellSliceContext context, RetailPViewFrameInput frame, bool forceFarZ) { // 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 (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++) { world[vertex] = Vector3.Transform( localVertices[vertex], cell.WorldTransform); world[vertex].Z += PortalVisibilityBuilder.ShellDrawLiftZ; } _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) { RetailPViewFramebufferSize framebuffer = _framebuffer.Capture(); var box = NdcScissorRect.ToPixels( ndcAabb, framebuffer.Width, framebuffer.Height); _gl.Enable(EnableCap.ScissorTest); _gl.Scissor(box.X, box.Y, (uint)box.Width, (uint)box.Height); return true; } private void EnableClipDistances() { for (int index = 0; index < ClipFrame.MaxPlanes; index++) _gl.Enable(EnableCap.ClipDistance0 + index); } private void DisableClipDistances() { for (int index = 0; index < ClipFrame.MaxPlanes; index++) _gl.Disable(EnableCap.ClipDistance0 + index); } }