fix(render): S2 chunk 6 review round — one particle turn per cell per stamp, cheap empty cells, dead owner-set stub deleted
Retail-lens review of chunk 6 (no blocking finding). Fixed: - a cell that gets two object-list turns in one frame (a chamber reached through two portals) submitted its emitters twice; retail's particle parts sit in the same shadow_part_list as every other part and CPhysicsPart::Draw's frame stamp suppresses the second draw, so the walk now dedupes the particle turn with the same frame-scoped set that dedupes the cell shell; - every visited land cell paid the full per-cell draw setup even with no emitter; DrawForCell now returns after the cell lookup, retail's own cost (DrawPartCell 0x005a07a0 `num_shadow_parts > 0`); - CopyRenderableEmittersInCell maintains LastRenderScopeEmitterVisitCount; - the OutdoorSceneParticleEntityIds / outdoorOwnerIds stub chain (permanently empty, never read) is deleted through IWorldSceneRenderer, WorldScenePViewRenderer, IWorldScenePasses and the composition root; - AD-117 item 4 names the two behavioral residuals (owner-cell substitution; no per-emission AddPartToShadowCells); - ParticleHookSinkTests pins that an emitter's draw cell is its owner's pose cell and survives the projection-visibility switch across the per-frame view pass. Gates: Core 4,988/4,988 (Vfx 108/108), App hermetic 6,760/6,760, Runtime 1,884/1,884. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
7969c2e6ad
commit
6d5afcccde
11 changed files with 891 additions and 800 deletions
|
|
@ -508,7 +508,6 @@ internal sealed class FrameRootCompositionPhase
|
|||
"The retail frame walk requires the landscape registry."),
|
||||
d.CellVisibility,
|
||||
d.PhysicsEngine.ShadowObjects),
|
||||
retailPViewPassExecutor,
|
||||
retailPViewPassExecutor),
|
||||
retailPViewCells,
|
||||
worldScenePasses,
|
||||
|
|
|
|||
|
|
@ -284,6 +284,12 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
return;
|
||||
|
||||
_particles.CopyRenderableEmittersInCell(renderPass, cellId, _scopedEmitterScratch);
|
||||
// Retail's per-cell particle cost for an emitter-less cell is one
|
||||
// count check (RenderDeviceD3D::DrawPartCell 0x005a07a0,
|
||||
// `num_shadow_parts > 0`); every visited land cell reaches here, so
|
||||
// pay nothing more than that here either (chunk 6 review F2).
|
||||
if (_scopedEmitterScratch.Count == 0)
|
||||
return;
|
||||
Matrix4x4.Invert(camera.View, out Matrix4x4 invView);
|
||||
Vector3 cameraRight = Vector3.Normalize(new Vector3(invView.M11, invView.M12, invView.M13));
|
||||
Vector3 cameraUp = Vector3.Normalize(new Vector3(invView.M21, invView.M22, invView.M23));
|
||||
|
|
|
|||
|
|
@ -50,18 +50,12 @@ internal sealed class RetailPViewCellSource : IRetailPViewCellSource
|
|||
/// cell shells/objects, then surviving dynamics. Landscape sky/terrain/weather
|
||||
/// placement follows <c>LScape::draw @ 0x00506330</c>.
|
||||
/// </summary>
|
||||
internal interface IOutdoorSceneParticleOwnerSource
|
||||
{
|
||||
IReadOnlySet<uint> OutdoorSceneParticleEntityIds { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="IWorldPassSurface"/>.
|
||||
/// </summary>
|
||||
internal sealed partial class RetailPViewPassExecutor :
|
||||
IOutdoorSceneParticleOwnerSource
|
||||
internal sealed partial class RetailPViewPassExecutor
|
||||
{
|
||||
private readonly IWorldPassSurface _surface;
|
||||
private readonly IRenderFrameGlState _frameGlState;
|
||||
|
|
@ -77,18 +71,7 @@ internal sealed partial class RetailPViewPassExecutor :
|
|||
private readonly WorldRenderDiagnostics _diagnostics;
|
||||
private readonly TerrainDrawDiagnosticsController _terrainDiagnostics;
|
||||
private readonly HashSet<uint> _noSceneParticleEntityIds = [];
|
||||
// Campaign OVERHAUL S2 chunk 6: the walk now draws every owner's scene
|
||||
// particles at its own cell's turn (DrawLandscapeStaticParticles/
|
||||
// DrawCellParticles below), so there is no longer a per-frame outdoor
|
||||
// owner set to hand the flat-world safety path's post-world particle
|
||||
// pass — that pass's outdoorOwnerIds parameter is unused on every live
|
||||
// code path (WorldScenePassExecutor.DrawPostWorldParticles) and this
|
||||
// property now always reports empty.
|
||||
private static readonly IReadOnlySet<uint> NoOutdoorSceneParticleEntityIds = new HashSet<uint>();
|
||||
public IReadOnlySet<uint> OutdoorSceneParticleEntityIds =>
|
||||
NoOutdoorSceneParticleEntityIds;
|
||||
|
||||
public RetailPViewPassExecutor(
|
||||
public RetailPViewPassExecutor(
|
||||
IWorldPassSurface surface,
|
||||
IRenderFrameGlState frameGlState,
|
||||
ClipFrame clipFrame,
|
||||
|
|
|
|||
|
|
@ -519,6 +519,8 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
private WalkPlane _lookInCyPlane;
|
||||
|
||||
private readonly HashSet<uint> _cellShellsDrawnThisFrame = new();
|
||||
// Chunk 6 review F1: one particle turn per cell per render stamp (see EmitCellContentsTurn).
|
||||
private readonly HashSet<uint> _cellParticleTurnsDrawnThisFrame = new();
|
||||
|
||||
IReadOnlyList<uint> IWalkLookInViewSource.LookInCellTurns => LookInCellTurns;
|
||||
|
||||
|
|
@ -653,6 +655,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
_floodViewRouteScratch.Clear();
|
||||
_dispatcher.EndWalkPartFrame();
|
||||
_cellShellsDrawnThisFrame.Clear();
|
||||
_cellParticleTurnsDrawnThisFrame.Clear();
|
||||
_lookInCyPlane = default;
|
||||
LookInCells.Clear();
|
||||
VisitedBuildings.Clear();
|
||||
|
|
@ -787,6 +790,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
_visibleClipSlotScratch.Clear();
|
||||
_lookInCyPlane = ctx.CyPlane;
|
||||
_cellShellsDrawnThisFrame.Clear();
|
||||
_cellParticleTurnsDrawnThisFrame.Clear();
|
||||
LookInCells.Clear();
|
||||
VisitedBuildings.Clear();
|
||||
VisitedLandscapeCellIds.Clear();
|
||||
|
|
@ -1249,6 +1253,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
{
|
||||
_dispatcher.AdvanceWalkPartPassStamp();
|
||||
_cellShellsDrawnThisFrame.Clear();
|
||||
_cellParticleTurnsDrawnThisFrame.Clear();
|
||||
}
|
||||
|
||||
// PView::DrawCells @0x005a4840: the gated full depth clear
|
||||
|
|
@ -1428,11 +1433,17 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
alphaSubmissions: _alphaSubmissions);
|
||||
MarkIfGrown();
|
||||
MarkAlphaIfGrown();
|
||||
// Fires unconditionally: retail's add_particle_shadow_to_cell draws
|
||||
// an emitter at its own cell's turn independent of whether that cell
|
||||
// has any visible static/dynamic owner record (Campaign OVERHAUL S2
|
||||
// chunk 6 — a suspended/hidden owner's emitter must still show).
|
||||
_events.Add(WalkFrameEvent.CellParticles(cellId));
|
||||
// Fires for every cell turn regardless of whether that cell has any
|
||||
// visible static/dynamic owner record (Campaign OVERHAUL S2 chunk 6 —
|
||||
// a suspended/hidden owner's emitter must still show), but ONCE per
|
||||
// render stamp: retail's particle parts sit in the same shadow_part_list
|
||||
// as every other part, so CPhysicsPart::Draw's frame stamp suppresses a
|
||||
// second submission when one cell gets two object-list turns in one
|
||||
// frame (a chamber reached through two portals). The same frame-scoped
|
||||
// set that dedupes the cell SHELL dedupes the particle turn (chunk 6
|
||||
// review F1).
|
||||
if (_cellParticleTurnsDrawnThisFrame.Add(cellId))
|
||||
_events.Add(WalkFrameEvent.CellParticles(cellId));
|
||||
}
|
||||
|
||||
private void CaptureCellViews(uint cellId)
|
||||
|
|
|
|||
|
|
@ -1,242 +1,240 @@
|
|||
using AcDream.App.Rendering.Sky;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.Core.Rendering;
|
||||
using AcDream.Core.Vfx;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal interface IWorldScenePassExecutor
|
||||
{
|
||||
HashSet<uint>? TerrainVisibleCellIds { get; }
|
||||
|
||||
void BeginFrame();
|
||||
|
||||
void PrepareFlatWorldClip();
|
||||
|
||||
void DrawFlatSky(
|
||||
in WorldCameraFrame camera,
|
||||
in RenderFrameFoundation foundation,
|
||||
DayGroupData? activeDayGroup,
|
||||
float dayFraction);
|
||||
|
||||
void DrawFlatTerrain(in WorldCameraFrame camera, uint? playerLandblockId);
|
||||
|
||||
void DrawFlatEntities(
|
||||
in WorldCameraFrame camera,
|
||||
IEnumerable<(uint LandblockId, System.Numerics.Vector3 AabbMin,
|
||||
System.Numerics.Vector3 AabbMax,
|
||||
IReadOnlyList<WorldEntity> Entities,
|
||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> entries,
|
||||
uint? playerLandblockId,
|
||||
HashSet<uint> animatedEntityIds);
|
||||
|
||||
string DrawPostWorldParticles(
|
||||
LoadedCell? clipRoot,
|
||||
ClipFrameAssembly? clipAssembly,
|
||||
in WorldCameraFrame camera,
|
||||
IReadOnlySet<uint> outdoorOwnerIds,
|
||||
string currentSignature);
|
||||
|
||||
void DrawFlatWeather(
|
||||
in WorldCameraFrame camera,
|
||||
in RenderFrameFoundation foundation,
|
||||
DayGroupData? activeDayGroup,
|
||||
float dayFraction);
|
||||
|
||||
void DisableClipDistances();
|
||||
|
||||
void AbortFrame();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Concrete leaf for the flat-world safety path and the post-world particle
|
||||
/// and weather passes. Retail PView frames remain owned by
|
||||
/// <see cref="RetailPViewRenderer"/> and <see cref="RetailPViewPassExecutor"/>.
|
||||
///
|
||||
/// <para>Campaign V slice V6j: backend-neutral. Everything it does is either
|
||||
/// delegation to a renderer or one of the four concerns
|
||||
/// <see cref="IWorldPassSurface"/> owns, so one implementation serves both
|
||||
/// backends and the retail ordering is written once.</para>
|
||||
/// </summary>
|
||||
internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
|
||||
{
|
||||
private readonly IWorldPassSurface _surface;
|
||||
private readonly IRenderFrameGlState _frameGlState;
|
||||
private readonly ClipFrame _clipFrame;
|
||||
private readonly WbDrawDispatcher _entities;
|
||||
private readonly EnvCellRenderer _environmentCells;
|
||||
private readonly TerrainModernRenderer? _terrain;
|
||||
private readonly TerrainDrawDiagnosticsController _terrainDiagnostics;
|
||||
private readonly SkyRenderer? _sky;
|
||||
using AcDream.Core.Rendering;
|
||||
using AcDream.Core.Vfx;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal interface IWorldScenePassExecutor
|
||||
{
|
||||
HashSet<uint>? TerrainVisibleCellIds { get; }
|
||||
|
||||
void BeginFrame();
|
||||
|
||||
void PrepareFlatWorldClip();
|
||||
|
||||
void DrawFlatSky(
|
||||
in WorldCameraFrame camera,
|
||||
in RenderFrameFoundation foundation,
|
||||
DayGroupData? activeDayGroup,
|
||||
float dayFraction);
|
||||
|
||||
void DrawFlatTerrain(in WorldCameraFrame camera, uint? playerLandblockId);
|
||||
|
||||
void DrawFlatEntities(
|
||||
in WorldCameraFrame camera,
|
||||
IEnumerable<(uint LandblockId, System.Numerics.Vector3 AabbMin,
|
||||
System.Numerics.Vector3 AabbMax,
|
||||
IReadOnlyList<WorldEntity> Entities,
|
||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> entries,
|
||||
uint? playerLandblockId,
|
||||
HashSet<uint> animatedEntityIds);
|
||||
|
||||
string DrawPostWorldParticles(
|
||||
LoadedCell? clipRoot,
|
||||
ClipFrameAssembly? clipAssembly,
|
||||
in WorldCameraFrame camera,
|
||||
string currentSignature);
|
||||
|
||||
void DrawFlatWeather(
|
||||
in WorldCameraFrame camera,
|
||||
in RenderFrameFoundation foundation,
|
||||
DayGroupData? activeDayGroup,
|
||||
float dayFraction);
|
||||
|
||||
void DisableClipDistances();
|
||||
|
||||
void AbortFrame();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Concrete leaf for the flat-world safety path and the post-world particle
|
||||
/// and weather passes. Retail PView frames remain owned by
|
||||
/// <see cref="RetailPViewRenderer"/> and <see cref="RetailPViewPassExecutor"/>.
|
||||
///
|
||||
/// <para>Campaign V slice V6j: backend-neutral. Everything it does is either
|
||||
/// delegation to a renderer or one of the four concerns
|
||||
/// <see cref="IWorldPassSurface"/> owns, so one implementation serves both
|
||||
/// backends and the retail ordering is written once.</para>
|
||||
/// </summary>
|
||||
internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
|
||||
{
|
||||
private readonly IWorldPassSurface _surface;
|
||||
private readonly IRenderFrameGlState _frameGlState;
|
||||
private readonly ClipFrame _clipFrame;
|
||||
private readonly WbDrawDispatcher _entities;
|
||||
private readonly EnvCellRenderer _environmentCells;
|
||||
private readonly TerrainModernRenderer? _terrain;
|
||||
private readonly TerrainDrawDiagnosticsController _terrainDiagnostics;
|
||||
private readonly SkyRenderer? _sky;
|
||||
private readonly ParticleSystem? _particles;
|
||||
private readonly ParticleRenderer? _particleRenderer;
|
||||
private readonly HashSet<uint> _visibleParticleOwners = [];
|
||||
private readonly HashSet<uint> _noExcludedParticleOwners = [];
|
||||
|
||||
public WorldScenePassExecutor(
|
||||
IWorldPassSurface surface,
|
||||
IRenderFrameGlState frameGlState,
|
||||
ClipFrame clipFrame,
|
||||
WbDrawDispatcher entities,
|
||||
EnvCellRenderer environmentCells,
|
||||
TerrainModernRenderer? terrain,
|
||||
private readonly HashSet<uint> _visibleParticleOwners = [];
|
||||
private readonly HashSet<uint> _noExcludedParticleOwners = [];
|
||||
|
||||
public WorldScenePassExecutor(
|
||||
IWorldPassSurface surface,
|
||||
IRenderFrameGlState frameGlState,
|
||||
ClipFrame clipFrame,
|
||||
WbDrawDispatcher entities,
|
||||
EnvCellRenderer environmentCells,
|
||||
TerrainModernRenderer? terrain,
|
||||
TerrainDrawDiagnosticsController terrainDiagnostics,
|
||||
SkyRenderer? sky,
|
||||
ParticleSystem? particles,
|
||||
ParticleRenderer? particleRenderer)
|
||||
{
|
||||
_surface = surface ?? throw new ArgumentNullException(nameof(surface));
|
||||
_frameGlState = frameGlState
|
||||
?? throw new ArgumentNullException(nameof(frameGlState));
|
||||
_clipFrame = clipFrame ?? throw new ArgumentNullException(nameof(clipFrame));
|
||||
_entities = entities ?? throw new ArgumentNullException(nameof(entities));
|
||||
_environmentCells = environmentCells
|
||||
?? throw new ArgumentNullException(nameof(environmentCells));
|
||||
_terrain = terrain;
|
||||
_terrainDiagnostics = terrainDiagnostics
|
||||
?? throw new ArgumentNullException(nameof(terrainDiagnostics));
|
||||
_sky = sky;
|
||||
{
|
||||
_surface = surface ?? throw new ArgumentNullException(nameof(surface));
|
||||
_frameGlState = frameGlState
|
||||
?? throw new ArgumentNullException(nameof(frameGlState));
|
||||
_clipFrame = clipFrame ?? throw new ArgumentNullException(nameof(clipFrame));
|
||||
_entities = entities ?? throw new ArgumentNullException(nameof(entities));
|
||||
_environmentCells = environmentCells
|
||||
?? throw new ArgumentNullException(nameof(environmentCells));
|
||||
_terrain = terrain;
|
||||
_terrainDiagnostics = terrainDiagnostics
|
||||
?? throw new ArgumentNullException(nameof(terrainDiagnostics));
|
||||
_sky = sky;
|
||||
_particles = particles;
|
||||
_particleRenderer = particleRenderer;
|
||||
}
|
||||
|
||||
public HashSet<uint>? TerrainVisibleCellIds => _terrain?.VisibleCellIds;
|
||||
|
||||
public void BeginFrame()
|
||||
{
|
||||
_visibleParticleOwners.Clear();
|
||||
_clipFrame.Reset();
|
||||
_entities.ClearClipRouting();
|
||||
_environmentCells.SetClipRouting(null);
|
||||
}
|
||||
|
||||
public void PrepareFlatWorldClip() => _surface.PrepareClipFrame(1);
|
||||
|
||||
public void DrawFlatSky(
|
||||
in WorldCameraFrame camera,
|
||||
in RenderFrameFoundation foundation,
|
||||
DayGroupData? activeDayGroup,
|
||||
float dayFraction)
|
||||
{
|
||||
_surface.BindTerrainClip();
|
||||
_surface.EnableClipDistances();
|
||||
Exception? drawFailure = null;
|
||||
try
|
||||
{
|
||||
_sky?.RenderSky(
|
||||
camera.Camera,
|
||||
camera.Position,
|
||||
dayFraction,
|
||||
activeDayGroup,
|
||||
foundation.Sky,
|
||||
foundation.EnvironOverrideActive);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
drawFailure = error;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
DisableClipDistances();
|
||||
}
|
||||
catch (Exception closeFailure) when (drawFailure is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"Sky drawing failed and its clip-distance bracket could not be closed.",
|
||||
drawFailure,
|
||||
closeFailure);
|
||||
}
|
||||
}
|
||||
|
||||
if (_particles is not null && _particleRenderer is not null)
|
||||
{
|
||||
_particleRenderer.Draw(
|
||||
camera.Camera,
|
||||
camera.Position,
|
||||
ParticleRenderPass.SkyPreScene);
|
||||
}
|
||||
}
|
||||
|
||||
public void DrawFlatTerrain(
|
||||
in WorldCameraFrame camera,
|
||||
uint? playerLandblockId)
|
||||
{
|
||||
_surface.EnableClipDistances();
|
||||
_terrainDiagnostics.Begin();
|
||||
_terrain?.Draw(
|
||||
camera.Camera,
|
||||
camera.Frustum,
|
||||
neverCullLandblockId: playerLandblockId);
|
||||
_terrainDiagnostics.Complete();
|
||||
}
|
||||
|
||||
public void DrawFlatEntities(
|
||||
in WorldCameraFrame camera,
|
||||
IEnumerable<(uint LandblockId, System.Numerics.Vector3 AabbMin,
|
||||
System.Numerics.Vector3 AabbMax,
|
||||
IReadOnlyList<WorldEntity> Entities,
|
||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> entries,
|
||||
uint? playerLandblockId,
|
||||
HashSet<uint> animatedEntityIds) =>
|
||||
_entities.Draw(
|
||||
camera.Camera,
|
||||
entries,
|
||||
camera.Frustum,
|
||||
neverCullLandblockId: playerLandblockId,
|
||||
visibleCellIds: null,
|
||||
animatedEntityIds: animatedEntityIds);
|
||||
|
||||
public string DrawPostWorldParticles(
|
||||
LoadedCell? clipRoot,
|
||||
ClipFrameAssembly? clipAssembly,
|
||||
in WorldCameraFrame camera,
|
||||
IReadOnlySet<uint> outdoorOwnerIds,
|
||||
string currentSignature)
|
||||
{
|
||||
if (_particles is null || _particleRenderer is null)
|
||||
return currentSignature;
|
||||
|
||||
if (clipRoot is null)
|
||||
{
|
||||
if (clipAssembly is not null)
|
||||
{
|
||||
_particleRenderer.DrawForOwners(
|
||||
camera.Camera,
|
||||
camera.Position,
|
||||
ParticleRenderPass.Scene,
|
||||
_visibleParticleOwners,
|
||||
includeUnattached: true,
|
||||
excludedAttachedOwnerIds: _noExcludedParticleOwners);
|
||||
return AppendSignature(currentSignature, "filtered");
|
||||
}
|
||||
|
||||
_particleRenderer.Draw(
|
||||
camera.Camera,
|
||||
camera.Position,
|
||||
ParticleRenderPass.Scene);
|
||||
return AppendSignature(currentSignature, "global");
|
||||
}
|
||||
|
||||
// Every PView root, including the outdoor sentinel, now submits scene
|
||||
// particles inside LScape::draw. Replaying them here is both a duplicate
|
||||
// and too late: it occurs after nested building cells, allowing exterior
|
||||
// waterfall/foliage alpha to repaint an indoor/outdoor transition.
|
||||
return currentSignature;
|
||||
}
|
||||
|
||||
public void DrawFlatWeather(
|
||||
in WorldCameraFrame camera,
|
||||
in RenderFrameFoundation foundation,
|
||||
DayGroupData? activeDayGroup,
|
||||
float dayFraction)
|
||||
{
|
||||
_surface.BindTerrainClip();
|
||||
_surface.EnableClipDistances();
|
||||
Exception? drawFailure = null;
|
||||
}
|
||||
|
||||
public HashSet<uint>? TerrainVisibleCellIds => _terrain?.VisibleCellIds;
|
||||
|
||||
public void BeginFrame()
|
||||
{
|
||||
_visibleParticleOwners.Clear();
|
||||
_clipFrame.Reset();
|
||||
_entities.ClearClipRouting();
|
||||
_environmentCells.SetClipRouting(null);
|
||||
}
|
||||
|
||||
public void PrepareFlatWorldClip() => _surface.PrepareClipFrame(1);
|
||||
|
||||
public void DrawFlatSky(
|
||||
in WorldCameraFrame camera,
|
||||
in RenderFrameFoundation foundation,
|
||||
DayGroupData? activeDayGroup,
|
||||
float dayFraction)
|
||||
{
|
||||
_surface.BindTerrainClip();
|
||||
_surface.EnableClipDistances();
|
||||
Exception? drawFailure = null;
|
||||
try
|
||||
{
|
||||
_sky?.RenderSky(
|
||||
camera.Camera,
|
||||
camera.Position,
|
||||
dayFraction,
|
||||
activeDayGroup,
|
||||
foundation.Sky,
|
||||
foundation.EnvironOverrideActive);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
drawFailure = error;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
DisableClipDistances();
|
||||
}
|
||||
catch (Exception closeFailure) when (drawFailure is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"Sky drawing failed and its clip-distance bracket could not be closed.",
|
||||
drawFailure,
|
||||
closeFailure);
|
||||
}
|
||||
}
|
||||
|
||||
if (_particles is not null && _particleRenderer is not null)
|
||||
{
|
||||
_particleRenderer.Draw(
|
||||
camera.Camera,
|
||||
camera.Position,
|
||||
ParticleRenderPass.SkyPreScene);
|
||||
}
|
||||
}
|
||||
|
||||
public void DrawFlatTerrain(
|
||||
in WorldCameraFrame camera,
|
||||
uint? playerLandblockId)
|
||||
{
|
||||
_surface.EnableClipDistances();
|
||||
_terrainDiagnostics.Begin();
|
||||
_terrain?.Draw(
|
||||
camera.Camera,
|
||||
camera.Frustum,
|
||||
neverCullLandblockId: playerLandblockId);
|
||||
_terrainDiagnostics.Complete();
|
||||
}
|
||||
|
||||
public void DrawFlatEntities(
|
||||
in WorldCameraFrame camera,
|
||||
IEnumerable<(uint LandblockId, System.Numerics.Vector3 AabbMin,
|
||||
System.Numerics.Vector3 AabbMax,
|
||||
IReadOnlyList<WorldEntity> Entities,
|
||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> entries,
|
||||
uint? playerLandblockId,
|
||||
HashSet<uint> animatedEntityIds) =>
|
||||
_entities.Draw(
|
||||
camera.Camera,
|
||||
entries,
|
||||
camera.Frustum,
|
||||
neverCullLandblockId: playerLandblockId,
|
||||
visibleCellIds: null,
|
||||
animatedEntityIds: animatedEntityIds);
|
||||
|
||||
public string DrawPostWorldParticles(
|
||||
LoadedCell? clipRoot,
|
||||
ClipFrameAssembly? clipAssembly,
|
||||
in WorldCameraFrame camera,
|
||||
string currentSignature)
|
||||
{
|
||||
if (_particles is null || _particleRenderer is null)
|
||||
return currentSignature;
|
||||
|
||||
if (clipRoot is null)
|
||||
{
|
||||
if (clipAssembly is not null)
|
||||
{
|
||||
_particleRenderer.DrawForOwners(
|
||||
camera.Camera,
|
||||
camera.Position,
|
||||
ParticleRenderPass.Scene,
|
||||
_visibleParticleOwners,
|
||||
includeUnattached: true,
|
||||
excludedAttachedOwnerIds: _noExcludedParticleOwners);
|
||||
return AppendSignature(currentSignature, "filtered");
|
||||
}
|
||||
|
||||
_particleRenderer.Draw(
|
||||
camera.Camera,
|
||||
camera.Position,
|
||||
ParticleRenderPass.Scene);
|
||||
return AppendSignature(currentSignature, "global");
|
||||
}
|
||||
|
||||
// Every PView root, including the outdoor sentinel, now submits scene
|
||||
// particles inside LScape::draw. Replaying them here is both a duplicate
|
||||
// and too late: it occurs after nested building cells, allowing exterior
|
||||
// waterfall/foliage alpha to repaint an indoor/outdoor transition.
|
||||
return currentSignature;
|
||||
}
|
||||
|
||||
public void DrawFlatWeather(
|
||||
in WorldCameraFrame camera,
|
||||
in RenderFrameFoundation foundation,
|
||||
DayGroupData? activeDayGroup,
|
||||
float dayFraction)
|
||||
{
|
||||
_surface.BindTerrainClip();
|
||||
_surface.EnableClipDistances();
|
||||
Exception? drawFailure = null;
|
||||
try
|
||||
{
|
||||
_sky?.RenderWeather(
|
||||
|
|
@ -246,63 +244,63 @@ internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
|
|||
activeDayGroup,
|
||||
foundation.Sky,
|
||||
foundation.EnvironOverrideActive);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
drawFailure = error;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
DisableClipDistances();
|
||||
}
|
||||
catch (Exception closeFailure) when (drawFailure is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"Weather drawing failed and its clip-distance bracket could not be closed.",
|
||||
drawFailure,
|
||||
closeFailure);
|
||||
}
|
||||
}
|
||||
|
||||
if (_particles is not null && _particleRenderer is not null)
|
||||
{
|
||||
_particleRenderer.Draw(
|
||||
camera.Camera,
|
||||
camera.Position,
|
||||
ParticleRenderPass.SkyPostScene);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
drawFailure = error;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
DisableClipDistances();
|
||||
}
|
||||
catch (Exception closeFailure) when (drawFailure is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"Weather drawing failed and its clip-distance bracket could not be closed.",
|
||||
drawFailure,
|
||||
closeFailure);
|
||||
}
|
||||
}
|
||||
|
||||
if (_particles is not null && _particleRenderer is not null)
|
||||
{
|
||||
_particleRenderer.Draw(
|
||||
camera.Camera,
|
||||
camera.Position,
|
||||
ParticleRenderPass.SkyPostScene);
|
||||
}
|
||||
}
|
||||
|
||||
public void DisableClipDistances() => _surface.DisableClipDistances();
|
||||
|
||||
public void AbortFrame()
|
||||
{
|
||||
List<Exception>? failures = null;
|
||||
TryAbort(_frameGlState.RestoreFrameDefaults);
|
||||
TryAbort(_clipFrame.Reset);
|
||||
TryAbort(_entities.ClearClipRouting);
|
||||
TryAbort(_entities.AbortCurrentRenderSceneObserverFrame);
|
||||
TryAbort(() => _environmentCells.SetClipRouting(null));
|
||||
_visibleParticleOwners.Clear();
|
||||
if (failures is { Count: > 0 })
|
||||
throw new AggregateException("World scene pass abort failed.", failures);
|
||||
|
||||
void TryAbort(Action operation)
|
||||
{
|
||||
try
|
||||
{
|
||||
operation();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
(failures ??= []).Add(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string AppendSignature(string current, string value) =>
|
||||
current == "none" ? value : current + "+" + value;
|
||||
}
|
||||
|
||||
public void AbortFrame()
|
||||
{
|
||||
List<Exception>? failures = null;
|
||||
TryAbort(_frameGlState.RestoreFrameDefaults);
|
||||
TryAbort(_clipFrame.Reset);
|
||||
TryAbort(_entities.ClearClipRouting);
|
||||
TryAbort(_entities.AbortCurrentRenderSceneObserverFrame);
|
||||
TryAbort(() => _environmentCells.SetClipRouting(null));
|
||||
_visibleParticleOwners.Clear();
|
||||
if (failures is { Count: > 0 })
|
||||
throw new AggregateException("World scene pass abort failed.", failures);
|
||||
|
||||
void TryAbort(Action operation)
|
||||
{
|
||||
try
|
||||
{
|
||||
operation();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
(failures ??= []).Add(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string AppendSignature(string current, string value) =>
|
||||
current == "none" ? value : current + "+" + value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,477 +1,470 @@
|
|||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Selection;
|
||||
using AcDream.App.Rendering.Packs;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.Core.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal interface IWorldScenePViewRenderer
|
||||
{
|
||||
IReadOnlySet<uint> OutdoorSceneParticleEntityIds { get; }
|
||||
|
||||
RetailPViewFrameResult DrawInside(RetailPViewFrameInput input);
|
||||
|
||||
void AbortFrame();
|
||||
}
|
||||
|
||||
internal readonly record struct PreparedWorldSceneFrame(
|
||||
bool ShouldRender,
|
||||
RenderFrameFoundation Foundation,
|
||||
WorldRenderFrame World,
|
||||
int ActiveDayGroup);
|
||||
|
||||
/// <summary>
|
||||
/// Pack-on-only two-stage seam. Preparation resolves the canonical camera and
|
||||
/// borrowed world roots once; the shadow prepass may consume those exact facts
|
||||
/// before the normal world pass executes them without a second PView/build.
|
||||
/// </summary>
|
||||
internal interface IPreparedWorldSceneFramePhase : IWorldSceneFramePhase
|
||||
{
|
||||
PreparedWorldSceneFrame PrepareEnhanced(RenderFrameInput input);
|
||||
|
||||
WorldRenderFrameOutcome RenderPreparedEnhanced(
|
||||
RenderFrameInput input,
|
||||
in PreparedWorldSceneFrame prepared);
|
||||
|
||||
void CancelPreparedEnhanced(in PreparedWorldSceneFrame prepared);
|
||||
}
|
||||
|
||||
internal sealed class WorldScenePViewRenderer : IWorldScenePViewRenderer
|
||||
{
|
||||
private readonly RetailPViewRenderer _renderer;
|
||||
private readonly RetailPViewPassExecutor _passes;
|
||||
private readonly IOutdoorSceneParticleOwnerSource _particles;
|
||||
|
||||
public WorldScenePViewRenderer(
|
||||
RetailPViewRenderer renderer,
|
||||
RetailPViewPassExecutor 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 : IPreparedWorldSceneFramePhase
|
||||
{
|
||||
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;
|
||||
private readonly IWorldGenerationAvailability _availability;
|
||||
private readonly IAtmosphericWorldFrameSink? _atmosphere;
|
||||
private readonly RetailPViewFrameInput _pviewFrameInput = new();
|
||||
private WorldRenderFrame _preparedEnhancedWorld;
|
||||
private bool _hasPreparedEnhancedWorld;
|
||||
|
||||
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,
|
||||
IWorldGenerationAvailability? availability = null,
|
||||
IAtmosphericWorldFrameSink? atmosphere = null)
|
||||
{
|
||||
_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));
|
||||
_availability = availability ?? AlwaysAvailableWorldGeneration.Instance;
|
||||
_atmosphere = atmosphere;
|
||||
}
|
||||
|
||||
public WorldRenderFrameOutcome Render(RenderFrameInput input)
|
||||
{
|
||||
_ = input;
|
||||
bool selectionFrameStarted = _selection is not null;
|
||||
bool worldFrameStarted = false;
|
||||
bool pviewFrameStarted = false;
|
||||
try
|
||||
{
|
||||
// Enhanced rendering builds the canonical camera before this world
|
||||
// transaction so the shadow prepass can consume it. Carry that
|
||||
// exact frustum across BeginFrame: clearing it here would make every
|
||||
// subsequently drawn part fail RetailSelectionScene's visibility
|
||||
// gate, leaving radar selection alive but disabling world picking.
|
||||
FrustumPlanes? preparedSelectionFrustum = _hasPreparedEnhancedWorld
|
||||
? _preparedEnhancedWorld.Camera.Frustum
|
||||
: null;
|
||||
_selection?.BeginFrame(preparedSelectionFrustum);
|
||||
if (!_availability.IsWorldAvailable)
|
||||
{
|
||||
_selection?.CompleteFrame();
|
||||
selectionFrameStarted = false;
|
||||
return default;
|
||||
}
|
||||
|
||||
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;
|
||||
if (_hasPreparedEnhancedWorld)
|
||||
{
|
||||
world = _preparedEnhancedWorld;
|
||||
_hasPreparedEnhancedWorld = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
world = _frames.Build(
|
||||
in foundation,
|
||||
_login.IsWaitingForLogin,
|
||||
_sky.ActiveDayGroup);
|
||||
}
|
||||
_atmosphere?.Publish(
|
||||
in foundation,
|
||||
in world,
|
||||
_sky.ActiveDayGroupIndex);
|
||||
_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);
|
||||
}
|
||||
|
||||
// The former sky-only "waiting for login" skip lived here.
|
||||
// It is unreachable now: LocalPlayerTeleportRenderStateSource
|
||||
// folds IsWaitingForLogin into PortalViewportVisible (retail's
|
||||
// pre-player gameplay screen draws NO world — black, not sky),
|
||||
// so the portal-visible return above already covers every
|
||||
// waiting frame. One gate computes the frame's visibility;
|
||||
// this phase only enforces it.
|
||||
_passes.DrawFlatTerrain(in camera, roots.PlayerLandblockId);
|
||||
terrainDrawn = true;
|
||||
if (_passes.TerrainVisibleCellIds is { } flatTerrainCells)
|
||||
_particleVisibility.MarkVisibleCells(flatTerrainCells);
|
||||
}
|
||||
else
|
||||
{
|
||||
terrainClipMode = TerrainClipMode.Skip;
|
||||
}
|
||||
|
||||
if (clipRoot is not null)
|
||||
{
|
||||
pviewFrameStarted = true;
|
||||
pviewResult = _pview.DrawInside(
|
||||
_pviewFrameInput.Reset(
|
||||
clipRoot,
|
||||
world.Buildings.NearbyBuildingCells,
|
||||
roots.ViewerEyePosition,
|
||||
camera.ViewProjection,
|
||||
_pviewCells,
|
||||
camera.Camera,
|
||||
camera.Position,
|
||||
camera.Frustum,
|
||||
roots.PlayerLandblockId,
|
||||
world.AnimatedEntityIds,
|
||||
roots.RenderCenterLandblockX,
|
||||
roots.RenderCenterLandblockY,
|
||||
_renderRange.NearRadius,
|
||||
_entities.LandblockEntries,
|
||||
renderSky,
|
||||
roots.PlayerSeenOutside,
|
||||
_sky.DayFraction,
|
||||
_sky.ActiveDayGroup,
|
||||
foundation.Sky,
|
||||
foundation.EnvironOverrideActive,
|
||||
roots.ViewerCellId,
|
||||
roots.PlayerCellId,
|
||||
roots.PlayerViewPosition,
|
||||
camera.Camera.View,
|
||||
_diagnostics.CameraCellResolution));
|
||||
|
||||
// One visibility answer: the walk's exact visited-cell set
|
||||
// feeds effects and the frame environment (point lights and
|
||||
// directional-shadow filtering).
|
||||
_particleVisibility.MarkVisibleCells(pviewResult.VisibleCells);
|
||||
_frames.ObserveDrawableCells(pviewResult.VisibleCells);
|
||||
_diagnostics.EmitPViewInput(
|
||||
pviewResult.VisibleCells,
|
||||
pviewResult.ClipAssembly.OutsideViewSlices.Length,
|
||||
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;
|
||||
RenderProjectionCounts sourceCounts =
|
||||
pviewResult.SourceCounts;
|
||||
if (sourceCounts.IndoorCellStatic > 0 || hasOutsideSlice)
|
||||
sceneParticles = "pviewScoped";
|
||||
outdoorSceneryDrawn =
|
||||
sourceCounts.OutdoorStatic > 0
|
||||
&& hasOutsideSlice;
|
||||
liveDynamicDrawnCount =
|
||||
sourceCounts.LiveDynamicRoot;
|
||||
}
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public PreparedWorldSceneFrame PrepareEnhanced(RenderFrameInput input)
|
||||
{
|
||||
_ = input;
|
||||
if (_hasPreparedEnhancedWorld)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The prior enhanced world preparation was not consumed.");
|
||||
}
|
||||
|
||||
RenderFrameFoundation foundation = _foundation.Foundation;
|
||||
if (!_availability.IsWorldAvailable || foundation.PortalViewportVisible)
|
||||
return new PreparedWorldSceneFrame(false, foundation, default, -1);
|
||||
|
||||
WorldRenderFrame world = _frames.Build(
|
||||
in foundation,
|
||||
_login.IsWaitingForLogin,
|
||||
_sky.ActiveDayGroup);
|
||||
WorldRootFrame roots = world.Roots;
|
||||
world = world with
|
||||
{
|
||||
ResidentStreamingWindow =
|
||||
_entities.CaptureResidentStreamingWindow(
|
||||
roots.RenderCenterLandblockX,
|
||||
roots.RenderCenterLandblockY),
|
||||
CelestialShadowSource =
|
||||
AuthoredCelestialShadowSourceResolver.Resolve(
|
||||
_sky.ActiveDayGroup,
|
||||
_sky.DayFraction,
|
||||
foundation.Sky),
|
||||
};
|
||||
_preparedEnhancedWorld = world;
|
||||
_hasPreparedEnhancedWorld = true;
|
||||
return new PreparedWorldSceneFrame(
|
||||
true,
|
||||
foundation,
|
||||
world,
|
||||
_sky.ActiveDayGroupIndex);
|
||||
}
|
||||
|
||||
public WorldRenderFrameOutcome RenderPreparedEnhanced(
|
||||
RenderFrameInput input,
|
||||
in PreparedWorldSceneFrame prepared)
|
||||
{
|
||||
if (prepared.ShouldRender != _hasPreparedEnhancedWorld)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The prepared enhanced-world token does not match the pending frame.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Render(input);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// A gate may change between prepare and execute. Never let that
|
||||
// exceptional edge leak borrowed builder scratch into another frame.
|
||||
_hasPreparedEnhancedWorld = false;
|
||||
_preparedEnhancedWorld = default;
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelPreparedEnhanced(in PreparedWorldSceneFrame prepared)
|
||||
{
|
||||
if (!prepared.ShouldRender)
|
||||
return;
|
||||
if (!_hasPreparedEnhancedWorld)
|
||||
return;
|
||||
_hasPreparedEnhancedWorld = false;
|
||||
_preparedEnhancedWorld = default;
|
||||
}
|
||||
|
||||
private void CompleteWorldFrame()
|
||||
{
|
||||
_alpha.EndFrame();
|
||||
_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Selection;
|
||||
using AcDream.App.Rendering.Packs;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.Core.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal interface IWorldScenePViewRenderer
|
||||
{
|
||||
|
||||
RetailPViewFrameResult DrawInside(RetailPViewFrameInput input);
|
||||
|
||||
void AbortFrame();
|
||||
}
|
||||
|
||||
internal readonly record struct PreparedWorldSceneFrame(
|
||||
bool ShouldRender,
|
||||
RenderFrameFoundation Foundation,
|
||||
WorldRenderFrame World,
|
||||
int ActiveDayGroup);
|
||||
|
||||
/// <summary>
|
||||
/// Pack-on-only two-stage seam. Preparation resolves the canonical camera and
|
||||
/// borrowed world roots once; the shadow prepass may consume those exact facts
|
||||
/// before the normal world pass executes them without a second PView/build.
|
||||
/// </summary>
|
||||
internal interface IPreparedWorldSceneFramePhase : IWorldSceneFramePhase
|
||||
{
|
||||
PreparedWorldSceneFrame PrepareEnhanced(RenderFrameInput input);
|
||||
|
||||
WorldRenderFrameOutcome RenderPreparedEnhanced(
|
||||
RenderFrameInput input,
|
||||
in PreparedWorldSceneFrame prepared);
|
||||
|
||||
void CancelPreparedEnhanced(in PreparedWorldSceneFrame prepared);
|
||||
}
|
||||
|
||||
internal sealed class WorldScenePViewRenderer : IWorldScenePViewRenderer
|
||||
{
|
||||
private readonly RetailPViewRenderer _renderer;
|
||||
private readonly RetailPViewPassExecutor _passes;
|
||||
|
||||
public WorldScenePViewRenderer(
|
||||
RetailPViewRenderer renderer,
|
||||
RetailPViewPassExecutor passes)
|
||||
{
|
||||
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
|
||||
_passes = passes ?? throw new ArgumentNullException(nameof(passes));
|
||||
}
|
||||
|
||||
|
||||
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 : IPreparedWorldSceneFramePhase
|
||||
{
|
||||
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;
|
||||
private readonly IWorldGenerationAvailability _availability;
|
||||
private readonly IAtmosphericWorldFrameSink? _atmosphere;
|
||||
private readonly RetailPViewFrameInput _pviewFrameInput = new();
|
||||
private WorldRenderFrame _preparedEnhancedWorld;
|
||||
private bool _hasPreparedEnhancedWorld;
|
||||
|
||||
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,
|
||||
IWorldGenerationAvailability? availability = null,
|
||||
IAtmosphericWorldFrameSink? atmosphere = null)
|
||||
{
|
||||
_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));
|
||||
_availability = availability ?? AlwaysAvailableWorldGeneration.Instance;
|
||||
_atmosphere = atmosphere;
|
||||
}
|
||||
|
||||
public WorldRenderFrameOutcome Render(RenderFrameInput input)
|
||||
{
|
||||
_ = input;
|
||||
bool selectionFrameStarted = _selection is not null;
|
||||
bool worldFrameStarted = false;
|
||||
bool pviewFrameStarted = false;
|
||||
try
|
||||
{
|
||||
// Enhanced rendering builds the canonical camera before this world
|
||||
// transaction so the shadow prepass can consume it. Carry that
|
||||
// exact frustum across BeginFrame: clearing it here would make every
|
||||
// subsequently drawn part fail RetailSelectionScene's visibility
|
||||
// gate, leaving radar selection alive but disabling world picking.
|
||||
FrustumPlanes? preparedSelectionFrustum = _hasPreparedEnhancedWorld
|
||||
? _preparedEnhancedWorld.Camera.Frustum
|
||||
: null;
|
||||
_selection?.BeginFrame(preparedSelectionFrustum);
|
||||
if (!_availability.IsWorldAvailable)
|
||||
{
|
||||
_selection?.CompleteFrame();
|
||||
selectionFrameStarted = false;
|
||||
return default;
|
||||
}
|
||||
|
||||
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;
|
||||
if (_hasPreparedEnhancedWorld)
|
||||
{
|
||||
world = _preparedEnhancedWorld;
|
||||
_hasPreparedEnhancedWorld = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
world = _frames.Build(
|
||||
in foundation,
|
||||
_login.IsWaitingForLogin,
|
||||
_sky.ActiveDayGroup);
|
||||
}
|
||||
_atmosphere?.Publish(
|
||||
in foundation,
|
||||
in world,
|
||||
_sky.ActiveDayGroupIndex);
|
||||
_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);
|
||||
}
|
||||
|
||||
// The former sky-only "waiting for login" skip lived here.
|
||||
// It is unreachable now: LocalPlayerTeleportRenderStateSource
|
||||
// folds IsWaitingForLogin into PortalViewportVisible (retail's
|
||||
// pre-player gameplay screen draws NO world — black, not sky),
|
||||
// so the portal-visible return above already covers every
|
||||
// waiting frame. One gate computes the frame's visibility;
|
||||
// this phase only enforces it.
|
||||
_passes.DrawFlatTerrain(in camera, roots.PlayerLandblockId);
|
||||
terrainDrawn = true;
|
||||
if (_passes.TerrainVisibleCellIds is { } flatTerrainCells)
|
||||
_particleVisibility.MarkVisibleCells(flatTerrainCells);
|
||||
}
|
||||
else
|
||||
{
|
||||
terrainClipMode = TerrainClipMode.Skip;
|
||||
}
|
||||
|
||||
if (clipRoot is not null)
|
||||
{
|
||||
pviewFrameStarted = true;
|
||||
pviewResult = _pview.DrawInside(
|
||||
_pviewFrameInput.Reset(
|
||||
clipRoot,
|
||||
world.Buildings.NearbyBuildingCells,
|
||||
roots.ViewerEyePosition,
|
||||
camera.ViewProjection,
|
||||
_pviewCells,
|
||||
camera.Camera,
|
||||
camera.Position,
|
||||
camera.Frustum,
|
||||
roots.PlayerLandblockId,
|
||||
world.AnimatedEntityIds,
|
||||
roots.RenderCenterLandblockX,
|
||||
roots.RenderCenterLandblockY,
|
||||
_renderRange.NearRadius,
|
||||
_entities.LandblockEntries,
|
||||
renderSky,
|
||||
roots.PlayerSeenOutside,
|
||||
_sky.DayFraction,
|
||||
_sky.ActiveDayGroup,
|
||||
foundation.Sky,
|
||||
foundation.EnvironOverrideActive,
|
||||
roots.ViewerCellId,
|
||||
roots.PlayerCellId,
|
||||
roots.PlayerViewPosition,
|
||||
camera.Camera.View,
|
||||
_diagnostics.CameraCellResolution));
|
||||
|
||||
// One visibility answer: the walk's exact visited-cell set
|
||||
// feeds effects and the frame environment (point lights and
|
||||
// directional-shadow filtering).
|
||||
_particleVisibility.MarkVisibleCells(pviewResult.VisibleCells);
|
||||
_frames.ObserveDrawableCells(pviewResult.VisibleCells);
|
||||
_diagnostics.EmitPViewInput(
|
||||
pviewResult.VisibleCells,
|
||||
pviewResult.ClipAssembly.OutsideViewSlices.Length,
|
||||
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;
|
||||
RenderProjectionCounts sourceCounts =
|
||||
pviewResult.SourceCounts;
|
||||
if (sourceCounts.IndoorCellStatic > 0 || hasOutsideSlice)
|
||||
sceneParticles = "pviewScoped";
|
||||
outdoorSceneryDrawn =
|
||||
sourceCounts.OutdoorStatic > 0
|
||||
&& hasOutsideSlice;
|
||||
liveDynamicDrawnCount =
|
||||
sourceCounts.LiveDynamicRoot;
|
||||
}
|
||||
else
|
||||
{
|
||||
_passes.DrawFlatEntities(
|
||||
in camera,
|
||||
_entities.LandblockEntries,
|
||||
roots.PlayerLandblockId,
|
||||
world.AnimatedEntityIds);
|
||||
_frames.ClearDrawableCells();
|
||||
}
|
||||
|
||||
_passes.DisableClipDistances();
|
||||
sceneParticles = _passes.DrawPostWorldParticles(
|
||||
clipRoot,
|
||||
pviewResult?.ClipAssembly,
|
||||
in camera,
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public PreparedWorldSceneFrame PrepareEnhanced(RenderFrameInput input)
|
||||
{
|
||||
_ = input;
|
||||
if (_hasPreparedEnhancedWorld)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The prior enhanced world preparation was not consumed.");
|
||||
}
|
||||
|
||||
RenderFrameFoundation foundation = _foundation.Foundation;
|
||||
if (!_availability.IsWorldAvailable || foundation.PortalViewportVisible)
|
||||
return new PreparedWorldSceneFrame(false, foundation, default, -1);
|
||||
|
||||
WorldRenderFrame world = _frames.Build(
|
||||
in foundation,
|
||||
_login.IsWaitingForLogin,
|
||||
_sky.ActiveDayGroup);
|
||||
WorldRootFrame roots = world.Roots;
|
||||
world = world with
|
||||
{
|
||||
ResidentStreamingWindow =
|
||||
_entities.CaptureResidentStreamingWindow(
|
||||
roots.RenderCenterLandblockX,
|
||||
roots.RenderCenterLandblockY),
|
||||
CelestialShadowSource =
|
||||
AuthoredCelestialShadowSourceResolver.Resolve(
|
||||
_sky.ActiveDayGroup,
|
||||
_sky.DayFraction,
|
||||
foundation.Sky),
|
||||
};
|
||||
_preparedEnhancedWorld = world;
|
||||
_hasPreparedEnhancedWorld = true;
|
||||
return new PreparedWorldSceneFrame(
|
||||
true,
|
||||
foundation,
|
||||
world,
|
||||
_sky.ActiveDayGroupIndex);
|
||||
}
|
||||
|
||||
public WorldRenderFrameOutcome RenderPreparedEnhanced(
|
||||
RenderFrameInput input,
|
||||
in PreparedWorldSceneFrame prepared)
|
||||
{
|
||||
if (prepared.ShouldRender != _hasPreparedEnhancedWorld)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The prepared enhanced-world token does not match the pending frame.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Render(input);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// A gate may change between prepare and execute. Never let that
|
||||
// exceptional edge leak borrowed builder scratch into another frame.
|
||||
_hasPreparedEnhancedWorld = false;
|
||||
_preparedEnhancedWorld = default;
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelPreparedEnhanced(in PreparedWorldSceneFrame prepared)
|
||||
{
|
||||
if (!prepared.ShouldRender)
|
||||
return;
|
||||
if (!_hasPreparedEnhancedWorld)
|
||||
return;
|
||||
_hasPreparedEnhancedWorld = false;
|
||||
_preparedEnhancedWorld = default;
|
||||
}
|
||||
|
||||
private void CompleteWorldFrame()
|
||||
{
|
||||
_alpha.EndFrame();
|
||||
_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -624,12 +624,14 @@ public sealed class ParticleSystem : IParticleSystem
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(destination);
|
||||
destination.Clear();
|
||||
LastRenderScopeEmitterVisitCount = 0;
|
||||
int passIndex = RenderPassIndex(renderPass);
|
||||
if (!_cellHandlesByPass[passIndex].TryGetValue(cellId, out OwnerEmitterBucket? bucket))
|
||||
return;
|
||||
|
||||
_scopeHandleScratch.Clear();
|
||||
bucket.CopyRenderableHandlesTo(_scopeHandleScratch);
|
||||
LastRenderScopeEmitterVisitCount = _scopeHandleScratch.Count;
|
||||
foreach (int handle in _scopeHandleScratch)
|
||||
{
|
||||
if (_byHandle.TryGetValue(handle, out ParticleEmitter? emitter))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue