feat(overhaul): select pack shadows from retail visibility

Borrow the exact prior-completed landscape visibility transaction and S2 CELLARRAY owner for the opt-in IA-24 directional-shadow pack. Select terrain by authored 1..64 cells, ordinary casters by CELLARRAY, and buildings by outdoor EffectCellId with no fallback.

Keep retained caster/material/terrain topology stable across visibility-only frames. Publish exact arbitrary active instance runs and bounded terrain commands through separate selection sequences; preserve transform-journal, fade/retry, deferral, shader/RHI, ordinary world, and pack-off behavior. Amend IA-24 and the S5 ledger.

Pre-commit gates: Release solution build 0 warnings/0 errors; focused visibility/frame/caster/prepared/GPU/terrain/pack lane 137/137; warmed caster/prepared/terrain selectors 0 B; git diff --check clean. Official hermetic and InstalledDat evidence intentionally run post-commit from this exact clean tree.

Mutation evidence (each restored exactly): (1) CELLARRAY->Parent first failed PriorLandscapeSelection expected [201,202,203,205], actual [204,205]. (2) all resident terrain first failed Assert.Single with 3 commands. (3) building EffectCell->anchor first failed expected trailing 205, actual 206. (4) admit missing membership first failed with extra 204. (5) completed->building scratch first failed completed-view Assert.True, expected true/actual false. (6) selection advanced BuildSequence first failed expected 1/actual 2. (7) alternating->prefix first failed active command count expected 3/actual 1.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-04 17:52:29 +02:00
parent 5c106bcdff
commit a4de2efc4e
19 changed files with 1198 additions and 100 deletions

View file

@ -5,6 +5,7 @@ using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Packs;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Vfx;
using DatReaderWriter.Enums;
namespace AcDream.App.Rendering;
@ -39,6 +40,7 @@ internal readonly record struct DirectionalSunShadowRenderInput(
bool MeasureGpuTimers = true,
bool MeasureCpuStages = false,
AtmosphericFrameBufferBinding AtmosphericFrame = default,
RetailLandscapeVisibilityFrame PriorLandscapeVisibility = default,
// #429 owner-approved pipelining: false keeps the retained caster/draw
// topology this frame (transform refresh only) so the rebuild lands on a
// quieter frame. The prepare seams below re-validate and rebuild anyway
@ -73,7 +75,8 @@ internal readonly record struct DirectionalShadowTransformChurnDiagnostics(
bool FlightFullDynamicFallback,
bool DenseDirectUpload,
bool DenseFlightReplay,
DirectionalShadowCasterClassDiagnostics CasterClasses = default);
DirectionalShadowCasterClassDiagnostics CasterClasses = default,
int ActiveSelectedCasters = 0);
internal readonly record struct DirectionalSunShadowDiagnostics(
DirectionalShadowGateReason GateReason,
@ -96,7 +99,15 @@ internal readonly record struct DirectionalSunShadowDiagnostics(
int SourceObjectIndex = -1,
uint SourceGfxObjId = 0u,
Vector3 SurfaceToLightDirection = default,
float LightElevationSin = 0f);
float LightElevationSin = 0f,
int ResidentWorldCasters = 0,
int ActiveWorldCasters = 0,
int ResidentWorldInstances = 0,
int ActiveWorldInstances = 0,
int ResidentWorldCommands = 0,
int ActiveWorldCommands = 0,
int ResidentTerrainCommands = 0,
int ActiveTerrainCommands = 0);
internal static class DirectionalShadowBatchFlags
{
@ -292,9 +303,9 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
internal DirectionalShadowFrameBinding CurrentFrameBinding => _currentFrameBinding;
/// <summary>
/// Topology-only command metadata lives in pack-owned device-local buffers.
/// It is rebuilt transactionally when the retained CPU product changes and
/// is never copied through a per-frame ring on a stable scene.
/// The bounded active indirect projection lives in pack-owned device-local
/// buffers. Selection republishes these buffers without rebuilding the
/// retained CPU topology and never copies them through a per-frame ring.
/// </summary>
internal long RetainedCommandBufferBytes => checked(
(_worldBatchBuffer?.SizeBytes ?? 0L)
@ -402,10 +413,15 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
world.PrepareDirectionalShadowDraws(
input.Casters,
input.AllowTopologyRebuild);
RetailLandscapeVisibilityFrame priorLandscapeVisibility =
input.PriorLandscapeVisibility;
DirectionalShadowTerrainPreparedDraws terrainDraws =
terrain.PrepareDirectionalShadowDraws();
terrain.PrepareDirectionalShadowDraws(
in priorLandscapeVisibility);
DirectionalShadowMeshGeometry? worldGeometry =
worldDraws.Commands.IsEmpty ? null : world.GetDirectionalShadowGeometry();
worldDraws.ActiveCommands.IsEmpty
? null
: world.GetDirectionalShadowGeometry();
DirectionalShadowTerrainGeometry? terrainGeometry =
terrainDraws.Commands.IsEmpty ? null : terrain.GetDirectionalShadowGeometry();
uint transformBindingSizeBytes =
@ -433,7 +449,7 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
DirectionalShadowCasterClassDiagnostics casterClasses =
CompleteCasterClassDiagnostics(
in casterStats,
terrainDraws.Commands.Length);
terrainDraws.ResidentRanges.Length);
DirectionalShadowTransformPublishStats publishStats =
_transformBuffers.LastStats;
var transformChurn = new DirectionalShadowTransformChurnDiagnostics(
@ -457,7 +473,8 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
publishStats.UsedFullDynamicFallback,
publishStats.DenseDirectUpload,
publishStats.DenseFlightReplay,
casterClasses);
casterClasses,
casterStats.ActiveSelected);
long preparedDrawsAndTransformsTicks = input.MeasureCpuStages
? Stopwatch.GetTimestamp() - cpuStageStarted
: 0L;
@ -661,7 +678,7 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
PublishDisabledReceiverBinding(frame, atmosphericFrame);
return Disabled(in environment, cpuStages);
}
if (!worldDraws.Commands.IsEmpty && worldGeometry is null)
if (!worldDraws.ActiveCommands.IsEmpty && worldGeometry is null)
throw new ArgumentNullException(nameof(worldGeometry));
if (!terrainDraws.Commands.IsEmpty && terrainGeometry is null)
throw new ArgumentNullException(nameof(terrainGeometry));
@ -812,9 +829,10 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
int drawsPerCascade = terrainDraws.Commands.IsEmpty ? 0 : 1;
drawsPerCascade = checked(
drawsPerCascade
+ (worldDraws.Commands.IsEmpty
+ (worldDraws.ActiveCommands.IsEmpty
? 0
: worldDraws.OpaqueRuns.Length + worldDraws.AlphaCutoutRuns.Length));
: worldDraws.ActiveOpaqueRuns.Length
+ worldDraws.ActiveAlphaCutoutRuns.Length));
long finished = Stopwatch.GetTimestamp();
cpuStages = cpuStages with
{
@ -833,8 +851,8 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
environment.Strength,
cascadeCount,
checked((MultiviewCascadesEnabled ? 1 : cascadeCount) * drawsPerCascade),
worldDraws.OpaqueCommandCount,
worldDraws.AlphaCutoutCommandCount,
worldDraws.ActiveOpaqueCommandCount,
worldDraws.ActiveAlphaCutoutCommandCount,
terrainDraws.Commands.Length,
worldDraws.BuildSequence,
terrainDraws.BuildSequence,
@ -848,7 +866,30 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
environment.SourceObjectIndex,
environment.SourceGfxObjId,
environment.SurfaceToLightDirection,
environment.LightElevationSin);
environment.LightElevationSin,
ResidentWorldCasters: inputCasterClassesCount(transformChurn),
ActiveWorldCasters: transformChurn.ActiveSelectedCasters,
ResidentWorldInstances: worldDraws.Stats.PreparedInstances,
ActiveWorldInstances: worldDraws.Stats.ActiveInstances,
ResidentWorldCommands: worldDraws.Commands.Length,
ActiveWorldCommands: worldDraws.ActiveCommands.Length,
ResidentTerrainCommands: terrainDraws.ResidentRanges.Length,
ActiveTerrainCommands: terrainDraws.Commands.Length);
static int inputCasterClassesCount(
DirectionalShadowTransformChurnDiagnostics churn)
{
DirectionalShadowCasterClassDiagnostics classes = churn.CasterClasses;
return checked(
classes.OutdoorStatics
+ classes.Buildings
+ classes.AnimatedStatics
+ classes.LocalPlayers
+ classes.RemotePlayers
+ classes.NonPlayerCreatures
+ classes.OtherLiveDynamics
+ classes.EquippedChildren);
}
}
private PreparedGpuUploads PrepareGpuData(
@ -856,9 +897,9 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
DirectionalShadowPreparedDraws world,
DirectionalShadowTerrainPreparedDraws terrain)
{
if (_worldGpuBuildSequence != world.BuildSequence)
if (_worldGpuBuildSequence != world.ActiveSelectionSequence)
RebuildWorldGpuData(world);
if (_terrainGpuBuildSequence != terrain.BuildSequence)
if (_terrainGpuBuildSequence != terrain.ActiveSelectionSequence)
RebuildTerrainGpuData(terrain);
return new PreparedGpuUploads(
@ -874,12 +915,12 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
IGpuBuffer? commands = null;
try
{
if (!world.Commands.IsEmpty)
if (!world.ActiveCommands.IsEmpty)
{
EnsureBatchCapacity(world.Batches.Length);
for (int i = 0; i < world.Batches.Length; i++)
EnsureBatchCapacity(world.ActiveBatches.Length);
for (int i = 0; i < world.ActiveBatches.Length; i++)
{
DirectionalShadowPreparedBatch batch = world.Batches[i];
DirectionalShadowPreparedBatch batch = world.ActiveBatches[i];
_batchScratch[i] = new DirectionalShadowBatchGpuData(
batch.TextureSlot.Index,
0u,
@ -892,15 +933,15 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
}
ReadOnlySpan<byte> batchBytes = MemoryMarshal.AsBytes(
_batchScratch.AsSpan(0, world.Batches.Length));
_batchScratch.AsSpan(0, world.ActiveBatches.Length));
ReadOnlySpan<byte> commandBytes = MemoryMarshal.AsBytes(
world.Commands);
world.ActiveCommands);
batches = CreateRetainedBuffer(
$"directional-shadow-world-batches-{world.BuildSequence}",
$"directional-shadow-world-batches-{world.ActiveSelectionSequence}",
batchBytes,
GpuBufferUsage.Storage);
commands = CreateRetainedBuffer(
$"directional-shadow-world-commands-{world.BuildSequence}",
$"directional-shadow-world-commands-{world.ActiveSelectionSequence}",
commandBytes,
GpuBufferUsage.Indirect);
}
@ -916,7 +957,7 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
IGpuBuffer? previousCommands = _worldCommandBuffer;
_worldBatchBuffer = batches;
_worldCommandBuffer = commands;
_worldGpuBuildSequence = world.BuildSequence;
_worldGpuBuildSequence = world.ActiveSelectionSequence;
previousCommands?.Dispose();
previousBatches?.Dispose();
}
@ -927,14 +968,14 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
if (!terrain.Commands.IsEmpty)
{
commands = CreateRetainedBuffer(
$"directional-shadow-terrain-commands-{terrain.BuildSequence}",
$"directional-shadow-terrain-commands-{terrain.ActiveSelectionSequence}",
MemoryMarshal.AsBytes(terrain.Commands),
GpuBufferUsage.Indirect);
}
IGpuBuffer? previous = _terrainCommandBuffer;
_terrainCommandBuffer = commands;
_terrainGpuBuildSequence = terrain.BuildSequence;
_terrainGpuBuildSequence = terrain.ActiveSelectionSequence;
previous?.Dispose();
}
@ -997,7 +1038,7 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
IGpuPipeline? opaquePipeline = null,
IGpuPipeline? cutoutPipeline = null)
{
if (draws.Commands.IsEmpty)
if (draws.ActiveCommands.IsEmpty)
return;
DirectionalShadowMeshGeometry actual = geometry!.Value;
encoder.BindStorageBuffer(
@ -1013,14 +1054,14 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
DrawWorldRange(
encoder,
uploads.WorldCommands,
draws.OpaqueRuns,
draws.ActiveOpaqueRuns,
cascadeIndex,
opaquePipeline ?? _worldOpaquePipeline,
actual);
DrawWorldRange(
encoder,
uploads.WorldCommands,
draws.AlphaCutoutRuns,
draws.ActiveAlphaCutoutRuns,
cascadeIndex,
cutoutPipeline ?? _worldCutoutPipeline,
actual);

View file

@ -4,6 +4,7 @@ using System.Numerics;
using System.Runtime.InteropServices;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Rendering.Wb;
using AcDream.Core.World;
using AcDream.Plugin.Abstractions.Rendering;
@ -421,6 +422,12 @@ internal sealed class AtmosphericPostProcessGraph :
!shadowInputsChanged || _shadowRebuildDeferrals >= 2;
ulong casterSequenceBefore = _shadowCasters.BuildSequence;
_shadowCasters.Build(in scene, allowTopologyRebuild);
RetailLandscapeVisibilityFrame priorLandscapeVisibility =
world.PriorLandscapeVisibility;
_shadowCasters.Select(
in priorLandscapeVisibility,
world.DirectionalShadowCellMembership
?? EmptyDirectionalShadowCellMembership.Instance);
if (_shadowCasters.BuildSequence != casterSequenceBefore)
allowTopologyRebuild = true;
_shadowRebuildDeferrals = allowTopologyRebuild
@ -463,9 +470,10 @@ internal sealed class AtmosphericPostProcessGraph :
frame.Serial),
MeasureCpuStages: measureCpuStages,
AtmosphericFrame: shadowAtmosphericFrame,
PriorLandscapeVisibility: world.PriorLandscapeVisibility,
AllowTopologyRebuild: allowTopologyRebuild);
long environmentFinished = measureCpuStages ? Stopwatch.GetTimestamp() : 0L;
_lastShadowCasterCount = _shadowCasters.Stats.Accepted;
_lastShadowCasterCount = _shadowCasters.Stats.ActiveSelected;
_lastShadowClassificationCalls = _shadowCasters.Stats.TopologyRebuilt ? 1 : 0;
_lastShadowDiagnostics = _directionalShadows.Render(
frame,

View file

@ -2,6 +2,7 @@ using System.Numerics;
using System.Runtime.InteropServices;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Rendering.Wb;
using AcDream.Core.World;
using AcDream.Plugin.Abstractions.Rendering;
@ -201,6 +202,12 @@ internal class DeclaredFullscreenRenderPackGraph :
?? throw new InvalidOperationException(
$"Pack '{Descriptor.Id}' has no declared directional-shadow executor.");
_shadowCasters.Build(in scene);
RetailLandscapeVisibilityFrame priorLandscapeVisibility =
world.PriorLandscapeVisibility;
_shadowCasters.Select(
in priorLandscapeVisibility,
world.DirectionalShadowCellMembership
?? EmptyDirectionalShadowCellMembership.Instance);
AuthoredCelestialShadowSource source = world.CelestialShadowSource;
float elevationStrength = RenderPackAtmospherePolicyEvaluation
.DirectionalShadowFromSin(
@ -225,8 +232,9 @@ internal class DeclaredFullscreenRenderPackGraph :
world.Camera.Projection,
_shadowCasters,
ResidentMaximumReachMeters:
world.ResidentStreamingWindow.MaximumReachMeters);
_lastShadowCasterCount = _shadowCasters.Stats.Accepted;
world.ResidentStreamingWindow.MaximumReachMeters,
PriorLandscapeVisibility: world.PriorLandscapeVisibility);
_lastShadowCasterCount = _shadowCasters.Stats.ActiveSelected;
_lastShadowClassificationCalls = _shadowCasters.Stats.TopologyRebuilt ? 1 : 0;
_lastShadowDiagnostics = renderer.Render(
frame,

View file

@ -1,3 +1,5 @@
using AcDream.App.Rendering.Vfx;
namespace AcDream.App.Rendering.Scene;
/// <summary>
@ -80,7 +82,8 @@ internal readonly record struct DirectionalShadowCasterBuildStats(
int LiveDynamicRootChanges = 0,
int EquippedChildChanges = 0,
bool DensityBulkRefresh = false,
int BatchedProjectionCopyCalls = 0)
int BatchedProjectionCopyCalls = 0,
int ActiveSelected = 0)
{
public DirectionalShadowCasterClassDiagnostics CasterClasses { get; init; }
}
@ -96,6 +99,7 @@ internal sealed class DirectionalShadowCasterFrame
private RenderProjectionRecord[] _outdoorStaticScratch = [];
private RenderProjectionRecord[] _outdoorDynamicScratch = [];
private DirectionalShadowCaster[] _casters = [];
private bool[] _selectedCasters = [];
private int[] _refreshCasterSlots = [];
private DirectionalShadowChangedPose[] _changedCasterPoses = [];
private bool[] _changedCasterFlags = [];
@ -123,9 +127,16 @@ internal sealed class DirectionalShadowCasterFrame
public ulong BuildSequence { get; private set; }
/// <summary>Per-frame active-selection publication identity. It is
/// intentionally independent from retained topology <see cref="BuildSequence"/>.</summary>
public ulong SelectionSequence { get; private set; }
public ReadOnlySpan<DirectionalShadowCaster> Casters =>
_casters.AsSpan(0, _casterCount);
internal ReadOnlySpan<bool> SelectedCasters =>
_selectedCasters.AsSpan(0, _casterCount);
internal ReadOnlySpan<int> RefreshCasterSlots =>
_refreshCasterSlots.AsSpan(0, _refreshCasterSlotCount);
@ -144,6 +155,7 @@ internal sealed class DirectionalShadowCasterFrame
* System.Runtime.CompilerServices.Unsafe.SizeOf<RenderProjectionRecord>()
+ (long)_casters.Length
* System.Runtime.CompilerServices.Unsafe.SizeOf<DirectionalShadowCaster>()
+ _selectedCasters.Length
+ (long)_refreshCasterSlots.Length * sizeof(int)
+ (long)_changedCasterPoses.Length
* System.Runtime.CompilerServices.Unsafe.SizeOf<
@ -233,6 +245,7 @@ internal sealed class DirectionalShadowCasterFrame
RenderSceneIndex.OutdoorDynamic,
_outdoorDynamicScratch.AsSpan(0, counts.OutdoorDynamic));
EnsureCapacity(ref _casters, checked(staticCount + dynamicCount));
EnsureCapacity(ref _selectedCasters, checked(staticCount + dynamicCount));
_casterCount = 0;
int rejectedNotDrawable = 0;
@ -394,6 +407,67 @@ internal sealed class DirectionalShadowCasterFrame
}
}
/// <summary>
/// Projects the prior-completed retail landscape product onto the retained
/// caster topology. This performs no render-scene copy, classification,
/// mesh lookup, or topology revision change.
/// </summary>
internal void Select(
in RetailLandscapeVisibilityFrame visibility,
IDirectionalShadowCellMembership membership)
{
ArgumentNullException.ThrowIfNull(membership);
IReadOnlySet<uint> visible = visibility.CellIds
?? RetailLandscapeVisibilityFrame.None.CellIds;
int selected = 0;
for (int casterIndex = 0; casterIndex < _casterCount; casterIndex++)
{
ref readonly DirectionalShadowCaster caster =
ref _casters[casterIndex];
bool active = visibility.HasCompletedWorldView
&& SelectsCaster(in caster, visible, membership);
_selectedCasters[casterIndex] = active;
if (active)
selected++;
}
SelectionSequence = checked(SelectionSequence + 1);
Stats = Stats with { ActiveSelected = selected };
}
private static bool SelectsCaster(
in DirectionalShadowCaster caster,
IReadOnlySet<uint> visible,
IDirectionalShadowCellMembership membership)
{
RenderSourceMetadata source = caster.Projection.Source;
if (caster.Kind is DirectionalShadowCasterKind.Building)
return IsOutdoorLandCell(source.EffectCellId)
&& visible.Contains(source.EffectCellId);
if (!membership.TryGetRetailCellArray(
source.LocalEntityId,
out IReadOnlyList<uint>? cells)
|| cells.Count == 0)
{
return false;
}
for (int cellIndex = 0; cellIndex < cells.Count; cellIndex++)
{
uint cellId = cells[cellIndex];
if (IsOutdoorLandCell(cellId) && visible.Contains(cellId))
return true;
}
return false;
}
private static bool IsOutdoorLandCell(uint cellId)
{
uint low = cellId & 0xFFFFu;
return low != 0u && low < 0x0100u;
}
/// <summary>
/// Pure pre-check for the deferral gate: would refreshing from the journal
/// demand the dense by-id re-copy? The journal copy is a read; the state

