feat(render): implement Campaign AR and terrain fidelity

This commit is contained in:
Erik 2026-08-22 13:13:29 +02:00
parent 99cf26e00c
commit 7a5f96ede5
368 changed files with 50611 additions and 950 deletions

View file

@ -0,0 +1,951 @@
using System.Diagnostics;
using System.Numerics;
using System.Runtime.InteropServices;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Packs;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Wb;
using DatReaderWriter.Enums;
namespace AcDream.App.Rendering;
internal readonly record struct DirectionalSunShadowRenderInput(
DirectionalShadowEnvironmentInput Environment,
Matrix4x4 CameraView,
Matrix4x4 CameraProjection,
DirectionalShadowCasterFrame Casters,
float CameraNearMeters = 0.1f,
float CasterDepthPaddingMeters = 48f,
float ResidentMaximumReachMeters = float.PositiveInfinity,
bool MeasureGpuTimers = true,
bool MeasureCpuStages = false);
internal readonly record struct DirectionalSunShadowCpuStageTicks(
long EnvironmentGateTicks,
long PreparedDrawsAndTransformsTicks,
long FitAndUniformTicks,
long LayeredPassRecordingTicks,
long BookkeepingTicks);
internal readonly record struct DirectionalShadowTransformChurnDiagnostics(
int CopiedSceneChanges,
int UpdateTransformChanges,
int UpdateAppearanceChanges,
int DynamicSynchronizationChanges,
int ActiveAnimatedStaticChanges,
int LiveDynamicRootChanges,
int EquippedChildChanges,
int DedupedCasterSlots,
bool SceneJournalFullRefresh,
bool DensityBulkRefresh,
int BatchedProjectionCopyCalls,
int ChangedMatrixSlots,
int FlightCurrentChangedMatrices,
int FlightPendingReplayMatrices,
int FlightUploadedMatrices,
int FlightUploadRanges,
long FlightBytesWritten,
bool FlightFullDynamicFallback,
bool DenseDirectUpload,
bool DenseFlightReplay,
DirectionalShadowCasterClassDiagnostics CasterClasses = default);
internal readonly record struct DirectionalSunShadowDiagnostics(
DirectionalShadowGateReason GateReason,
float Strength,
int CascadeCount,
int DrawCalls,
int WorldOpaqueCommands,
int WorldAlphaCutoutCommands,
int TerrainCommands,
ulong WorldPreparationSequence,
ulong TerrainPreparationSequence,
double CpuMilliseconds,
double LastResolvedGpuMilliseconds,
bool HasResolvedGpuMeasurement,
long ResidentDepthBytes,
DirectionalSunShadowCpuStageTicks CpuStages = default,
DirectionalShadowTransformChurnDiagnostics TransformChurn = default,
AuthoredCelestialShadowSourceKind SourceKind =
AuthoredCelestialShadowSourceKind.None,
int SourceObjectIndex = -1,
uint SourceGfxObjId = 0u,
Vector3 SurfaceToLightDirection = default,
float LightElevationSin = 0f);
internal static class DirectionalShadowBatchFlags
{
internal const uint AlphaCutout = 1u << 0;
internal static uint Encode(DirectionalShadowCasterMaterial material) =>
material is DirectionalShadowCasterMaterial.AlphaCutout
? AlphaCutout
: 0u;
}
/// <summary>
/// Tier-2 producer only: fits selected celestial-light cascades and records
/// their depth maps.
/// It does not alter the retail world pass or sample shadows in receivers.
/// </summary>
internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverSource, IDisposable
{
internal const string TimerPrefix = "directional-shadow-cascade-";
internal const string MultiviewTimerName = "directional-shadow-multiview";
internal const uint LowMultiviewMask = 0b11;
private const int DrawCommandStride = 20;
private readonly IGpuDevice _device;
private readonly DirectionalShadowQuality _quality;
private readonly DirectionalShadowAtmospherePolicy _atmospherePolicy;
private readonly DirectionalShadowPipelineShaders _pipelineShaders;
private readonly bool _multiviewCascades;
private readonly IGpuDirectionalDepthTarget _target;
private readonly IGpuSampler _sampler;
private readonly GpuTextureSlot _textureSlot;
private readonly IGpuPipeline _terrainPipeline;
private readonly IGpuPipeline _worldOpaquePipeline;
private readonly IGpuPipeline _worldCutoutPipeline;
private readonly IGpuPipeline? _terrainMultiviewPipeline;
private readonly IGpuPipeline? _worldOpaqueMultiviewPipeline;
private readonly IGpuPipeline? _worldCutoutMultiviewPipeline;
private readonly DirectionalShadowTransformBufferSet _transformBuffers;
private readonly DirectionalShadowCascade[] _cascades = new DirectionalShadowCascade[4];
private DirectionalShadowBatchGpuData[] _batchScratch = [];
private IGpuBuffer? _worldBatchBuffer;
private IGpuBuffer? _worldCommandBuffer;
private IGpuBuffer? _terrainCommandBuffer;
private ulong _worldGpuBuildSequence;
private ulong _terrainGpuBuildSequence;
private DirectionalShadowFrameBinding _currentFrameBinding;
private bool _disposed;
internal DirectionalSunShadowRenderer(
IGpuDevice device,
DirectionalShadowPreset preset,
DirectionalShadowAtmospherePolicy? atmospherePolicy = null,
DirectionalShadowPipelineShaders? pipelineShaders = null,
bool multiviewCascades = false)
: this(
device,
DirectionalShadowQuality.For(preset),
atmospherePolicy,
pipelineShaders,
multiviewCascades)
{
}
internal DirectionalSunShadowRenderer(
IGpuDevice device,
DirectionalShadowQuality quality,
DirectionalShadowAtmospherePolicy? atmospherePolicy = null,
DirectionalShadowPipelineShaders? pipelineShaders = null,
bool multiviewCascades = false)
{
_device = device ?? throw new ArgumentNullException(nameof(device));
if (quality.CascadeCount is < 1 or > 4
|| quality.MapResolution <= 0
|| !float.IsFinite(quality.MaximumReachMeters)
|| quality.MaximumReachMeters <= 0f
|| quality.PcfRadiusTexels is < 0 or > 2)
{
throw new ArgumentOutOfRangeException(
nameof(quality),
"Directional-shadow quality must declare 1..4 cascades, a positive "
+ "resolution/reach, and a 0..2 PCF radius.");
}
_quality = quality;
_atmospherePolicy = atmospherePolicy ?? DirectionalShadowAtmospherePolicy.BuiltIn;
_pipelineShaders = pipelineShaders ?? DirectionalShadowPipelineShaders.Local;
_multiviewCascades = multiviewCascades;
if (multiviewCascades && quality.CascadeCount != 2)
throw new NotSupportedException("The multiview shadow hint requires exactly two Low cascades.");
if (multiviewCascades && !device.Capabilities.SupportsMultiview)
throw new NotSupportedException("The selected device does not support multiview shadow cascades.");
if (multiviewCascades && _pipelineShaders.MultiviewCasters is null)
throw new NotSupportedException("The pack did not declare all multiview shadow caster variants.");
IGpuDirectionalDepthTarget? target = null;
IGpuSampler? sampler = null;
GpuTextureSlot textureSlot = GpuTextureSlot.Unassigned;
IGpuPipeline? terrain = null;
IGpuPipeline? opaque = null;
IGpuPipeline? cutout = null;
IGpuPipeline? terrainMultiview = null;
IGpuPipeline? opaqueMultiview = null;
IGpuPipeline? cutoutMultiview = null;
DirectionalShadowTransformBufferSet? transformBuffers = null;
try
{
target = device.CreateDirectionalDepthTarget(
new GpuDirectionalDepthTargetDescription(
$"directional-shadow-{quality.Preset.ToString().ToLowerInvariant()}",
_quality.MapResolution,
_quality.CascadeCount));
sampler = device.CreateSampler(GpuSamplerDescription.ShadowNearestClamp);
textureSlot = device.RegisterTexture(target.DepthTexture, sampler);
terrain = CreatePipeline(
device,
"directional-shadow-terrain",
_pipelineShaders.TerrainCaster,
TerrainModernRenderer.TerrainVertexLayout,
GpuFrontFace.CounterClockwise);
opaque = CreatePipeline(
device,
"directional-shadow-world-opaque",
_pipelineShaders.WorldOpaqueCaster,
GpuVertexLayout.WorldMesh,
GpuFrontFace.Clockwise);
cutout = CreatePipeline(
device,
"directional-shadow-world-cutout",
_pipelineShaders.WorldAlphaCutoutCaster,
GpuVertexLayout.WorldMesh,
GpuFrontFace.Clockwise);
if (multiviewCascades)
{
DirectionalShadowMultiviewPipelineShaders shaders =
_pipelineShaders.MultiviewCasters!.Value;
terrainMultiview = CreatePipeline(device, "directional-shadow-terrain-multiview",
shaders.TerrainCaster, TerrainModernRenderer.TerrainVertexLayout,
GpuFrontFace.CounterClockwise, LowMultiviewMask);
opaqueMultiview = CreatePipeline(device, "directional-shadow-world-opaque-multiview",
shaders.WorldOpaqueCaster, GpuVertexLayout.WorldMesh,
GpuFrontFace.Clockwise, LowMultiviewMask);
cutoutMultiview = CreatePipeline(device, "directional-shadow-world-cutout-multiview",
shaders.WorldAlphaCutoutCaster, GpuVertexLayout.WorldMesh,
GpuFrontFace.Clockwise, LowMultiviewMask);
}
transformBuffers = new DirectionalShadowTransformBufferSet(device);
}
catch
{
transformBuffers?.Dispose();
cutoutMultiview?.Dispose();
opaqueMultiview?.Dispose();
terrainMultiview?.Dispose();
cutout?.Dispose();
opaque?.Dispose();
terrain?.Dispose();
if (textureSlot.IsAssigned)
device.ReleaseTextureSlot(textureSlot);
sampler?.Dispose();
target?.Dispose();
throw;
}
_target = target;
_sampler = sampler;
_textureSlot = textureSlot;
_terrainPipeline = terrain;
_worldOpaquePipeline = opaque;
_worldCutoutPipeline = cutout;
_terrainMultiviewPipeline = terrainMultiview;
_worldOpaqueMultiviewPipeline = opaqueMultiview;
_worldCutoutMultiviewPipeline = cutoutMultiview;
_transformBuffers = transformBuffers;
}
internal DirectionalShadowQuality Quality => _quality;
internal bool MultiviewCascadesEnabled => _multiviewCascades;
internal static string TimerName(int cascadeIndex) => cascadeIndex switch
{
0 => "directional-shadow-cascade-0",
1 => "directional-shadow-cascade-1",
2 => "directional-shadow-cascade-2",
3 => "directional-shadow-cascade-3",
_ => throw new ArgumentOutOfRangeException(nameof(cascadeIndex)),
};
internal IGpuTexture DepthTexture => _target.DepthTexture;
internal GpuTextureSlot TextureSlot => _textureSlot;
public DirectionalShadowPipelineShaders PipelineShaders => _pipelineShaders;
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.
/// </summary>
internal long RetainedCommandBufferBytes => checked(
(_worldBatchBuffer?.SizeBytes ?? 0L)
+ (_worldCommandBuffer?.SizeBytes ?? 0L)
+ (_terrainCommandBuffer?.SizeBytes ?? 0L));
internal int RetainedCommandBufferCount =>
(_worldBatchBuffer is null ? 0 : 1)
+ (_worldCommandBuffer is null ? 0 : 1)
+ (_terrainCommandBuffer is null ? 0 : 1);
internal long RetainedGpuBufferBytes => checked(
RetainedCommandBufferBytes + _transformBuffers.RetainedGpuBytes);
internal int RetainedGpuBufferCount => checked(
RetainedCommandBufferCount + _transformBuffers.BufferCount);
public bool TryGetCurrentFrameBinding(
IGpuFrame frame,
out DirectionalShadowFrameBinding binding)
{
ArgumentNullException.ThrowIfNull(frame);
binding = _currentFrameBinding;
return !_disposed && binding.IsValidFor(frame);
}
internal DirectionalSunShadowDiagnostics Render(
IGpuFrame frame,
in DirectionalSunShadowRenderInput input,
WbDrawDispatcher world,
TerrainModernRenderer terrain)
{
ObjectDisposedException.ThrowIf(_disposed, this);
ArgumentNullException.ThrowIfNull(frame);
_currentFrameBinding = DirectionalShadowFrameBinding.Disabled;
ArgumentNullException.ThrowIfNull(world);
ArgumentNullException.ThrowIfNull(terrain);
long cpuStageStarted = input.MeasureCpuStages ? Stopwatch.GetTimestamp() : 0L;
DirectionalShadowEnvironmentState environment =
DirectionalShadowEnvironmentGate.Evaluate(
input.Environment,
_atmospherePolicy);
long environmentGateTicks = input.MeasureCpuStages
? Stopwatch.GetTimestamp() - cpuStageStarted
: 0L;
if (!environment.ShouldRender)
return Disabled(
in environment,
new DirectionalSunShadowCpuStageTicks(
environmentGateTicks, 0L, 0L, 0L, 0L));
if (input.ResidentMaximumReachMeters <= input.CameraNearMeters)
{
environment = environment with
{
Reason = DirectionalShadowGateReason.ResidentWindowUnavailable,
};
return Disabled(
in environment,
new DirectionalSunShadowCpuStageTicks(
environmentGateTicks, 0L, 0L, 0L, 0L));
}
cpuStageStarted = input.MeasureCpuStages ? Stopwatch.GetTimestamp() : 0L;
DirectionalShadowPreparedDraws worldDraws =
world.PrepareDirectionalShadowDraws(input.Casters);
DirectionalShadowTerrainPreparedDraws terrainDraws =
terrain.PrepareDirectionalShadowDraws();
DirectionalShadowMeshGeometry? worldGeometry =
worldDraws.Commands.IsEmpty ? null : world.GetDirectionalShadowGeometry();
DirectionalShadowTerrainGeometry? terrainGeometry =
terrainDraws.Commands.IsEmpty ? null : terrain.GetDirectionalShadowGeometry();
uint transformBindingSizeBytes =
world.ResolveDirectionalShadowTransformBindingSize(
worldDraws.Transforms.Length,
// Stats counts every current WB source render batch before
// transparent/cutout shadow rejection. Ordinary WB submission
// publishes at most one matrix per source batch, making this a
// complete-frame upper bound available before shadow commands
// bind the one authoritative pose buffer.
worldDraws.Stats.SourceBatches);
WorldTransformFrameSlice retainedTransforms = _transformBuffers.Publish(
frame,
worldDraws.BuildSequence,
worldDraws.Transforms,
worldDraws.DynamicTransformSlots,
worldDraws.AllDynamicTransformSlots,
worldDraws.LastDynamicTransformRefreshWasDense,
transformBindingSizeBytes);
WorldTransformFrameSlice transforms =
world.BeginDirectionalShadowTransformFrame(
frame,
in retainedTransforms);
DirectionalShadowCasterBuildStats casterStats = input.Casters.Stats;
DirectionalShadowCasterClassDiagnostics casterClasses =
CompleteCasterClassDiagnostics(
in casterStats,
terrainDraws.Commands.Length);
DirectionalShadowTransformPublishStats publishStats =
_transformBuffers.LastStats;
var transformChurn = new DirectionalShadowTransformChurnDiagnostics(
casterStats.CopiedTransformChanges,
casterStats.UpdateTransformChanges,
casterStats.UpdateAppearanceChanges,
casterStats.DynamicSynchronizationChanges,
casterStats.ActiveAnimatedStaticChanges,
casterStats.LiveDynamicRootChanges,
casterStats.EquippedChildChanges,
casterStats.DedupedChangedCasterSlots,
casterStats.TransformJournalFullRefresh,
casterStats.DensityBulkRefresh,
casterStats.BatchedProjectionCopyCalls,
worldDraws.LastDynamicTransformRefreshCount,
publishStats.CurrentChangedMatrices,
publishStats.PendingReplayMatrices,
publishStats.DynamicMatricesUpdated,
publishStats.DynamicRangesUpdated,
publishStats.BytesWritten,
publishStats.UsedFullDynamicFallback,
publishStats.DenseDirectUpload,
publishStats.DenseFlightReplay,
casterClasses);
long preparedDrawsAndTransformsTicks = input.MeasureCpuStages
? Stopwatch.GetTimestamp() - cpuStageStarted
: 0L;
try
{
return RenderPrepared(
frame,
environment,
input.CameraView,
input.CameraProjection,
input.CameraNearMeters,
input.CasterDepthPaddingMeters,
worldDraws,
terrainDraws,
worldGeometry,
terrainGeometry,
transforms,
input.ResidentMaximumReachMeters,
input.MeasureGpuTimers,
input.MeasureCpuStages,
new DirectionalSunShadowCpuStageTicks(
environmentGateTicks,
preparedDrawsAndTransformsTicks,
0L,
0L,
0L),
transformChurn);
}
catch
{
world.CancelDirectionalShadowTransformFrame(frame);
throw;
}
}
internal static DirectionalShadowCasterClassDiagnostics
CompleteCasterClassDiagnostics(
in DirectionalShadowCasterBuildStats casterStats,
int terrainCommandCount)
{
ArgumentOutOfRangeException.ThrowIfNegative(terrainCommandCount);
return casterStats.CasterClasses with
{
TerrainCommands = terrainCommandCount,
};
}
internal DirectionalSunShadowDiagnostics RenderPrepared(
IGpuFrame frame,
in DirectionalShadowEnvironmentState environment,
Matrix4x4 cameraView,
Matrix4x4 cameraProjection,
float cameraNearMeters,
float casterDepthPaddingMeters,
DirectionalShadowPreparedDraws worldDraws,
DirectionalShadowTerrainPreparedDraws terrainDraws,
DirectionalShadowMeshGeometry? worldGeometry,
DirectionalShadowTerrainGeometry? terrainGeometry,
WorldTransformFrameSlice worldTransforms,
float residentMaximumReachMeters = float.PositiveInfinity,
bool measureGpuTimers = true,
bool measureCpuStages = false,
DirectionalSunShadowCpuStageTicks cpuStages = default,
DirectionalShadowTransformChurnDiagnostics transformChurn = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
ArgumentNullException.ThrowIfNull(frame);
_currentFrameBinding = DirectionalShadowFrameBinding.Disabled;
ArgumentNullException.ThrowIfNull(worldDraws);
ArgumentNullException.ThrowIfNull(terrainDraws);
if (!environment.ShouldRender)
return Disabled(in environment, cpuStages);
if (!worldDraws.Commands.IsEmpty && worldGeometry is null)
throw new ArgumentNullException(nameof(worldGeometry));
if (!terrainDraws.Commands.IsEmpty && terrainGeometry is null)
throw new ArgumentNullException(nameof(terrainGeometry));
if (!worldTransforms.IsValidFor(frame))
throw new ArgumentException(
"Shadow transforms must use this frame's shared N.5 allocation.",
nameof(worldTransforms));
long started = Stopwatch.GetTimestamp();
var fit = new DirectionalShadowCascadeFitInput(
cameraView,
cameraProjection,
environment.SurfaceToLightDirection,
_quality,
cameraNearMeters,
PracticalSplitLambda: 0.65f,
casterDepthPaddingMeters,
residentMaximumReachMeters);
int cascadeCount = DirectionalShadowCascadeFitter.Fit(
fit,
_cascades);
if (cascadeCount == 0)
{
DirectionalShadowEnvironmentState unavailable = environment with
{
Reason = DirectionalShadowGateReason.ResidentWindowUnavailable,
};
return Disabled(in unavailable, cpuStages);
}
ReadOnlySpan<DirectionalShadowCascade> cascades =
_cascades.AsSpan(0, cascadeCount);
DirectionalShadowUniforms uniforms = DirectionalShadowUniforms.Create(
cascades,
environment,
_quality,
_textureSlot);
GpuRingAllocation uniformAllocation = frame.AllocateRing(
DirectionalShadowUniforms.SizeInBytes,
GpuRingUsage.Uniform);
MemoryMarshal.Write(uniformAllocation.Data, in uniforms);
long fitAndUniformFinished = measureCpuStages ? Stopwatch.GetTimestamp() : 0L;
PreparedGpuUploads uploads = PrepareGpuData(
worldTransforms,
worldDraws,
terrainDraws);
if (MultiviewCascadesEnabled)
{
using IGpuPassEncoder encoder = frame.BeginPass(
GpuPassDescription.DirectionalDepthMultiview(
"directional-shadow-multiview",
_target,
LowMultiviewMask));
using IDisposable? timer = measureGpuTimers
? encoder.BeginTimerScope(MultiviewTimerName)
: null;
encoder.BindUniformBuffer(
GpuBindingModel.UniformDirectionalShadow,
uniformAllocation.Buffer,
uniformAllocation.OffsetBytes,
DirectionalShadowUniforms.SizeInBytes);
DrawTerrain(encoder, uploads, terrainDraws, terrainGeometry, 0,
_terrainMultiviewPipeline);
DrawWorld(encoder, uploads, worldDraws, worldGeometry, 0,
_worldOpaqueMultiviewPipeline, _worldCutoutMultiviewPipeline);
}
else for (int cascadeIndex = 0; cascadeIndex < cascadeCount; cascadeIndex++)
{
using IGpuPassEncoder encoder = frame.BeginPass(
GpuPassDescription.DirectionalDepth(
$"directional-shadow-{cascadeIndex}",
_target,
cascadeIndex));
using IDisposable? timer = measureGpuTimers
? encoder.BeginTimerScope(TimerName(cascadeIndex))
: null;
encoder.BindUniformBuffer(
GpuBindingModel.UniformDirectionalShadow,
uniformAllocation.Buffer,
uniformAllocation.OffsetBytes,
DirectionalShadowUniforms.SizeInBytes);
DrawTerrain(encoder, uploads, terrainDraws, terrainGeometry, cascadeIndex);
DrawWorld(encoder, uploads, worldDraws, worldGeometry, cascadeIndex);
}
long passRecordingFinished = measureCpuStages ? Stopwatch.GetTimestamp() : 0L;
_currentFrameBinding = new DirectionalShadowFrameBinding(
frame.Serial,
Enabled: true,
uniformAllocation.Buffer,
uniformAllocation.OffsetBytes,
DirectionalShadowUniforms.SizeInBytes,
_textureSlot,
cascadeCount);
(bool hasGpu, double gpuMilliseconds) = ResolveGpu(cascadeCount);
int drawsPerCascade = terrainDraws.Commands.IsEmpty ? 0 : 1;
drawsPerCascade = checked(
drawsPerCascade
+ (worldDraws.Commands.IsEmpty
? 0
: worldDraws.OpaqueRuns.Length + worldDraws.AlphaCutoutRuns.Length));
long finished = Stopwatch.GetTimestamp();
cpuStages = cpuStages with
{
FitAndUniformTicks = measureCpuStages
? fitAndUniformFinished - started
: 0L,
LayeredPassRecordingTicks = measureCpuStages
? passRecordingFinished - fitAndUniformFinished
: 0L,
BookkeepingTicks = measureCpuStages
? finished - passRecordingFinished
: 0L,
};
return new DirectionalSunShadowDiagnostics(
DirectionalShadowGateReason.Enabled,
environment.Strength,
cascadeCount,
checked((MultiviewCascadesEnabled ? 1 : cascadeCount) * drawsPerCascade),
worldDraws.OpaqueCommandCount,
worldDraws.AlphaCutoutCommandCount,
terrainDraws.Commands.Length,
worldDraws.BuildSequence,
terrainDraws.BuildSequence,
(finished - started) * 1000d / Stopwatch.Frequency,
gpuMilliseconds,
hasGpu,
_quality.ApproximateDepthMapBytes,
cpuStages,
transformChurn,
environment.SourceKind,
environment.SourceObjectIndex,
environment.SourceGfxObjId,
environment.SurfaceToLightDirection,
environment.LightElevationSin);
}
private PreparedGpuUploads PrepareGpuData(
in WorldTransformFrameSlice transforms,
DirectionalShadowPreparedDraws world,
DirectionalShadowTerrainPreparedDraws terrain)
{
if (_worldGpuBuildSequence != world.BuildSequence)
RebuildWorldGpuData(world);
if (_terrainGpuBuildSequence != terrain.BuildSequence)
RebuildTerrainGpuData(terrain);
return new PreparedGpuUploads(
transforms,
Slice(_worldBatchBuffer),
Slice(_worldCommandBuffer),
Slice(_terrainCommandBuffer));
}
private void RebuildWorldGpuData(DirectionalShadowPreparedDraws world)
{
IGpuBuffer? batches = null;
IGpuBuffer? commands = null;
try
{
if (!world.Commands.IsEmpty)
{
EnsureBatchCapacity(world.Batches.Length);
for (int i = 0; i < world.Batches.Length; i++)
{
DirectionalShadowPreparedBatch batch = world.Batches[i];
_batchScratch[i] = new DirectionalShadowBatchGpuData(
batch.TextureSlot.Index,
0u,
batch.TextureLayer,
DirectionalShadowBatchFlags.Encode(batch.Material));
}
ReadOnlySpan<byte> batchBytes = MemoryMarshal.AsBytes(
_batchScratch.AsSpan(0, world.Batches.Length));
ReadOnlySpan<byte> commandBytes = MemoryMarshal.AsBytes(
world.Commands);
batches = CreateRetainedBuffer(
$"directional-shadow-world-batches-{world.BuildSequence}",
batchBytes,
GpuBufferUsage.Storage);
commands = CreateRetainedBuffer(
$"directional-shadow-world-commands-{world.BuildSequence}",
commandBytes,
GpuBufferUsage.Indirect);
}
}
catch
{
commands?.Dispose();
batches?.Dispose();
throw;
}
IGpuBuffer? previousBatches = _worldBatchBuffer;
IGpuBuffer? previousCommands = _worldCommandBuffer;
_worldBatchBuffer = batches;
_worldCommandBuffer = commands;
_worldGpuBuildSequence = world.BuildSequence;
previousCommands?.Dispose();
previousBatches?.Dispose();
}
private void RebuildTerrainGpuData(DirectionalShadowTerrainPreparedDraws terrain)
{
IGpuBuffer? commands = null;
if (!terrain.Commands.IsEmpty)
{
commands = CreateRetainedBuffer(
$"directional-shadow-terrain-commands-{terrain.BuildSequence}",
MemoryMarshal.AsBytes(terrain.Commands),
GpuBufferUsage.Indirect);
}
IGpuBuffer? previous = _terrainCommandBuffer;
_terrainCommandBuffer = commands;
_terrainGpuBuildSequence = terrain.BuildSequence;
previous?.Dispose();
}
private IGpuBuffer CreateRetainedBuffer(
string name,
ReadOnlySpan<byte> contents,
GpuBufferUsage usage)
{
if (contents.IsEmpty)
throw new ArgumentException("Retained shadow buffers cannot be empty.", nameof(contents));
IGpuBuffer buffer = _device.CreateBuffer(new GpuBufferDescription(
name,
contents.Length,
usage | GpuBufferUsage.TransferDestination,
GpuMemoryResidency.DeviceLocal));
try
{
buffer.Upload(0, contents);
return buffer;
}
catch
{
buffer.Dispose();
throw;
}
}
private static RetainedGpuBufferSlice Slice(IGpuBuffer? buffer) =>
new(buffer, 0u, checked((uint)(buffer?.SizeBytes ?? 0L)));
private void DrawTerrain(
IGpuPassEncoder encoder,
in PreparedGpuUploads uploads,
DirectionalShadowTerrainPreparedDraws draws,
DirectionalShadowTerrainGeometry? geometry,
int cascadeIndex,
IGpuPipeline? pipeline = null)
{
if (draws.Commands.IsEmpty)
return;
DirectionalShadowTerrainGeometry actual = geometry!.Value;
encoder.BindPipeline(pipeline ?? _terrainPipeline);
encoder.BindVertexBuffer(0, actual.VertexBuffer, 0);
encoder.BindIndexBuffer(actual.IndexBuffer, 0, GpuIndexType.UInt32);
GpuPushConstants push = PushForCascade(cascadeIndex, 0);
encoder.SetPushConstants(in push);
encoder.MultiDrawIndexedIndirect(
uploads.TerrainCommands.RequireBuffer(),
uploads.TerrainCommands.OffsetBytes,
checked((uint)draws.Commands.Length),
DrawCommandStride);
}
private void DrawWorld(
IGpuPassEncoder encoder,
in PreparedGpuUploads uploads,
DirectionalShadowPreparedDraws draws,
DirectionalShadowMeshGeometry? geometry,
int cascadeIndex,
IGpuPipeline? opaquePipeline = null,
IGpuPipeline? cutoutPipeline = null)
{
if (draws.Commands.IsEmpty)
return;
DirectionalShadowMeshGeometry actual = geometry!.Value;
encoder.BindStorageBuffer(
GpuBindingModel.StorageInstances,
uploads.Transforms.Buffer,
uploads.Transforms.BaseOffsetBytes,
uploads.Transforms.BindingSizeBytes);
encoder.BindStorageBuffer(
GpuBindingModel.StorageBatches,
uploads.Batches.RequireBuffer(),
uploads.Batches.OffsetBytes,
uploads.Batches.SizeBytes);
DrawWorldRange(
encoder,
uploads.WorldCommands,
draws.OpaqueRuns,
cascadeIndex,
opaquePipeline ?? _worldOpaquePipeline,
actual);
DrawWorldRange(
encoder,
uploads.WorldCommands,
draws.AlphaCutoutRuns,
cascadeIndex,
cutoutPipeline ?? _worldCutoutPipeline,
actual);
}
private static void DrawWorldRange(
IGpuPassEncoder encoder,
in RetainedGpuBufferSlice commands,
ReadOnlySpan<DirectionalShadowPreparedRun> runs,
int cascadeIndex,
IGpuPipeline pipeline,
in DirectionalShadowMeshGeometry geometry)
{
if (runs.IsEmpty)
return;
encoder.BindPipeline(pipeline);
encoder.BindVertexBuffer(0, geometry.VertexBuffer, 0);
encoder.BindIndexBuffer(geometry.IndexBuffer, 0, GpuIndexType.UInt16);
for (int runIndex = 0; runIndex < runs.Length; runIndex++)
{
DirectionalShadowPreparedRun run = runs[runIndex];
ApplyCull(encoder, run.CullMode);
GpuPushConstants push = PushForCascade(cascadeIndex, run.StartCommand);
encoder.SetPushConstants(in push);
encoder.MultiDrawIndexedIndirect(
commands.RequireBuffer(),
commands.OffsetBytes + checked((uint)(run.StartCommand * DrawCommandStride)),
checked((uint)run.CommandCount),
DrawCommandStride);
}
}
private static GpuPushConstants PushForCascade(int cascadeIndex, int drawIdOffset)
{
GpuPushConstants push = GpuPushConstants.Default;
push.RenderPass = cascadeIndex;
push.DrawIdOffset = drawIdOffset;
return push;
}
private static void ApplyCull(IGpuPassEncoder encoder, CullMode mode)
{
encoder.SetFrontFace(GpuFrontFace.Clockwise);
encoder.SetCullMode(mode switch
{
CullMode.None => GpuCullMode.None,
CullMode.Clockwise => GpuCullMode.Front,
_ => GpuCullMode.Back,
});
}
private static IGpuPipeline CreatePipeline(
IGpuDevice device,
string name,
GpuShaderSet shaders,
GpuVertexLayout layout,
GpuFrontFace frontFace,
uint viewMask = 0) =>
device.CreatePipeline(new GpuPipelineDescription
{
Name = name,
Shaders = shaders,
VertexLayout = layout,
Topology = GpuPrimitiveTopology.TriangleList,
Blend = GpuBlendMode.None,
Depth = new GpuDepthState(true, true, GpuCompareOp.Less),
Cull = GpuCullMode.Back,
FrontFace = frontFace,
AlphaToCoverage = false,
ColorWrite = false,
HasColorAttachment = false,
AllowColorFormatVariants = false,
SampleCount = 1,
UsesRenderPackShaderAbi = true,
ViewMask = viewMask,
});
private (bool HasMeasurement, double Milliseconds) ResolveGpu(int cascadeCount)
{
if (MultiviewCascadesEnabled)
return _device.Timers.TryResolve(MultiviewTimerName, out double measured)
? (true, measured)
: (false, 0d);
double total = 0d;
for (int i = 0; i < cascadeCount; i++)
{
if (!_device.Timers.TryResolve(TimerName(i), out double milliseconds))
return (false, 0d);
total += milliseconds;
}
return (true, total);
}
private DirectionalSunShadowDiagnostics Disabled(
in DirectionalShadowEnvironmentState environment,
DirectionalSunShadowCpuStageTicks cpuStages = default) =>
new(
environment.Reason,
0f,
0,
0,
0,
0,
0,
0,
0,
0d,
0d,
false,
_quality.ApproximateDepthMapBytes,
cpuStages,
SourceKind: environment.SourceKind,
SourceObjectIndex: environment.SourceObjectIndex,
SourceGfxObjId: environment.SourceGfxObjId,
SurfaceToLightDirection: environment.SurfaceToLightDirection,
LightElevationSin: environment.LightElevationSin);
private void EnsureBatchCapacity(int required)
{
if (_batchScratch.Length >= required)
return;
int capacity = _batchScratch.Length == 0 ? 16 : _batchScratch.Length;
while (capacity < required)
capacity = checked(capacity * 2);
Array.Resize(ref _batchScratch, capacity);
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_currentFrameBinding = DirectionalShadowFrameBinding.Disabled;
_worldCutoutPipeline.Dispose();
_worldCutoutMultiviewPipeline?.Dispose();
_worldOpaqueMultiviewPipeline?.Dispose();
_terrainMultiviewPipeline?.Dispose();
_worldOpaquePipeline.Dispose();
_terrainPipeline.Dispose();
_terrainCommandBuffer?.Dispose();
_worldCommandBuffer?.Dispose();
_worldBatchBuffer?.Dispose();
_transformBuffers.Dispose();
_device.ReleaseTextureSlot(_textureSlot);
_sampler.Dispose();
_target.Dispose();
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
private readonly record struct DirectionalShadowBatchGpuData(
uint TextureIndex,
uint Reserved,
uint TextureLayer,
uint Flags);
private readonly record struct RetainedGpuBufferSlice(
IGpuBuffer? Buffer,
uint OffsetBytes,
uint SizeBytes)
{
internal IGpuBuffer RequireBuffer() => Buffer
?? throw new InvalidOperationException(
"A non-empty directional-shadow draw has no retained GPU buffer.");
}
private readonly record struct PreparedGpuUploads(
WorldTransformFrameSlice transforms,
RetainedGpuBufferSlice batches,
RetainedGpuBufferSlice worldCommands,
RetainedGpuBufferSlice terrainCommands)
{
internal WorldTransformFrameSlice Transforms { get; } = transforms;
internal RetainedGpuBufferSlice Batches { get; } = batches;
internal RetainedGpuBufferSlice WorldCommands { get; } = worldCommands;
internal RetainedGpuBufferSlice TerrainCommands { get; } = terrainCommands;
}
}