acdream/src/AcDream.App/Rendering/RenderFrameResourceController.cs
Erik 28e1cf8029 refactor(render): extract world scene frame owner
Move fallback and PView world drawing, shared alpha, particles, visibility, selection, and diagnostics behind focused frame owners. Make exceptional frames failure-atomic by restoring the shared GL baseline and preserving primary alpha failures through cleanup.

Co-authored-by: Codex <codex@openai.com>
2026-07-22 07:22:09 +02:00

382 lines
13 KiB
C#

using AcDream.App.Diagnostics;
using AcDream.App.Input;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.World;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
internal readonly record struct RenderFrameFoundation(
bool PortalViewportVisible,
SkyKeyframe Sky,
AtmosphereSnapshot Atmosphere)
{
public bool EnvironOverrideActive =>
Atmosphere.Override != EnvironOverride.None;
}
internal interface IRenderFrameBeginResources
{
void Begin(int gpuSlot);
}
internal interface IRenderFrameSlotSource
{
int CurrentSlot { get; }
}
internal interface IRenderFrameClearPhase
{
RenderFrameFoundation Clear();
}
internal interface IRenderFrameFoundationSource
{
RenderFrameFoundation Foundation { get; }
}
internal interface IRenderFrameLivePreparation
{
void Prepare(int gpuSlot);
}
/// <summary>
/// Owns the pre-world resource transaction after GPU-flight begin. The three
/// typed phases preserve resource begin, clear/state, then live publication
/// order without exposing a renderer service bag.
/// </summary>
internal sealed class RenderFrameResourceController :
IRenderFrameResourcePhase,
IRenderFrameFoundationSource
{
private readonly IRenderFrameSlotSource _frameSlots;
private readonly IRenderFrameBeginResources _resources;
private readonly IRenderFrameClearPhase _clear;
private readonly IRenderFrameLivePreparation _live;
public RenderFrameResourceController(
IRenderFrameSlotSource frameSlots,
IRenderFrameBeginResources resources,
IRenderFrameClearPhase clear,
IRenderFrameLivePreparation live)
{
_frameSlots = frameSlots
?? throw new ArgumentNullException(nameof(frameSlots));
_resources = resources ?? throw new ArgumentNullException(nameof(resources));
_clear = clear ?? throw new ArgumentNullException(nameof(clear));
_live = live ?? throw new ArgumentNullException(nameof(live));
}
public RenderFrameFoundation Foundation { get; private set; }
public void Prepare(RenderFrameInput input)
{
int gpuSlot = _frameSlots.CurrentSlot;
_resources.Begin(gpuSlot);
Foundation = _clear.Clear();
_live.Prepare(gpuSlot);
}
}
/// <summary>Exact ordered begin calls for resources shared by the frame.</summary>
internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResources
{
private readonly TextureCache? _textures;
private readonly WbDrawDispatcher? _dispatcher;
private readonly EnvCellRenderer? _environmentCells;
private readonly PortalDepthMaskRenderer? _portalDepth;
private readonly TextRenderer? _worldText;
private readonly TextRenderer? _uiText;
private readonly ClipFrame? _clip;
private readonly TerrainModernRenderer? _terrain;
private readonly SceneLightingUboBinding? _lighting;
private readonly FrameProfiler _profiler;
private readonly GL _gl;
public RuntimeRenderFrameBeginResources(
TextureCache? textures,
WbDrawDispatcher? dispatcher,
EnvCellRenderer? environmentCells,
PortalDepthMaskRenderer? portalDepth,
TextRenderer? worldText,
TextRenderer? uiText,
ClipFrame? clip,
TerrainModernRenderer? terrain,
SceneLightingUboBinding? lighting,
FrameProfiler profiler,
GL gl)
{
_textures = textures;
_dispatcher = dispatcher;
_environmentCells = environmentCells;
_portalDepth = portalDepth;
_worldText = worldText;
_uiText = uiText;
_clip = clip;
_terrain = terrain;
_lighting = lighting;
_profiler = profiler ?? throw new ArgumentNullException(nameof(profiler));
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
}
public void Begin(int gpuSlot)
{
_textures?.BeginCompositeTextureFrame();
_textures?.TickCompositeTextureCache();
_dispatcher?.BeginFrame(gpuSlot);
_environmentCells?.BeginFrame(gpuSlot);
_portalDepth?.BeginFrame(gpuSlot);
_worldText?.BeginFrame(gpuSlot);
_uiText?.BeginFrame(gpuSlot);
_clip?.BeginFrame(gpuSlot);
_terrain?.BeginFrame(gpuSlot);
_lighting?.BeginFrame(gpuSlot);
_profiler.FrameBoundary(_gl);
}
}
internal interface IRenderFramePortalStateSource
{
bool IsPortalViewportVisible { get; }
uint ActiveDestinationCell { get; }
}
internal sealed class LocalPlayerTeleportRenderStateSource
: IRenderFramePortalStateSource
{
private readonly LocalPlayerTeleportController _teleport;
public LocalPlayerTeleportRenderStateSource(LocalPlayerTeleportController teleport)
{
_teleport = teleport ?? throw new ArgumentNullException(nameof(teleport));
}
public bool IsPortalViewportVisible => _teleport.IsPortalViewportVisible;
public uint ActiveDestinationCell => _teleport.ActiveDestinationCell;
}
/// <summary>Atmosphere clear and frame-global GL state establishment.</summary>
internal sealed class RuntimeRenderFrameClearPhase : IRenderFrameClearPhase
{
private readonly GL _gl;
private readonly WorldTimeService _worldTime;
private readonly WeatherSystem _weather;
private readonly IRenderFramePortalStateSource _portal;
private readonly ParticleVisibilityController _particleVisibility;
private readonly WorldRenderDiagnostics _diagnostics;
private readonly IRenderFrameGlState _frameGlState;
public RuntimeRenderFrameClearPhase(
GL gl,
WorldTimeService worldTime,
WeatherSystem weather,
IRenderFramePortalStateSource portal,
ParticleVisibilityController particleVisibility,
WorldRenderDiagnostics diagnostics,
IRenderFrameGlState frameGlState)
{
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
_worldTime = worldTime ?? throw new ArgumentNullException(nameof(worldTime));
_weather = weather ?? throw new ArgumentNullException(nameof(weather));
_portal = portal ?? throw new ArgumentNullException(nameof(portal));
_particleVisibility = particleVisibility
?? throw new ArgumentNullException(nameof(particleVisibility));
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
_frameGlState = frameGlState
?? throw new ArgumentNullException(nameof(frameGlState));
}
public RenderFrameFoundation Clear()
{
bool portalViewportVisible = _portal.IsPortalViewportVisible;
if (portalViewportVisible)
_particleVisibility.Reset();
SkyKeyframe sky = _worldTime.CurrentSky;
AtmosphereSnapshot atmosphere = _weather.Snapshot(in sky);
if (portalViewportVisible)
{
// SceneTool::BeginScene @ 0x0043DAD0 starts the replacement
// CreatureMode frame with an opaque black target.
_gl.ClearColor(0f, 0f, 0f, 1f);
}
else
{
_gl.ClearColor(
Math.Clamp(atmosphere.FogColor.X, 0f, 1f),
Math.Clamp(atmosphere.FogColor.Y, 0f, 1f),
Math.Clamp(atmosphere.FogColor.Z, 0f, 1f),
1f);
}
_frameGlState.RestoreFrameDefaults();
_gl.Clear(
ClearBufferMask.ColorBufferBit
| ClearBufferMask.DepthBufferBit
| ClearBufferMask.StencilBufferBit);
_diagnostics.EmitGlStateTripwireIfChanged(
AcDream.Core.Rendering.RenderingDiagnostics.ProbeGlStateEnabled);
return new RenderFrameFoundation(
portalViewportVisible,
sky,
atmosphere);
}
}
internal interface IRenderLoginStateSource
{
bool IsWaitingForLogin { get; }
}
internal sealed class RenderLoginStateSource : IRenderLoginStateSource
{
private readonly bool _liveMode;
private readonly ILocalPlayerModeSource _mode;
public RenderLoginStateSource(bool liveMode, ILocalPlayerModeSource mode)
{
_liveMode = liveMode;
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
}
public bool IsWaitingForLogin => _liveMode && !_mode.ChaseModeEverEntered;
}
internal interface ILoginRevealCellSource
{
bool TryGet(out uint cellId);
}
internal sealed class LiveLoginRevealCellSource : ILoginRevealCellSource
{
private readonly LiveEntityRuntime _entities;
private readonly ILocalPlayerIdentitySource _identity;
public LiveLoginRevealCellSource(
LiveEntityRuntime entities,
ILocalPlayerIdentitySource identity)
{
_entities = entities ?? throw new ArgumentNullException(nameof(entities));
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
}
public bool TryGet(out uint cellId)
{
cellId = 0;
if (!_entities.TryGetSnapshot(_identity.ServerGuid, out var playerSpawn)
|| playerSpawn.Position is not { } position)
{
return false;
}
cellId = position.LandblockId;
return cellId != 0;
}
}
/// <summary>Render-thread mesh publication, reveal evaluation, and VFX begin.</summary>
internal sealed class RuntimeRenderFrameLivePreparation
: IRenderFrameLivePreparation
{
private readonly TextureCache? _textures;
private readonly WbMeshAdapter? _meshes;
private readonly WorldRevealCoordinator? _worldReveal;
private readonly IRenderFramePortalStateSource _portal;
private readonly IRenderLoginStateSource _login;
private readonly ILoginRevealCellSource _loginCell;
private readonly ParticleRenderer? _particles;
private readonly FrameProfiler _profiler;
private readonly bool _diagnosticsEnabled;
private readonly RollingTimingSampleWindow _uploadTiming = new(256);
public RuntimeRenderFrameLivePreparation(
TextureCache? textures,
WbMeshAdapter? meshes,
WorldRevealCoordinator? worldReveal,
IRenderFramePortalStateSource portal,
IRenderLoginStateSource login,
ILoginRevealCellSource loginCell,
ParticleRenderer? particles,
FrameProfiler profiler,
bool diagnosticsEnabled)
{
_textures = textures;
_meshes = meshes;
_worldReveal = worldReveal;
_portal = portal ?? throw new ArgumentNullException(nameof(portal));
_login = login ?? throw new ArgumentNullException(nameof(login));
_loginCell = loginCell ?? throw new ArgumentNullException(nameof(loginCell));
_particles = particles;
_profiler = profiler ?? throw new ArgumentNullException(nameof(profiler));
_diagnosticsEnabled = diagnosticsEnabled;
}
public RollingTimingPercentiles UploadTiming => _uploadTiming.Snapshot();
public void Prepare(int gpuSlot)
{
_textures?.TickSurfaceHistogramDumpIfEnabled();
long start = _diagnosticsEnabled
? System.Diagnostics.Stopwatch.GetTimestamp()
: 0L;
using (var uploadStage = _profiler.BeginStage(FrameStage.Upload))
{
_meshes?.Tick();
uint revealCell = _portal.ActiveDestinationCell;
if (revealCell == 0
&& _login.IsWaitingForLogin
&& _loginCell.TryGet(out uint loginCell))
{
revealCell = loginCell;
}
if (revealCell != 0)
_worldReveal?.PrepareAndEvaluate(revealCell);
_particles?.BeginFrame(gpuSlot);
}
if (_diagnosticsEnabled)
{
_uploadTiming.PushStopwatchTicks(
System.Diagnostics.Stopwatch.GetTimestamp() - start);
}
}
}
/// <summary>Owns the render-time weather clock and deterministic day index.</summary>
internal sealed class RenderWeatherFrameController
{
private readonly WorldTimeService _worldTime;
private readonly WeatherSystem _weather;
private double _elapsedSeconds;
public RenderWeatherFrameController(
WorldTimeService worldTime,
WeatherSystem weather)
{
_worldTime = worldTime ?? throw new ArgumentNullException(nameof(worldTime));
_weather = weather ?? throw new ArgumentNullException(nameof(weather));
}
internal double ElapsedSeconds => _elapsedSeconds;
public void Tick(double deltaSeconds)
{
DerethDateTime.Calendar calendar = _worldTime.CurrentCalendar;
int dayIndex = calendar.Year
* (DerethDateTime.DaysInAMonth * DerethDateTime.MonthsInAYear)
+ (int)calendar.Month * DerethDateTime.DaysInAMonth
+ (calendar.Day - 1);
_weather.Tick(
nowSeconds: _elapsedSeconds,
dayIndex,
dtSeconds: (float)deltaSeconds);
_elapsedSeconds += deltaSeconds;
}
}