View file

@ -1,37 +1,49 @@
using System.Runtime.CompilerServices;
using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Vfx;
namespace AcDream.App.Rendering;
internal readonly record struct DirectionalShadowTerrainRange(
uint FirstIndex,
int IndexCount);
int IndexCount,
uint LandblockId = 0u);
internal readonly record struct DirectionalShadowTerrainGeometry(
IGpuBuffer VertexBuffer,
IGpuBuffer IndexBuffer);
/// <summary>
/// The complete resident terrain arena expressed once as indirect commands.
/// It deliberately has no camera, PView, portal, or cascade input.
/// The complete resident terrain arena expressed once as retained slot ranges,
/// plus a separately sequenced active indirect projection.
/// </summary>
internal sealed class DirectionalShadowTerrainPreparedDraws
{
private DrawElementsIndirectCommand[] _commands = [];
private int _count;
private DirectionalShadowTerrainRange[] _ranges = [];
private DrawElementsIndirectCommand[] _activeCommands = [];
private int _rangeCount;
private int _activeCount;
private bool _building;
public long SourceFrameSequence { get; private set; }
public ulong BuildSequence { get; private set; }
public ulong ActiveSelectionSequence { get; private set; }
public ReadOnlySpan<DrawElementsIndirectCommand> Commands =>
_commands.AsSpan(0, _count);
_activeCommands.AsSpan(0, _activeCount);
public ReadOnlySpan<DirectionalShadowTerrainRange> ResidentRanges =>
_ranges.AsSpan(0, _rangeCount);
public long RetainedScratchBytes =>
checked((long)_commands.Length
* Unsafe.SizeOf<DrawElementsIndirectCommand>());
checked(
(long)_ranges.Length
* Unsafe.SizeOf<DirectionalShadowTerrainRange>()
+ (long)_activeCommands.Length
* Unsafe.SizeOf<DrawElementsIndirectCommand>());
public bool TryBegin(long frameSequence, int estimatedCommands)
{
@ -45,7 +57,7 @@ internal sealed class DirectionalShadowTerrainPreparedDraws
return false;
EnsureCapacity(estimatedCommands);
_count = 0;
_rangeCount = 0;
_building = true;
return true;
}
@ -57,15 +69,8 @@ internal sealed class DirectionalShadowTerrainPreparedDraws
"Begin a terrain shadow draw build before adding ranges.");
if (range.IndexCount <= 0)
throw new ArgumentOutOfRangeException(nameof(range));
EnsureCapacity(checked(_count + 1));
_commands[_count++] = new DrawElementsIndirectCommand
{
Count = checked((uint)range.IndexCount),
InstanceCount = 1,
FirstIndex = range.FirstIndex,
BaseVertex = 0,
BaseInstance = 0,
};
EnsureCapacity(checked(_rangeCount + 1));
_ranges[_rangeCount++] = range;
}
public void Complete(long frameSequence)
@ -78,22 +83,73 @@ internal sealed class DirectionalShadowTerrainPreparedDraws
SourceFrameSequence = frameSequence;
BuildSequence = checked(BuildSequence + 1);
_building = false;
RebuildActiveAll();
}
internal void ApplySelection(in RetailLandscapeVisibilityFrame visibility)
{
IReadOnlySet<uint> visible = visibility.CellIds
?? RetailLandscapeVisibilityFrame.None.CellIds;
_activeCount = 0;
if (visibility.HasCompletedWorldView)
{
for (int rangeIndex = 0; rangeIndex < _rangeCount; rangeIndex++)
{
DirectionalShadowTerrainRange range = _ranges[rangeIndex];
uint prefix = range.LandblockId & 0xFFFF0000u;
bool selected = false;
for (uint low = 1u; low <= 64u; low++)
{
if (visible.Contains(prefix | low))
{
selected = true;
break;
}
}
if (selected)
Emit(in range);
}
}
ActiveSelectionSequence = checked(ActiveSelectionSequence + 1);
}
private void RebuildActiveAll()
{
EnsureCapacity(_rangeCount);
_activeCount = 0;
for (int rangeIndex = 0; rangeIndex < _rangeCount; rangeIndex++)
Emit(in _ranges[rangeIndex]);
ActiveSelectionSequence = checked(ActiveSelectionSequence + 1);
}
private void Emit(in DirectionalShadowTerrainRange range)
{
_activeCommands[_activeCount++] = new DrawElementsIndirectCommand
{
Count = checked((uint)range.IndexCount),
InstanceCount = 1,
FirstIndex = range.FirstIndex,
BaseVertex = 0,
BaseInstance = 0,
};
}
public void Abort()
{
_count = 0;
_rangeCount = 0;
_activeCount = 0;
_building = false;
}
private void EnsureCapacity(int required)
{
if (_commands.Length >= required)
if (_ranges.Length >= required)
return;
int capacity = _commands.Length == 0 ? 16 : _commands.Length;
int capacity = _ranges.Length == 0 ? 16 : _ranges.Length;
while (capacity < required)
capacity = checked(capacity * 2);
Array.Resize(ref _commands, capacity);
Array.Resize(ref _ranges, capacity);
Array.Resize(ref _activeCommands, capacity);
}
}
@ -101,46 +157,107 @@ public sealed partial class TerrainModernRenderer
{
private readonly DirectionalShadowTerrainPreparedDraws
_directionalShadowTerrainDraws = new();
// BeginFrame's established overflow guard/frame counter remains part of
// the ordinary renderer lifecycle; shadow topology no longer keys on it.
private long _directionalShadowFrameSequence;
private uint[] _directionalShadowSlotLandblocks = [];
private uint[] _directionalShadowSlotFirstIndices = [];
private int[] _directionalShadowSlotIndexCounts = [];
private bool[] _directionalShadowSlotPresent = [];
private bool _directionalShadowTopologySnapshotValid;
private long _directionalShadowTopologySequence;
internal DirectionalShadowTerrainGeometry GetDirectionalShadowGeometry() => new(
_vertexStore ?? throw new InvalidOperationException("Terrain has no vertex store."),
_indexStore ?? throw new InvalidOperationException("Terrain has no index store."));
/// <summary>
/// Builds one all-resident indirect list for the current frame. Repeated
/// calls by individual cascades return the same retained product.
/// Retains exact loaded-slot topology and projects the borrowed prior-view
/// selection into a bounded indirect list. Camera-only changes do not
/// advance the topology build sequence or rebuild terrain geometry.
/// </summary>
internal DirectionalShadowTerrainPreparedDraws
PrepareDirectionalShadowDraws()
PrepareDirectionalShadowDraws(
in RetailLandscapeVisibilityFrame visibility)
{
if (!_directionalShadowTerrainDraws.TryBegin(
_directionalShadowFrameSequence,
_alloc.LoadedCount))
EnsureDirectionalShadowSlotCapacity(_slots.Length);
bool topologyChanged = !_directionalShadowTopologySnapshotValid;
for (int slot = 0; slot < _slots.Length; slot++)
{
return _directionalShadowTerrainDraws;
SlotData? data = _slots[slot];
bool present = data is not null;
if (_directionalShadowSlotPresent[slot] != present
|| present
&& (_directionalShadowSlotLandblocks[slot] != data!.LandblockId
|| _directionalShadowSlotFirstIndices[slot] != data.FirstIndex
|| _directionalShadowSlotIndexCounts[slot] != data.IndexCount))
{
topologyChanged = true;
}
}
try
if (topologyChanged)
{
for (int slot = 0; slot < _slots.Length; slot++)
_directionalShadowTopologySequence = checked(
_directionalShadowTopologySequence + 1);
_directionalShadowTerrainDraws.TryBegin(
_directionalShadowTopologySequence,
_alloc.LoadedCount);
try
{
SlotData? data = _slots[slot];
if (data is null)
continue;
var range = new DirectionalShadowTerrainRange(
data.FirstIndex,
data.IndexCount);
_directionalShadowTerrainDraws.Add(in range);
for (int slot = 0; slot < _slots.Length; slot++)
{
SlotData? data = _slots[slot];
bool present = data is not null;
_directionalShadowSlotPresent[slot] = present;
if (!present)
{
_directionalShadowSlotLandblocks[slot] = 0u;
_directionalShadowSlotFirstIndices[slot] = 0u;
_directionalShadowSlotIndexCounts[slot] = 0;
continue;
}
_directionalShadowSlotLandblocks[slot] = data!.LandblockId;
_directionalShadowSlotFirstIndices[slot] = data.FirstIndex;
_directionalShadowSlotIndexCounts[slot] = data.IndexCount;
var range = new DirectionalShadowTerrainRange(
data.FirstIndex,
data.IndexCount,
data.LandblockId);
_directionalShadowTerrainDraws.Add(in range);
}
_directionalShadowTerrainDraws.Complete(
_directionalShadowTopologySequence);
_directionalShadowTopologySnapshotValid = true;
}
catch
{
// The snapshot fields are populated while the retained
// product is built. If publication fails, force the next
// frame to retry even when those fields already match the
// live slots; an aborted build is never a valid topology.
_directionalShadowTopologySnapshotValid = false;
_directionalShadowTerrainDraws.Abort();
throw;
}
_directionalShadowTerrainDraws.Complete(
_directionalShadowFrameSequence);
return _directionalShadowTerrainDraws;
}
catch
{
_directionalShadowTerrainDraws.Abort();
throw;
}
_directionalShadowTerrainDraws.ApplySelection(in visibility);
return _directionalShadowTerrainDraws;
}
private void EnsureDirectionalShadowSlotCapacity(int required)
{
if (_directionalShadowSlotPresent.Length >= required)
return;
int capacity = _directionalShadowSlotPresent.Length == 0
? 16
: _directionalShadowSlotPresent.Length;
while (capacity < required)
capacity = checked(capacity * 2);
Array.Resize(ref _directionalShadowSlotLandblocks, capacity);
Array.Resize(ref _directionalShadowSlotFirstIndices, capacity);
Array.Resize(ref _directionalShadowSlotIndexCounts, capacity);
Array.Resize(ref _directionalShadowSlotPresent, capacity);
}
}

