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>
This commit is contained in:
parent
85239fb373
commit
28e1cf8029
25 changed files with 2679 additions and 844 deletions
345
src/AcDream.App/Rendering/WorldSceneRenderer.cs
Normal file
345
src/AcDream.App/Rendering/WorldSceneRenderer.cs
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
using AcDream.App.Rendering.Selection;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.Core.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal interface IWorldScenePViewRenderer
|
||||
{
|
||||
IReadOnlySet<uint> OutdoorSceneParticleEntityIds { get; }
|
||||
|
||||
RetailPViewFrameResult DrawInside(RetailPViewFrameInput input);
|
||||
|
||||
void AbortFrame();
|
||||
}
|
||||
|
||||
internal sealed class WorldScenePViewRenderer : IWorldScenePViewRenderer
|
||||
{
|
||||
private readonly RetailPViewRenderer _renderer;
|
||||
private readonly IRetailPViewPassExecutor _passes;
|
||||
private readonly IOutdoorSceneParticleOwnerSource _particles;
|
||||
|
||||
public WorldScenePViewRenderer(
|
||||
RetailPViewRenderer renderer,
|
||||
IRetailPViewPassExecutor passes,
|
||||
IOutdoorSceneParticleOwnerSource particles)
|
||||
{
|
||||
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
|
||||
_passes = passes ?? throw new ArgumentNullException(nameof(passes));
|
||||
_particles = particles ?? throw new ArgumentNullException(nameof(particles));
|
||||
}
|
||||
|
||||
public IReadOnlySet<uint> OutdoorSceneParticleEntityIds =>
|
||||
_particles.OutdoorSceneParticleEntityIds;
|
||||
|
||||
public RetailPViewFrameResult DrawInside(RetailPViewFrameInput input) =>
|
||||
_renderer.DrawInside(input, _passes);
|
||||
|
||||
public void AbortFrame() => _passes.AbortFrame();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns the normal-world render transaction between shared frame-resource
|
||||
/// preparation and private presentation. It preserves retail's one PView
|
||||
/// decision: a resolved viewer root uses DrawInside; only an unresolved root
|
||||
/// uses the flat safety path.
|
||||
/// </summary>
|
||||
internal sealed class WorldSceneRenderer : IWorldSceneFramePhase
|
||||
{
|
||||
private readonly IRenderFrameFoundationSource _foundation;
|
||||
private readonly IRenderLoginStateSource _login;
|
||||
private readonly IWorldSceneSkyStateSource _sky;
|
||||
private readonly IWorldRenderFrameBuilder _frames;
|
||||
private readonly IWorldSceneEntitySource _entities;
|
||||
private readonly IWorldSceneSelectionFrame? _selection;
|
||||
private readonly IWorldSceneAlphaFrame _alpha;
|
||||
private readonly IWorldSceneParticleVisibility _particleVisibility;
|
||||
private readonly IWorldScenePViewRenderer _pview;
|
||||
private readonly IRetailPViewCellSource _pviewCells;
|
||||
private readonly IWorldScenePassExecutor _passes;
|
||||
private readonly IWorldRenderRangeSource _renderRange;
|
||||
private readonly IWorldSceneDiagnostics _diagnostics;
|
||||
|
||||
public WorldSceneRenderer(
|
||||
IRenderFrameFoundationSource foundation,
|
||||
IRenderLoginStateSource login,
|
||||
IWorldSceneSkyStateSource sky,
|
||||
IWorldRenderFrameBuilder frames,
|
||||
IWorldSceneEntitySource entities,
|
||||
IWorldSceneSelectionFrame? selection,
|
||||
IWorldSceneAlphaFrame alpha,
|
||||
IWorldSceneParticleVisibility particleVisibility,
|
||||
IWorldScenePViewRenderer pview,
|
||||
IRetailPViewCellSource pviewCells,
|
||||
IWorldScenePassExecutor passes,
|
||||
IWorldRenderRangeSource renderRange,
|
||||
IWorldSceneDiagnostics diagnostics)
|
||||
{
|
||||
_foundation = foundation ?? throw new ArgumentNullException(nameof(foundation));
|
||||
_login = login ?? throw new ArgumentNullException(nameof(login));
|
||||
_sky = sky ?? throw new ArgumentNullException(nameof(sky));
|
||||
_frames = frames ?? throw new ArgumentNullException(nameof(frames));
|
||||
_entities = entities ?? throw new ArgumentNullException(nameof(entities));
|
||||
_selection = selection;
|
||||
_alpha = alpha ?? throw new ArgumentNullException(nameof(alpha));
|
||||
_particleVisibility = particleVisibility
|
||||
?? throw new ArgumentNullException(nameof(particleVisibility));
|
||||
_pview = pview ?? throw new ArgumentNullException(nameof(pview));
|
||||
_pviewCells = pviewCells ?? throw new ArgumentNullException(nameof(pviewCells));
|
||||
_passes = passes ?? throw new ArgumentNullException(nameof(passes));
|
||||
_renderRange = renderRange ?? throw new ArgumentNullException(nameof(renderRange));
|
||||
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
|
||||
}
|
||||
|
||||
public WorldRenderFrameOutcome Render(RenderFrameInput input)
|
||||
{
|
||||
_ = input;
|
||||
bool selectionFrameStarted = _selection is not null;
|
||||
bool worldFrameStarted = false;
|
||||
bool pviewFrameStarted = false;
|
||||
try
|
||||
{
|
||||
_selection?.BeginFrame();
|
||||
RenderFrameFoundation foundation = _foundation.Foundation;
|
||||
if (foundation.PortalViewportVisible)
|
||||
{
|
||||
_selection?.CompleteFrame();
|
||||
selectionFrameStarted = false;
|
||||
return default;
|
||||
}
|
||||
|
||||
// Set before BeginFrame so an already-poisoned queue is recovered by
|
||||
// the same abort path instead of making every later frame fail.
|
||||
worldFrameStarted = true;
|
||||
_alpha.BeginFrame();
|
||||
WorldRenderFrame world = _frames.Build(
|
||||
in foundation,
|
||||
_login.IsWaitingForLogin,
|
||||
_sky.ActiveDayGroup);
|
||||
_passes.BeginFrame();
|
||||
|
||||
WorldCameraFrame camera = world.Camera;
|
||||
WorldRootFrame roots = world.Roots;
|
||||
LoadedCell? clipRoot = world.ClipRoot;
|
||||
bool renderSky = roots.RenderSky;
|
||||
bool drawSkyThisFrame = false;
|
||||
bool terrainDrawn = false;
|
||||
bool skyDrawn = false;
|
||||
bool depthClear = false;
|
||||
bool outdoorSceneryDrawn = false;
|
||||
int liveDynamicDrawnCount = 0;
|
||||
string sceneParticles = "none";
|
||||
TerrainClipMode terrainClipMode = TerrainClipMode.Planes;
|
||||
RetailPViewFrameResult? pviewResult = null;
|
||||
|
||||
if (clipRoot is null)
|
||||
{
|
||||
_passes.PrepareFlatWorldClip();
|
||||
drawSkyThisFrame = renderSky;
|
||||
skyDrawn = drawSkyThisFrame;
|
||||
if (drawSkyThisFrame)
|
||||
{
|
||||
_passes.DrawFlatSky(
|
||||
in camera,
|
||||
in foundation,
|
||||
_sky.ActiveDayGroup,
|
||||
_sky.DayFraction);
|
||||
}
|
||||
|
||||
// Retail keeps the live sky during EnterWorld while suppressing
|
||||
// terrain and object geometry until chase mode has engaged.
|
||||
if (_login.IsWaitingForLogin)
|
||||
return CompleteSkippedWorld();
|
||||
|
||||
_passes.DrawFlatTerrain(in camera, roots.PlayerLandblockId);
|
||||
terrainDrawn = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
terrainClipMode = TerrainClipMode.Skip;
|
||||
}
|
||||
|
||||
if (clipRoot is not null)
|
||||
{
|
||||
pviewFrameStarted = true;
|
||||
pviewResult = _pview.DrawInside(
|
||||
new RetailPViewFrameInput
|
||||
{
|
||||
RootCell = clipRoot,
|
||||
NearbyBuildingCells = world.Buildings.NearbyBuildingCells,
|
||||
ViewerEyePos = roots.ViewerEyePosition,
|
||||
ViewProjection = camera.ViewProjection,
|
||||
Cells = _pviewCells,
|
||||
Camera = camera.Camera,
|
||||
CameraWorldPosition = camera.Position,
|
||||
Frustum = camera.Frustum,
|
||||
PlayerLandblockId = roots.PlayerLandblockId,
|
||||
AnimatedEntityIds = world.AnimatedEntityIds,
|
||||
RenderCenterLbX = roots.RenderCenterLandblockX,
|
||||
RenderCenterLbY = roots.RenderCenterLandblockY,
|
||||
RenderRadius = _renderRange.NearRadius,
|
||||
LandblockEntries = _entities.LandblockEntries,
|
||||
RenderSky = renderSky,
|
||||
RenderWeather = roots.PlayerSeenOutside,
|
||||
DayFraction = _sky.DayFraction,
|
||||
ActiveDayGroup = _sky.ActiveDayGroup,
|
||||
SkyKeyframe = foundation.Sky,
|
||||
EnvironOverrideActive = foundation.EnvironOverrideActive,
|
||||
ViewerCellId = roots.ViewerCellId,
|
||||
PlayerCellId = roots.PlayerCellId,
|
||||
PlayerViewPosition = roots.PlayerViewPosition,
|
||||
CameraView = camera.Camera.View,
|
||||
CameraCellResolution = _diagnostics.CameraCellResolution,
|
||||
});
|
||||
|
||||
_particleVisibility.MarkVisibleCells(pviewResult.DrawableCells);
|
||||
_frames.ObserveDrawableCells(pviewResult.DrawableCells);
|
||||
_diagnostics.EmitPViewInput(
|
||||
pviewResult.PortalFrame,
|
||||
camera.ViewProjection,
|
||||
clipRoot,
|
||||
camera.Position,
|
||||
roots.PlayerViewPosition);
|
||||
|
||||
bool hasOutsideSlice = pviewResult.ClipAssembly.OutsideViewSlices.Length > 0;
|
||||
terrainDrawn = hasOutsideSlice;
|
||||
skyDrawn = renderSky && hasOutsideSlice;
|
||||
// This mirrors the established diagnostic meaning, including
|
||||
// outdoor roots whose PView executor does not issue an interior
|
||||
// depth clear.
|
||||
depthClear = hasOutsideSlice;
|
||||
if (pviewResult.Partition.ByCell.Count > 0 || hasOutsideSlice)
|
||||
sceneParticles = "pviewScoped";
|
||||
outdoorSceneryDrawn = pviewResult.Partition.OutdoorStatic.Count > 0
|
||||
&& hasOutsideSlice;
|
||||
liveDynamicDrawnCount = pviewResult.Partition.Dynamics.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
_passes.DrawFlatEntities(
|
||||
in camera,
|
||||
_entities.LandblockEntries,
|
||||
roots.PlayerLandblockId,
|
||||
world.AnimatedEntityIds);
|
||||
_frames.ClearDrawableCells();
|
||||
}
|
||||
|
||||
_passes.DisableClipDistances();
|
||||
sceneParticles = _passes.DrawPostWorldParticles(
|
||||
clipRoot,
|
||||
pviewResult?.ClipAssembly,
|
||||
in camera,
|
||||
_pview.OutdoorSceneParticleEntityIds,
|
||||
sceneParticles);
|
||||
|
||||
if (clipRoot is null && drawSkyThisFrame)
|
||||
{
|
||||
skyDrawn = true;
|
||||
_passes.DrawFlatWeather(
|
||||
in camera,
|
||||
in foundation,
|
||||
_sky.ActiveDayGroup,
|
||||
_sky.DayFraction);
|
||||
}
|
||||
|
||||
_diagnostics.EmitRenderSignature(
|
||||
clipRoot is null ? "OutdoorRoot" : "RetailPViewInside",
|
||||
clipRoot,
|
||||
roots.ViewerRoot,
|
||||
roots.PlayerRoot,
|
||||
roots.ViewerCellId,
|
||||
roots.PlayerCellId,
|
||||
roots.PlayerIndoorGate,
|
||||
roots.CameraInsideCell,
|
||||
renderSky,
|
||||
drawSkyThisFrame,
|
||||
terrainDrawn,
|
||||
terrainClipMode,
|
||||
skyDrawn,
|
||||
depthClear,
|
||||
outdoorSceneryDrawn,
|
||||
liveDynamicDrawnCount,
|
||||
sceneParticles,
|
||||
pviewResult,
|
||||
camera.Position,
|
||||
roots.PlayerViewPosition);
|
||||
|
||||
WorldSceneDiagnosticOutcome diagnostic = _diagnostics.DrawAndPublish(
|
||||
in camera,
|
||||
_entities.LandblockBounds);
|
||||
CompleteWorldFrame();
|
||||
worldFrameStarted = false;
|
||||
pviewFrameStarted = false;
|
||||
_selection?.CompleteFrame();
|
||||
selectionFrameStarted = false;
|
||||
return new WorldRenderFrameOutcome(
|
||||
diagnostic.VisibleLandblocks,
|
||||
diagnostic.TotalLandblocks,
|
||||
NormalWorldDrawn: true);
|
||||
|
||||
WorldRenderFrameOutcome CompleteSkippedWorld()
|
||||
{
|
||||
CompleteWorldFrame();
|
||||
worldFrameStarted = false;
|
||||
pviewFrameStarted = false;
|
||||
_selection?.CompleteFrame();
|
||||
selectionFrameStarted = false;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
catch (Exception renderFailure)
|
||||
{
|
||||
List<Exception>? abortFailures = AbortFrame(
|
||||
worldFrameStarted,
|
||||
pviewFrameStarted,
|
||||
selectionFrameStarted);
|
||||
if (abortFailures is { Count: > 0 })
|
||||
{
|
||||
abortFailures.Insert(0, renderFailure);
|
||||
throw new AggregateException(
|
||||
"World rendering failed and the incomplete frame could not be fully aborted.",
|
||||
abortFailures);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void CompleteWorldFrame()
|
||||
{
|
||||
_alpha.EndFrame();
|
||||
if (_passes.TerrainVisibleCellIds is { } visibleTerrainCells)
|
||||
_particleVisibility.MarkVisibleCells(visibleTerrainCells);
|
||||
_particleVisibility.CompleteFrame();
|
||||
}
|
||||
|
||||
private List<Exception>? AbortFrame(
|
||||
bool worldFrameStarted,
|
||||
bool pviewFrameStarted,
|
||||
bool selectionFrameStarted)
|
||||
{
|
||||
List<Exception>? failures = null;
|
||||
if (worldFrameStarted)
|
||||
{
|
||||
if (pviewFrameStarted)
|
||||
TryAbort(_pview.AbortFrame);
|
||||
TryAbort(_passes.AbortFrame);
|
||||
TryAbort(_particleVisibility.AbortFrame);
|
||||
TryAbort(_alpha.AbortFrame);
|
||||
}
|
||||
if (selectionFrameStarted && _selection is not null)
|
||||
TryAbort(_selection.AbortFrame);
|
||||
return failures;
|
||||
|
||||
void TryAbort(Action abort)
|
||||
{
|
||||
try
|
||||
{
|
||||
abort();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
(failures ??= []).Add(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue