refactor(render): extract typed retail pview passes

This commit is contained in:
Erik 2026-07-22 06:40:09 +02:00
parent 6d6e5b5fa5
commit 85239fb373
13 changed files with 1888 additions and 841 deletions

View file

@ -0,0 +1,576 @@
using System.Numerics;
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<uint> _outdoor = [];
private readonly HashSet<uint> _visible = [];
private readonly HashSet<uint> _dynamics = [];
public IReadOnlySet<uint> Outdoor => _outdoor;
public HashSet<uint> Visible => _visible;
public HashSet<uint> Dynamics => _dynamics;
public void BeginFrame()
{
_outdoor.Clear();
_visible.Clear();
_dynamics.Clear();
}
public void ReplaceOutdoor(IReadOnlyList<WorldEntity> owners)
{
_outdoor.Clear();
foreach (WorldEntity owner in owners)
_outdoor.Add(owner.Id);
}
}
/// <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>
internal sealed class RetailPViewPassExecutor : IRetailPViewPassExecutor
{
private readonly GL _gl;
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<uint> _noSceneParticleEntityIds = [];
/// <summary>
/// Borrowed until the next late landscape pass. The outdoor-root post-world
/// particle pass consumes this synchronously before another PView frame.
/// </summary>
public IReadOnlySet<uint> OutdoorSceneParticleEntityIds =>
_particleClassifications.Outdoor;
public RetailPViewPassExecutor(
GL gl,
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));
_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 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<Vector4> 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<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 DrawTransparentCellShells(HashSet<uint> cellIds) =>
_envCells.Render(WbRenderPass.Transparent, cellIds);
public void DrawTransparentCellShellsOrdered(IReadOnlyList<uint> cellIds) =>
_envCells.RenderTransparentOrdered(cellIds);
public void DrawEntityBucket(
RetailPViewFrameInput frame,
IReadOnlyList<WorldEntity> entities,
HashSet<uint>? visibleCellIds)
{
uint landblockId = frame.PlayerLandblockId ?? 0u;
var entry = (
landblockId,
Vector3.Zero,
Vector3.Zero,
entities,
(IReadOnlyDictionary<uint, WorldEntity>?)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<WorldEntity> 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.OutdoorEntities.Count > 0)
{
var sceneryEntry = (
frame.PlayerLandblockId ?? 0u,
Vector3.Zero,
Vector3.Zero,
context.OutdoorEntities,
(IReadOnlyDictionary<uint, WorldEntity>?)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.Dynamics.Count > 0)
{
var dynamicsEntry = (
frame.PlayerLandblockId ?? 0u,
Vector3.Zero,
Vector3.Zero,
context.Dynamics,
(IReadOnlyDictionary<uint, WorldEntity>?)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<uint> 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<WorldEntity> survivors)
{
if (_particles is null || _particleRenderer is null || survivors.Count == 0)
return;
HashSet<uint> 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<Vector3> 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);
}
}