View file

@ -12,6 +12,21 @@ internal interface IWorldSceneParticleVisibility
void AbortFrame();
}
/// <summary>
/// Borrowed prior-completed retail landscape visibility. <see cref="CellIds"/>
/// is the controller's one retained completed set, not a reconstructed or
/// current-camera answer. The borrow is valid through the enhanced-world
/// prepass that captured it.
/// </summary>
internal readonly record struct RetailLandscapeVisibilityFrame(
IReadOnlySet<uint> CellIds,
bool HasCompletedWorldView)
{
internal static RetailLandscapeVisibilityFrame None { get; } = new(
System.Collections.Frozen.FrozenSet<uint>.Empty,
HasCompletedWorldView: false);
}
/// <summary>
/// Bridges the retained retail PView result into the next physics update's
/// <c>CObjCell::IsInView</c> particle gate. The controller owns only immutable
@ -115,6 +130,13 @@ public sealed class ParticleVisibilityController : IWorldSceneParticleVisibility
rangeMultiplier);
}
/// <summary>
/// Borrows the exact prior-completed landscape product for the opt-in
/// directional-shadow prepass. No set is copied or reconstructed.
/// </summary>
internal RetailLandscapeVisibilityFrame CaptureCompletedLandscapeVisibility() =>
new(_completedCellIds, _hasCompletedWorldView);
public void Reset()
{
_buildingCellIds.Clear();

View file

@ -42,7 +42,9 @@ internal readonly record struct DirectionalShadowPreparationStats(
int RejectedTransparentBatches,
int RejectedFadedParts,
int MissingMeshes,
int UnresolvedAlphaCutoutTextures);
int UnresolvedAlphaCutoutTextures,
int ActiveInstances = 0,
int ActiveCommands = 0);
internal readonly record struct DirectionalShadowMeshGeometry(
IGpuBuffer VertexBuffer,
@ -55,6 +57,14 @@ internal readonly record struct DirectionalShadowTransformSource(
bool IsSetupPart,
Matrix4x4 SetupPartTransform)
{
public static DirectionalShadowTransformSource Static(int casterIndex) =>
new(
false,
casterIndex,
MeshIndex: 0,
IsSetupPart: false,
SetupPartTransform: default);
public static DirectionalShadowTransformSource Dynamic(
int casterIndex,
int meshIndex,
@ -89,6 +99,9 @@ internal sealed class DirectionalShadowPreparedDraws
private DrawElementsIndirectCommand[] _commands = [];
private DirectionalShadowPreparedBatch[] _batches = [];
private DirectionalShadowPreparedRun[] _runs = [];
private DrawElementsIndirectCommand[] _activeCommands = [];
private DirectionalShadowPreparedBatch[] _activeBatches = [];
private DirectionalShadowPreparedRun[] _activeRuns = [];
private int[] _drawNextInGroup = [];
private int[] _groupHead = [];
private int[] _groupTail = [];
@ -101,6 +114,8 @@ internal sealed class DirectionalShadowPreparedDraws
private int _sourceCount;
private int _commandCount;
private int _runCount;
private int _activeCommandCount;
private int _activeRunCount;
private int _dynamicTransformSlotCount;
private int _allDynamicTransformSlotCount;
private int _mappedCasterCount;
@ -117,6 +132,10 @@ internal sealed class DirectionalShadowPreparedDraws
public ulong BuildSequence { get; private set; }
public ulong SourceCasterSelectionSequence { get; private set; }
public ulong ActiveSelectionSequence { get; private set; }
public int LastDynamicTransformRefreshCount { get; private set; }
public bool LastDynamicTransformRefreshWasDense { get; private set; }
@ -174,6 +193,27 @@ internal sealed class DirectionalShadowPreparedDraws
public ReadOnlySpan<DirectionalShadowPreparedRun> AlphaCutoutRuns =>
_runs.AsSpan(OpaqueRunCount, _runCount - OpaqueRunCount);
public int ActiveOpaqueCommandCount { get; private set; }
public int ActiveAlphaCutoutCommandCount =>
_activeCommandCount - ActiveOpaqueCommandCount;
public int ActiveOpaqueRunCount { get; private set; }
public ReadOnlySpan<DrawElementsIndirectCommand> ActiveCommands =>
_activeCommands.AsSpan(0, _activeCommandCount);
public ReadOnlySpan<DirectionalShadowPreparedBatch> ActiveBatches =>
_activeBatches.AsSpan(0, _activeCommandCount);
public ReadOnlySpan<DirectionalShadowPreparedRun> ActiveOpaqueRuns =>
_activeRuns.AsSpan(0, ActiveOpaqueRunCount);
public ReadOnlySpan<DirectionalShadowPreparedRun> ActiveAlphaCutoutRuns =>
_activeRuns.AsSpan(
ActiveOpaqueRunCount,
_activeRunCount - ActiveOpaqueRunCount);
public DirectionalShadowPreparationStats Stats { get; private set; }
public long RetainedScratchBytes => checked(
@ -193,6 +233,9 @@ internal sealed class DirectionalShadowPreparedDraws
+ (long)_commands.Length * Unsafe.SizeOf<DrawElementsIndirectCommand>()
+ (long)_batches.Length * Unsafe.SizeOf<DirectionalShadowPreparedBatch>()
+ (long)_runs.Length * Unsafe.SizeOf<DirectionalShadowPreparedRun>()
+ (long)_activeCommands.Length * Unsafe.SizeOf<DrawElementsIndirectCommand>()
+ (long)_activeBatches.Length * Unsafe.SizeOf<DirectionalShadowPreparedBatch>()
+ (long)_activeRuns.Length * Unsafe.SizeOf<DirectionalShadowPreparedRun>()
+ (long)_drawNextInGroup.Length * sizeof(int)
+ (long)_groupHead.Length * sizeof(int)
+ (long)_groupTail.Length * sizeof(int)
@ -248,6 +291,8 @@ internal sealed class DirectionalShadowPreparedDraws
_sourceCount = 0;
_commandCount = 0;
_runCount = 0;
_activeCommandCount = 0;
_activeRunCount = 0;
_dynamicTransformSlotCount = 0;
_allDynamicTransformSlotCount = 0;
if (_mappedCasterCount != 0)
@ -260,6 +305,8 @@ internal sealed class DirectionalShadowPreparedDraws
_mappedCasterCount = 0;
OpaqueCommandCount = 0;
OpaqueRunCount = 0;
ActiveOpaqueCommandCount = 0;
ActiveOpaqueRunCount = 0;
Stats = default;
LastDynamicTransformRefreshCount = 0;
LastDynamicTransformRefreshWasDense = false;
@ -443,6 +490,9 @@ internal sealed class DirectionalShadowPreparedDraws
EnsureCapacity(ref _commands, _sourceCount);
EnsureCapacity(ref _batches, _sourceCount);
EnsureCapacity(ref _runs, _sourceCount);
EnsureCapacity(ref _activeCommands, _sourceCount);
EnsureCapacity(ref _activeBatches, _sourceCount);
EnsureCapacity(ref _activeRuns, _sourceCount);
int maxCasterIndex = -1;
for (int index = 0; index < _sourceCount; index++)
@ -562,6 +612,152 @@ internal sealed class DirectionalShadowPreparedDraws
PreparedAlphaCutoutCommands = commandIndex - opaqueCommands,
};
_building = false;
RebuildActiveAll();
}
/// <summary>
/// Projects arbitrary selected caster instances into contiguous transform
/// runs without changing retained material/mesh topology or its transform
/// address space.
/// </summary>
internal void ApplySelection(DirectionalShadowCasterFrame casters)
{
ArgumentNullException.ThrowIfNull(casters);
if (casters.SelectionSequence == 0)
return;
if (SourceGeneration != casters.Generation
|| SourceCasterBuildSequence != casters.BuildSequence)
{
throw new InvalidOperationException(
"Directional-shadow selection does not match prepared topology.");
}
if (SourceCasterSelectionSequence == casters.SelectionSequence)
return;
ApplySelection(casters.SelectedCasters, casters.SelectionSequence);
}
internal void ApplySelection(
ReadOnlySpan<bool> selected,
ulong casterSelectionSequence)
{
if (casterSelectionSequence == 0)
throw new ArgumentOutOfRangeException(nameof(casterSelectionSequence));
if (SourceCasterSelectionSequence == casterSelectionSequence)
return;
_activeCommandCount = 0;
int activeInstances = 0;
for (int commandIndex = 0; commandIndex < _commandCount; commandIndex++)
{
DrawElementsIndirectCommand command = _commands[commandIndex];
int first = checked((int)command.BaseInstance);
int end = checked(first + (int)command.InstanceCount);
int runStart = -1;
for (int transformIndex = first; transformIndex < end; transformIndex++)
{
int casterIndex = _transformSources[transformIndex].CasterIndex;
if ((uint)casterIndex >= (uint)selected.Length)
{
throw new InvalidOperationException(
"Prepared directional-shadow instance has a stale caster slot.");
}
bool active = selected[casterIndex];
if (active && runStart < 0)
runStart = transformIndex;
if (!active && runStart >= 0)
{
EmitActive(commandIndex, in command, runStart, transformIndex - runStart);
activeInstances += transformIndex - runStart;
runStart = -1;
}
}
if (runStart >= 0)
{
EmitActive(commandIndex, in command, runStart, end - runStart);
activeInstances += end - runStart;
}
}
BuildActiveRuns();
SourceCasterSelectionSequence = casterSelectionSequence;
ActiveSelectionSequence = checked(ActiveSelectionSequence + 1);
Stats = Stats with
{
ActiveInstances = activeInstances,
ActiveCommands = _activeCommandCount,
};
}
private void RebuildActiveAll()
{
EnsureCapacity(ref _activeCommands, _commandCount);
EnsureCapacity(ref _activeBatches, _commandCount);
EnsureCapacity(ref _activeRuns, _runCount);
_commands.AsSpan(0, _commandCount).CopyTo(_activeCommands);
_batches.AsSpan(0, _commandCount).CopyTo(_activeBatches);
_activeCommandCount = _commandCount;
BuildActiveRuns();
SourceCasterSelectionSequence = 0;
ActiveSelectionSequence = checked(ActiveSelectionSequence + 1);
Stats = Stats with
{
ActiveInstances = _sourceCount,
ActiveCommands = _activeCommandCount,
};
}
private void EmitActive(
int sourceCommandIndex,
in DrawElementsIndirectCommand source,
int baseInstance,
int instanceCount)
{
int destination = _activeCommandCount++;
_activeCommands[destination] = source with
{
BaseInstance = checked((uint)baseInstance),
InstanceCount = checked((uint)instanceCount),
};
_activeBatches[destination] = _batches[sourceCommandIndex];
}
private void BuildActiveRuns()
{
_activeRunCount = 0;
ActiveOpaqueCommandCount = 0;
while (ActiveOpaqueCommandCount < _activeCommandCount
&& _activeBatches[ActiveOpaqueCommandCount].Material
is DirectionalShadowCasterMaterial.Opaque)
{
ActiveOpaqueCommandCount++;
}
int runStart = 0;
while (runStart < _activeCommandCount)
{
DirectionalShadowPreparedBatch first = _activeBatches[runStart];
int runEnd = runStart + 1;
while (runEnd < _activeCommandCount
&& _activeBatches[runEnd].CullMode == first.CullMode
&& _activeBatches[runEnd].Material == first.Material)
{
runEnd++;
}
_activeRuns[_activeRunCount++] = new DirectionalShadowPreparedRun(
runStart,
runEnd - runStart,
first.CullMode,
first.Material);
runStart = runEnd;
}
ActiveOpaqueRunCount = 0;
while (ActiveOpaqueRunCount < _activeRunCount
&& _activeRuns[ActiveOpaqueRunCount].Material
is DirectionalShadowCasterMaterial.Opaque)
{
ActiveOpaqueRunCount++;
}
}
public void RefreshDynamicTransforms(
@ -878,6 +1074,8 @@ internal sealed class DirectionalShadowPreparedDraws
_sourceCount = 0;
_commandCount = 0;
_runCount = 0;
_activeCommandCount = 0;
_activeRunCount = 0;
_dynamicTransformSlotCount = 0;
_allDynamicTransformSlotCount = 0;
if (_mappedCasterCount != 0)
@ -890,11 +1088,14 @@ internal sealed class DirectionalShadowPreparedDraws
_mappedCasterCount = 0;
OpaqueCommandCount = 0;
OpaqueRunCount = 0;
ActiveOpaqueCommandCount = 0;
ActiveOpaqueRunCount = 0;
Stats = default;
SourceGeneration = default;
SourceCasterBuildSequence = 0;
SourceRenderDataAvailabilityVersion = 0;
SourceTranslucencyFadeRevision = 0;
SourceCasterSelectionSequence = 0;
LastDynamicTransformRefreshCount = 0;
LastDynamicTransformRefreshWasDense = false;
_retryClassificationNextFrame = false;
@ -1034,6 +1235,7 @@ public sealed partial class WbDrawDispatcher
translucencyFadeRevision))
{
_directionalShadowDraws.RefreshDynamicTransforms(casters);
_directionalShadowDraws.ApplySelection(casters);
return _directionalShadowDraws;
}
// #429 owner-approved pipelining: a deferred frame keeps the retained
@ -1047,6 +1249,7 @@ public sealed partial class WbDrawDispatcher
&& _directionalShadowDraws.SourceGeneration == casters.Generation)
{
_directionalShadowDraws.RefreshDynamicTransforms(casters);
_directionalShadowDraws.ApplySelection(casters);
return _directionalShadowDraws;
}
@ -1064,6 +1267,7 @@ public sealed partial class WbDrawDispatcher
renderDataAvailabilityVersion,
translucencyFadeRevision))
{
_directionalShadowDraws.ApplySelection(casters);
return _directionalShadowDraws;
}
@ -1168,7 +1372,8 @@ public sealed partial class WbDrawDispatcher
meshIndex,
true,
in partTransform)
: default;
: DirectionalShadowTransformSource.Static(
casterIndex);
AddDirectionalShadowBatches(
partData,
in candidate,
@ -1203,7 +1408,8 @@ public sealed partial class WbDrawDispatcher
meshIndex,
false,
in noSetupPart)
: default;
: DirectionalShadowTransformSource.Static(
casterIndex);
AddDirectionalShadowBatches(
renderData,
in candidate,
@ -1236,6 +1442,7 @@ public sealed partial class WbDrawDispatcher
in stats,
renderDataAvailabilityVersion,
translucencyFadeRevision);
_directionalShadowDraws.ApplySelection(casters);
return _directionalShadowDraws;
}
catch

