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:
Erik 2026-09-03 06:52:28 +02:00
parent 7969c2e6ad
commit 6d5afcccde
11 changed files with 891 additions and 800 deletions

View file

@ -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,

View file

@ -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));

View file

@ -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,

View file

@ -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)

View file

@ -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;
}

View file

@ -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);
}
}
}
}

View file

@ -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))