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; /// /// Campaign VM VM6: the caster's own set 3/binding 5 AtmosphericFrame slice /// (192 bytes, ABI v2 — see RenderPackShaderAbi.AtmosphericFrameSizeBytes), /// owned and allocated by the graph exactly like the world receiver's frame /// block, so the caster can read the identical foliage-wind clock/amplitude /// inputs through the shared foliage_wind.glsl include. Unbound (the /// default) when the caller has no atmospheric-frame data to offer — the /// declared-pack graph does not build one, and the renderer simply skips /// binding 5 for that caller rather than manufacturing a buffer nobody asked /// for. /// internal readonly record struct AtmosphericFrameBufferBinding( IGpuBuffer? Buffer, uint OffsetBytes, uint SizeBytes) { internal bool IsBound => Buffer is not null; } 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, AtmosphericFrameBufferBinding AtmosphericFrame = default); 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; } /// /// 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. /// 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; /// /// 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. /// 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, input.AtmosphericFrame); } catch { world.CancelDirectionalShadowTransformFrame(frame); throw; } } internal static DirectionalShadowCasterClassDiagnostics CompleteCasterClassDiagnostics( in DirectionalShadowCasterBuildStats casterStats, int terrainCommandCount) { ArgumentOutOfRangeException.ThrowIfNegative(terrainCommandCount); return casterStats.CasterClasses with { TerrainCommands = terrainCommandCount, }; } /// /// The fitted frustum slice bounds receivers, not the casters that project /// onto them. At low celestial elevations a modest tree can be many metres /// farther along the light ray than its visible ground shadow. Padding by /// less than the effective receiver reach therefore clips a caster as the /// camera-relative slice turns, producing partial or disappearing shadows. /// This expands depth only; cascade XY density, draw count, and caster /// membership are unchanged. /// internal static float ResolveCasterDepthPaddingMeters( float configuredPaddingMeters, float qualityReachMeters, float residentMaximumReachMeters) { if (!float.IsFinite(configuredPaddingMeters) || configuredPaddingMeters <= 0f) { throw new ArgumentOutOfRangeException( nameof(configuredPaddingMeters)); } if (!float.IsFinite(qualityReachMeters) || qualityReachMeters <= 0f) throw new ArgumentOutOfRangeException(nameof(qualityReachMeters)); if (float.IsNaN(residentMaximumReachMeters) || residentMaximumReachMeters <= 0f) { throw new ArgumentOutOfRangeException( nameof(residentMaximumReachMeters)); } float effectiveReceiverReach = MathF.Min( qualityReachMeters, residentMaximumReachMeters); return MathF.Max(configuredPaddingMeters, effectiveReceiverReach); } 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, AtmosphericFrameBufferBinding atmosphericFrame = 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(); float effectiveCasterDepthPaddingMeters = ResolveCasterDepthPaddingMeters( casterDepthPaddingMeters, _quality.MaximumReachMeters, residentMaximumReachMeters); var fit = new DirectionalShadowCascadeFitInput( cameraView, cameraProjection, environment.SurfaceToLightDirection, _quality, cameraNearMeters, PracticalSplitLambda: 0.65f, effectiveCasterDepthPaddingMeters, residentMaximumReachMeters); int cascadeCount = DirectionalShadowCascadeFitter.Fit( fit, _cascades); if (cascadeCount == 0) { DirectionalShadowEnvironmentState unavailable = environment with { Reason = DirectionalShadowGateReason.ResidentWindowUnavailable, }; return Disabled(in unavailable, cpuStages); } ReadOnlySpan 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); // Campaign VM VM6: the caster pass reads foliage-wind clock/ // amplitude inputs through the same shared AtmosphericFrame // block the world receiver binds (set 3/binding 5), so a // displaced leaf's shadow moves with it by construction. Bound // only when the caller supplied one — declared (non-built-in) // packs leave this unbound and their caster shaders, which never // declare binding 5, are unaffected. if (atmosphericFrame.IsBound) { encoder.BindUniformBuffer( GpuBindingModel.UniformAtmosphericFrame, atmosphericFrame.Buffer!, atmosphericFrame.OffsetBytes, atmosphericFrame.SizeBytes); } 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); // Campaign VM VM6 — see the matching comment in the multiview // branch above. if (atmosphericFrame.IsBound) { encoder.BindUniformBuffer( GpuBindingModel.UniformAtmosphericFrame, atmosphericFrame.Buffer!, atmosphericFrame.OffsetBytes, atmosphericFrame.SizeBytes); } 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, // Campaign VM VM6: batch.FoliageFlags rides alongside // the existing alpha-cutout bit in the same word the // caster shaders read as BatchData.flags. DirectionalShadowBatchFlags.Encode(batch.Material) | batch.FoliageFlags); } ReadOnlySpan batchBytes = MemoryMarshal.AsBytes( _batchScratch.AsSpan(0, world.Batches.Length)); ReadOnlySpan 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 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 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; } }