View file

@ -91,6 +91,50 @@ internal readonly record struct WorldRenderFrame(
/// directional-shadow prepass.
/// </summary>
public AuthoredCelestialShadowSource CelestialShadowSource { get; init; }
/// <summary>
/// Pack-on-only borrow of the prior successfully completed retail
/// landscape visibility transaction. It deliberately trails the camera
/// resolved by this frame.
/// </summary>
public RetailLandscapeVisibilityFrame PriorLandscapeVisibility { get; init; }
/// <summary>Pack-on-only read seam over S2's exact retained CELLARRAY
/// owner. The frame never copies or retains registry rows.</summary>
public IDirectionalShadowCellMembership? DirectionalShadowCellMembership
{ get; init; }
}
internal interface IDirectionalShadowCellMembership
{
bool TryGetRetailCellArray(uint entityId, out IReadOnlyList<uint> cells);
}
internal sealed class EmptyDirectionalShadowCellMembership
: IDirectionalShadowCellMembership
{
internal static EmptyDirectionalShadowCellMembership Instance { get; } = new();
public bool TryGetRetailCellArray(
uint entityId,
out IReadOnlyList<uint> cells)
{
_ = entityId;
cells = Array.Empty<uint>();
return false;
}
}
internal sealed class RuntimeDirectionalShadowCellMembership(
ShadowObjectRegistry source) : IDirectionalShadowCellMembership
{
private readonly ShadowObjectRegistry _source = source
?? throw new ArgumentNullException(nameof(source));
public bool TryGetRetailCellArray(
uint entityId,
out IReadOnlyList<uint> cells) =>
_source.TryGetRetailCellArray(entityId, out cells);
}
internal interface IWorldRenderFrameBuilder
@ -114,6 +158,8 @@ internal interface IWorldFrameRootSource
internal interface IWorldFrameVisibilityPreparation
{
RetailLandscapeVisibilityFrame CaptureCompletedLandscapeVisibility();
void Begin(in WorldCameraFrame camera, bool waitingForLogin);
void PublishViewProjection(in WorldCameraFrame camera);
@ -160,6 +206,7 @@ internal sealed class WorldRenderFrameBuilder : IWorldRenderFrameBuilder
private readonly IWorldFrameEnvironmentPreparation _environment;
private readonly IWorldFrameAnimatedEntitySource _animated;
private readonly IWorldFrameBuildingSource _buildings;
private readonly IDirectionalShadowCellMembership _directionalShadowCells;
public WorldRenderFrameBuilder(
IWorldFrameCameraSource camera,
@ -168,7 +215,8 @@ internal sealed class WorldRenderFrameBuilder : IWorldRenderFrameBuilder
IWorldFrameRootSource roots,
IWorldFrameEnvironmentPreparation environment,
IWorldFrameAnimatedEntitySource animated,
IWorldFrameBuildingSource buildings)
IWorldFrameBuildingSource buildings,
IDirectionalShadowCellMembership directionalShadowCells)
{
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
_visibility = visibility ?? throw new ArgumentNullException(nameof(visibility));
@ -177,6 +225,8 @@ internal sealed class WorldRenderFrameBuilder : IWorldRenderFrameBuilder
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
_animated = animated ?? throw new ArgumentNullException(nameof(animated));
_buildings = buildings ?? throw new ArgumentNullException(nameof(buildings));
_directionalShadowCells = directionalShadowCells
?? throw new ArgumentNullException(nameof(directionalShadowCells));
}
public WorldRenderFrame Build(
@ -184,6 +234,10 @@ internal sealed class WorldRenderFrameBuilder : IWorldRenderFrameBuilder
bool waitingForLogin,
DayGroupData? activeDayGroup)
{
RetailLandscapeVisibilityFrame priorLandscapeVisibility =
_visibility.CaptureCompletedLandscapeVisibility();
if (waitingForLogin)
priorLandscapeVisibility = RetailLandscapeVisibilityFrame.None;
WorldCameraFrame camera = _camera.Resolve();
_visibility.Begin(in camera, waitingForLogin);
_settings.Apply(in camera);
@ -196,7 +250,11 @@ internal sealed class WorldRenderFrameBuilder : IWorldRenderFrameBuilder
roots.ViewerRoot,
roots.ViewerCellId,
in frustum);
return new WorldRenderFrame(camera, roots, buildings, animated);
return new WorldRenderFrame(camera, roots, buildings, animated)
{
PriorLandscapeVisibility = priorLandscapeVisibility,
DirectionalShadowCellMembership = _directionalShadowCells,
};
}
}
@ -350,6 +408,9 @@ internal sealed class RuntimeWorldFrameVisibilityPreparation
_environmentFrustum = environmentFrustum;
}
public RetailLandscapeVisibilityFrame CaptureCompletedLandscapeVisibility() =>
_particles.CaptureCompletedLandscapeVisibility();
public void Begin(in WorldCameraFrame camera, bool waitingForLogin)
{
_selection?.SetViewFrustum(camera.Frustum);