using System.Numerics; using System.Runtime.InteropServices; using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Scene; using AcDream.App.Rendering.Wb; using AcDream.Core.World; using AcDream.Plugin.Abstractions.Rendering; namespace AcDream.App.Rendering.Packs; /// /// API-v1 executor for declaration-only fullscreen graphs. It supports the /// portable Tier-1 hooks/resources without recognizing a pack id or shader /// filename. Scene replay and renderer-pipeline variants remain separate host /// facilities and are rejected by the factory before this runtime is built. /// internal class DeclaredFullscreenRenderPackGraph : IAtmosphericWorldGraphRuntime, IRenderPackRuntimePerformanceSource, IRenderPackRuntimeDiagnosticsSource { private readonly IGpuDevice _device; private readonly IDisposable _hdrLease; private readonly IGpuSampler _sampler; private readonly Node[] _nodes; private readonly IReadOnlyDictionary _resources; private readonly PackSettingsUniforms _settings; private readonly DirectionalSunShadowRenderer? _directionalShadows; private readonly DirectionalShadowCasterFrame _shadowCasters = new(); private readonly RenderPassDeclaration? _shadowPass; private readonly float _shadowStrength; private TargetSet? _targets; private RenderPackResourceBudget _resourceBudget; private long _resourceGeneration; private long _residentGpuBudgetBytes; private AtmosphericFrameInputs _lastInputs; private DirectionalSunShadowDiagnostics _lastShadowDiagnostics; private int _lastShadowCasterCount; private int _lastShadowClassificationCalls; private WbDrawDispatcher? _lastShadowWorldMeshes; private long _lastShadowFrameSerial = -1; private bool _renderedFrame; private bool _disposed; internal DeclaredFullscreenRenderPackGraph( IGpuDevice device, RenderPackDescriptor descriptor, IRenderPackAssets assets, RenderQualityPreset preset, IReadOnlyDictionary userSettingOverrides) : this( device, descriptor, RenderPackShaderAssets.Validate(descriptor, assets), preset, userSettingOverrides) { } internal DeclaredFullscreenRenderPackGraph( IGpuDevice device, RenderPackDescriptor descriptor, ValidatedRenderPackShaderAssets assets, RenderQualityPreset preset, IReadOnlyDictionary userSettingOverrides) { _device = device ?? throw new ArgumentNullException(nameof(device)); Descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor)); ArgumentNullException.ThrowIfNull(assets); Preset = preset ?? throw new ArgumentNullException(nameof(preset)); ArgumentNullException.ThrowIfNull(userSettingOverrides); if (device is not IGpuPipelineFormatVariantHost variants) throw new NotSupportedException("The active RHI cannot build an HDR world intermediate."); _resources = descriptor.Resources.ToDictionary(value => value.Id, StringComparer.OrdinalIgnoreCase); RenderPassDeclaration[] passes = descriptor.Passes .OrderBy(value => value.Hook) .ToArray(); RenderPassDeclaration[] fullscreenPasses = passes .Where(static value => value.Semantic != RenderPassSemantic.DirectionalShadowDepth) .ToArray(); if (!fullscreenPasses.Any(value => value.Hook == RenderPassHook.ToneMap && value.ResourceWrites.Count == 0)) { throw new NotSupportedException( $"Fullscreen pack '{descriptor.Id}' must declare a ToneMap pass that writes the output surface."); } IDisposable? lease = null; DirectionalSunShadowRenderer? directionalShadows = null; var nodes = new List(fullscreenPasses.Length); try { lease = variants.AcquirePipelineColorFormat(GpuTextureFormat.Rgba16FloatRenderTarget); _sampler = device.CreateSampler(GpuSamplerDescription.WorldClamp); foreach (RenderPassDeclaration pass in fullscreenPasses) { if (pass.Hook is not RenderPassHook.AtmosphereBeforeToneMap and not RenderPassHook.ToneMap) throw new NotSupportedException($"Fullscreen executor does not support hook '{pass.Hook}'."); RenderSemanticInput? unsupported = pass.SemanticInputs.FirstOrDefault(value => value is RenderSemanticInput.SceneNormals or RenderSemanticInput.ShadowCasterTransforms or RenderSemanticInput.DirectionalShadowMaps); if (unsupported is RenderSemanticInput.SceneNormals or RenderSemanticInput.ShadowCasterTransforms or RenderSemanticInput.DirectionalShadowMaps) { throw new NotSupportedException( $"Tier-1 fullscreen pass '{pass.Id}' requires unsupported semantic '{unsupported}'."); } if (pass.ResourceWrites.Count > 1) throw new NotSupportedException($"Pass '{pass.Id}' writes more than one colour target."); GpuTextureFormat format = pass.ResourceWrites.Count == 0 ? GpuTextureFormat.Rgba8UnormRenderTarget : ValidateOutput(Resource(pass.ResourceWrites[0])); var pipeline = device.CreatePipeline(new GpuPipelineDescription { Name = $"render-pack-{descriptor.Id}-{pass.Id}", Shaders = RenderPackShaderAssets.LoadPass(descriptor, assets, pass), VertexLayout = GpuVertexLayout.None, Blend = GpuBlendMode.None, Depth = GpuDepthState.Disabled, Cull = GpuCullMode.None, ColorFormat = format, AllowColorFormatVariants = false, SampleCount = 1, UsesRenderPackShaderAbi = true, }); string timerName = $"render-pack-{descriptor.Id}-{pass.Id}"; nodes.Add(new Node( pass, pipeline, [.. RenderPackTextureBindingResolver.Resolve(pass, _resources)], timerName)); } _nodes = [.. nodes]; _settings = PackSettingsUniforms.Create( descriptor, preset, userSettingOverrides); _shadowPass = passes.SingleOrDefault(static value => value.Semantic == RenderPassSemantic.DirectionalShadowDepth); _shadowStrength = _shadowPass is null ? 0f : ReadSemanticSetting( descriptor, preset, userSettingOverrides, RenderSettingSemantic.DirectionalShadowStrength); if (_shadowPass is not null) { directionalShadows = new DirectionalSunShadowRenderer( device, ResolveShadowQuality( descriptor, preset, userSettingOverrides), RenderPackAtmospherePolicyEvaluation.NeutralDirectionalShadowElevation, LoadDirectionalShadowShaders(descriptor, assets), multiviewCascades: (preset.ExecutionHints & RenderQualityExecutionHints .MultiviewDirectionalShadowCascades) != 0); } _directionalShadows = directionalShadows; directionalShadows = null; _hdrLease = lease; lease = null; } catch { directionalShadows?.Dispose(); for (int i = nodes.Count - 1; i >= 0; i--) nodes[i].Pipeline.Dispose(); lease?.Dispose(); throw; } } public RenderPackDescriptor Descriptor { get; } public RenderQualityPreset Preset { get; } internal IDirectionalShadowReceiverSource DeclaredDirectionalShadowReceivers => _directionalShadows ?? throw new InvalidOperationException( $"Pack '{Descriptor.Id}' has no declared directional-shadow executor."); internal DirectionalSunShadowDiagnostics RenderDeclaredDirectionalShadows( IGpuFrame frame, in RenderFrameFoundation foundation, in WorldRenderFrame world, int activeDayGroup, in RenderSceneQuery scene, WbDrawDispatcher worldMeshes, TerrainModernRenderer terrain) { ObjectDisposedException.ThrowIf(_disposed, this); DirectionalSunShadowRenderer renderer = _directionalShadows ?? throw new InvalidOperationException( $"Pack '{Descriptor.Id}' has no declared directional-shadow executor."); _shadowCasters.Build(in scene); AuthoredCelestialShadowSource source = world.CelestialShadowSource; float elevationStrength = RenderPackAtmospherePolicyEvaluation .DirectionalShadowFromSin( Descriptor.AtmospherePolicy!.DirectionalShadowLightElevationResponse, source.ElevationSin, fallback: 0f); var environment = new DirectionalShadowEnvironmentInput( PackEnabled: true, PortalOrLoginCoverVisible: foundation.PortalViewportVisible, PlayerInsideCell: world.Roots.PlayerInsideCell || world.Roots.CameraInsideCell, source, foundation.Atmosphere, ActiveDayGroupMultiplier: Math.Clamp( EvaluateDayGroupPolicy(activeDayGroup) * elevationStrength * _shadowStrength, 0f, 1f)); var input = new DirectionalSunShadowRenderInput( environment, world.Camera.Camera.View, world.Camera.Projection, _shadowCasters, ResidentMaximumReachMeters: world.ResidentStreamingWindow.MaximumReachMeters); _lastShadowCasterCount = _shadowCasters.Stats.Accepted; _lastShadowClassificationCalls = _shadowCasters.Stats.TopologyRebuilt ? 1 : 0; _lastShadowDiagnostics = renderer.Render( frame, in input, worldMeshes, terrain); _lastShadowWorldMeshes = worldMeshes; _lastShadowFrameSerial = frame.Serial; RequireRetainedGpuBudget(renderer); return _lastShadowDiagnostics; } public IGpuRenderTarget PrepareWorldTarget(int width, int height, int sampleCount) { ObjectDisposedException.ThrowIf(_disposed, this); if (_targets is { } current && current.Width == width && current.Height == height && current.SampleCount == sampleCount) return current.World; RenderPackHostCapabilities capabilities = RenderPackCapabilityResolver.Resolve(_device.Capabilities); RenderPackResourceBudget budget = RenderPackResourceBudgetPlanner.RequireWithinHost( Descriptor, Preset, width, height, sampleCount, capabilities); TargetSet candidate = TargetSet.Create( _device, Descriptor, Preset, _sampler, width, height, sampleCount); TargetSet? prior = _targets; _targets = candidate; _resourceBudget = budget; _residentGpuBudgetBytes = RenderPackResidentBudget.Effective( Preset.MaxResidentGpuBytes, width, height, capabilities.MaxPackResidentBytes); _resourceGeneration = checked(_resourceGeneration + 1); _renderedFrame = false; prior?.Dispose(); return candidate.World; } public void RenderPostProcess(IGpuFrame frame, in AtmosphericFrameInputs inputs) { ObjectDisposedException.ThrowIf(_disposed, this); TargetSet targets = _targets ?? throw new InvalidOperationException("PrepareWorldTarget must run before the fullscreen graph."); if (inputs.ViewportWidth != targets.Width || inputs.ViewportHeight != targets.Height) throw new InvalidOperationException("Fullscreen graph inputs and targets belong to different frames."); float elevationPolicy = EvaluateSunElevationPolicy(inputs.SunElevationDegrees); float dayGroupPolicy = EvaluateDayGroupPolicy(inputs.ActiveDayGroup); IReadOnlyList shadowCurve = Descriptor.AtmospherePolicy?.DirectionalShadowLightElevationResponse ?? []; IReadOnlyList volumetricCurve = Descriptor.AtmospherePolicy?.VolumetricShaftSunElevationResponse ?? []; float shadowElevationPolicy = shadowCurve.Count == 0 ? elevationPolicy : RenderPackAtmospherePolicyEvaluation.DirectionalShadow( shadowCurve, inputs.SunElevationDegrees, elevationPolicy); float volumetricElevationPolicy = volumetricCurve.Count == 0 ? 0f : RenderPackAtmospherePolicyEvaluation.VolumetricShaft( volumetricCurve, inputs.SunElevationDegrees, 0f); float sunPolicy = EvaluateSunPolicy( inputs, elevationPolicy, dayGroupPolicy); var frameValues = new AtmosphericFrameUniforms( new Vector4(inputs.SunScreenUv, sunPolicy, inputs.SunElevationDegrees), new Vector4(inputs.SunColor, sunPolicy), new Vector4(targets.Width, targets.Height, 1f / targets.Width, 1f / targets.Height), new Vector4((float)inputs.Weather, inputs.WeatherIntensity, (float)Math.Clamp(inputs.DeltaSeconds, 0d, 1d), inputs.IsOutdoor ? 1f : 0f), new Vector4(inputs.SunDirection, inputs.SunDirectionalBrightness), new Vector4( inputs.ActiveDayGroup, dayGroupPolicy, shadowElevationPolicy, volumetricElevationPolicy), inputs.InverseViewProjection, // Campaign VM VM6 ABI v2 additions. Foliage wind is a built-in- // pack feature (BuiltInAtmosphericRenderPack.Settings / // AtmosphericPostProcessGraph) — declared (third-party) packs // write zero here and their shaders, compiled before VM6, never // declare binding 5's appended members. Vector4.Zero, Vector4.Zero); GpuRingAllocation frameBlock = frame.AllocateRing(AtmosphericFrameUniforms.SizeInBytes, GpuRingUsage.Uniform); MemoryMarshal.Write(frameBlock.Data, in frameValues); GpuRingAllocation settingsBlock = frame.AllocateRing(PackSettingsUniforms.SizeInBytes, GpuRingUsage.Uniform); PackSettingsUniforms settings = _settings; MemoryMarshal.Write(settingsBlock.Data, in settings); foreach (Node node in _nodes) Draw(frame, node, targets, frameBlock, settingsBlock); _lastInputs = inputs; _renderedFrame = true; } public RenderPackRuntimeDiagnostics CaptureDiagnostics() { ObjectDisposedException.ThrowIf(_disposed, this); TargetSet? targets = _targets; if (!_renderedFrame || targets is null) return RenderPackRuntimeDiagnostics.Empty(Preset.Id); int shadowPassCount = _shadowPass is null ? 0 : 1; var passes = new RenderPackPassDiagnostics[_nodes.Length + shadowPassCount]; int passIndex = 0; if (_shadowPass is not null) { passes[passIndex++] = new RenderPackPassDiagnostics( _shadowPass.Id, _lastShadowDiagnostics.LastResolvedGpuMilliseconds, _lastShadowDiagnostics.DrawCalls, DispatchCalls: 0); } for (int i = 0; i < _nodes.Length; i++) { Node node = _nodes[i]; _device.Timers.TryResolve(node.TimerName, out double milliseconds); passes[passIndex++] = new RenderPackPassDiagnostics( node.Pass.Id, milliseconds, DrawCalls: 1, DispatchCalls: 0); } return new RenderPackRuntimeDiagnostics( Preset.Id, checked( _resourceBudget.RetainedGpuBytes + (_directionalShadows?.RetainedGpuBufferBytes ?? 0L)), _resourceBudget.MultisampleGpuBytes, targets.ImageCount + shadowPassCount, BufferCount: _directionalShadows?.RetainedGpuBufferCount ?? 0, DrawCalls: _nodes.Length + _lastShadowDiagnostics.DrawCalls, DispatchCalls: 0, ShadowCasterCount: _lastShadowCasterCount, CascadeDrawCount: _lastShadowDiagnostics.CascadeCount, CpuClassificationCalls: _lastShadowClassificationCalls, _lastInputs.SunElevationDegrees, _lastInputs.ActiveDayGroup, _lastInputs.Weather.ToString(), _lastInputs.WeatherIntensity, _lastInputs.IsOutdoor, DirectionalShadowStrength: _lastShadowDiagnostics.Strength, passes) { DirectionalShadowSourceKind = _lastShadowDiagnostics.SourceKind, DirectionalShadowSourceObjectIndex = _lastShadowDiagnostics.SourceObjectIndex, DirectionalShadowSourceGfxObjId = _lastShadowDiagnostics.SourceGfxObjId, DirectionalShadowSurfaceToLightDirection = _lastShadowDiagnostics.SurfaceToLightDirection, DirectionalShadowLightElevationSin = _lastShadowDiagnostics.LightElevationSin, ShadowTransformChurn = _lastShadowDiagnostics.TransformChurn, SharedWorldTransformUsedInstances = _lastShadowWorldMeshes is not null && _lastShadowWorldMeshes.HasDirectionalShadowTransformFrame( _lastShadowFrameSerial) ? _lastShadowWorldMeshes .DirectionalShadowTransformFrameUsedInstances : 0u, }; } public RenderPackRuntimePerformanceMetrics CapturePerformanceMetrics() { ObjectDisposedException.ThrowIf(_disposed, this); double gpuMilliseconds = 0d; bool resolved = _targets is not null; for (int i = 0; i < _nodes.Length; i++) { if (!_device.Timers.TryTakeResolved( _nodes[i].TimerName, out double milliseconds)) { resolved = false; } else { gpuMilliseconds += milliseconds; } } if (_directionalShadows is not null) { if (!_device.Timers.TryTakeResolved( RenderPackPerformanceScopeNames.EnhancedWorldReceiver, out double receiverMilliseconds)) { resolved = false; } else { gpuMilliseconds += receiverMilliseconds; } int shadowTimerCount = _directionalShadows.MultiviewCascadesEnabled && _lastShadowDiagnostics.CascadeCount > 0 ? 1 : _lastShadowDiagnostics.CascadeCount; for (int i = 0; i < shadowTimerCount; i++) { if (!_device.Timers.TryTakeResolved( _directionalShadows.MultiviewCascadesEnabled ? DirectionalSunShadowRenderer.MultiviewTimerName : DirectionalSunShadowRenderer.TimerName(i), out double milliseconds)) { resolved = false; } else { gpuMilliseconds += milliseconds; } } } return new RenderPackRuntimePerformanceMetrics( _resourceGeneration, resolved, resolved ? gpuMilliseconds : 0d, checked( _resourceBudget.RetainedGpuBytes + (_directionalShadows?.RetainedGpuBufferBytes ?? 0L)), _resourceBudget.MultisampleGpuBytes); } private void RequireRetainedGpuBudget( DirectionalSunShadowRenderer renderer) { long total = checked( _resourceBudget.RetainedGpuBytes + renderer.RetainedGpuBufferBytes); if (total <= _residentGpuBudgetBytes) return; throw new NotSupportedException( $"Render pack preset '{Preset.Id}' needs {total} resident GPU bytes " + "after materializing its scene-dependent shadow command buffers; " + $"the active pack budget is {_residentGpuBudgetBytes} bytes."); } public void Dispose() { if (_disposed) return; _disposed = true; _targets?.Dispose(); _directionalShadows?.Dispose(); for (int i = _nodes.Length - 1; i >= 0; i--) _nodes[i].Pipeline.Dispose(); _hdrLease.Dispose(); } private void Draw( IGpuFrame frame, Node node, TargetSet targets, GpuRingAllocation frameBlock, GpuRingAllocation settingsBlock) { IGpuRenderTarget? output = node.Pass.ResourceWrites.Count == 0 ? null : targets.Resource(node.Pass.ResourceWrites[0]).Target; using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription { Name = node.TimerName, Color = new GpuColorAttachment(output, GpuLoadOp.Clear, GpuStoreOp.Store, Vector4.Zero), Depth = null, SampleCount = 1, }); using IDisposable timer = encoder.BeginTimerScope(node.TimerName); encoder.BindPipeline(node.Pipeline); encoder.BindUniformBuffer(GpuBindingModel.UniformAtmosphericFrame, frameBlock.Buffer, frameBlock.OffsetBytes, AtmosphericFrameUniforms.SizeInBytes); GpuRingAllocation passBlock = frame.AllocateRing( AtmosphericPackPassUniforms.SizeInBytes, GpuRingUsage.Uniform); var zero = AtmosphericPackPassUniforms.From(Vector4.Zero); MemoryMarshal.Write(passBlock.Data, in zero); encoder.BindUniformBuffer(GpuBindingModel.UniformPackPass, passBlock.Buffer, passBlock.OffsetBytes, AtmosphericPackPassUniforms.SizeInBytes); encoder.BindUniformBuffer(GpuBindingModel.UniformPackSettings, settingsBlock.Buffer, settingsBlock.OffsetBytes, PackSettingsUniforms.SizeInBytes); Span slots = stackalloc GpuTextureSlot[4]; slots.Fill(GpuTextureSlot.Unassigned); for (int i = 0; i < node.Inputs.Length; i++) slots[i] = Resolve(node.Inputs[i], targets); GpuPushConstants push = GpuPushConstants.Default; push.TextureIndexA = slots[0].Index; push.TextureIndexB = slots[1].Index; push.ParamA = BitConverter.UInt32BitsToSingle(slots[2].Index); push.ParamB = BitConverter.UInt32BitsToSingle(slots[3].Index); encoder.SetPushConstants(in push); encoder.Draw(3, 1, 0, 0); } private static GpuTextureSlot Resolve(RenderPackTextureInput input, TargetSet targets) { if (input.Semantic is { } semantic) { return semantic switch { RenderSemanticInput.WorldColor => targets.WorldColor, RenderSemanticInput.SceneDepth => targets.WorldDepth, _ => throw new NotSupportedException($"Texture semantic '{semantic}' is unsupported by Tier-1."), }; } return targets.Resource(input.ResourceId!).Slot; } private float EvaluateSunElevationPolicy(float elevation) { IReadOnlyList? points = Descriptor.AtmospherePolicy?.SunElevationResponse; return RenderPackAtmospherePolicyEvaluation.Ray(points, elevation); } private float EvaluateDayGroupPolicy(int activeDayGroup) { ActiveDayGroupMultiplier? value = Descriptor.AtmospherePolicy? .ActiveDayGroupMultipliers .FirstOrDefault(entry => entry.ActiveDayGroup == activeDayGroup); return value is null ? 1f : (float)value.Multiplier; } private static float EvaluateSunPolicy( in AtmosphericFrameInputs inputs, float elevationPolicy, float dayGroupPolicy) { if (!inputs.IsOutdoor || !inputs.SunIsOnScreen) return 0f; return Math.Clamp( elevationPolicy * dayGroupPolicy * EvaluateWeatherPolicy(inputs.Weather, inputs.WeatherIntensity), 0f, 4f); } private static float EvaluateWeatherPolicy( AcDream.Core.World.WeatherKind weather, float intensity) { float weatherTarget = weather switch { AcDream.Core.World.WeatherKind.Clear => 1f, AcDream.Core.World.WeatherKind.Overcast => 0.18f, AcDream.Core.World.WeatherKind.Rain => 0.10f, AcDream.Core.World.WeatherKind.Snow => 0.16f, AcDream.Core.World.WeatherKind.Storm => 0.06f, _ => 0f, }; return 1f + ((weatherTarget - 1f) * Math.Clamp(intensity, 0f, 1f)); } private static DirectionalShadowPipelineShaders LoadDirectionalShadowShaders( RenderPackDescriptor descriptor, ValidatedRenderPackShaderAssets assets) { DirectionalShadowPipelineShaders shaders = new( Variant(RenderPipelineVariantSemantic.TerrainDirectionalShadowCaster), Variant(RenderPipelineVariantSemantic.WorldOpaqueDirectionalShadowCaster), Variant(RenderPipelineVariantSemantic.WorldAlphaCutoutDirectionalShadowCaster), Variant(RenderPipelineVariantSemantic.TerrainDirectionalShadowReceiver), Variant(RenderPipelineVariantSemantic.WorldDirectionalShadowReceiver)); if (descriptor.PipelineVariants.Any(value => value.Semantic == RenderPipelineVariantSemantic.TerrainMultiviewDirectionalShadowCaster)) { shaders = shaders with { MultiviewCasters = new DirectionalShadowMultiviewPipelineShaders( Variant(RenderPipelineVariantSemantic.TerrainMultiviewDirectionalShadowCaster), Variant(RenderPipelineVariantSemantic.WorldOpaqueMultiviewDirectionalShadowCaster), Variant(RenderPipelineVariantSemantic.WorldAlphaCutoutMultiviewDirectionalShadowCaster)), }; } return shaders; GpuShaderSet Variant(RenderPipelineVariantSemantic semantic) { PipelineVariantDeclaration variant = descriptor.PipelineVariants .Single(value => value.Semantic == semantic); return RenderPackShaderAssets.LoadVariant(descriptor, assets, variant); } } private static DirectionalShadowQuality ResolveShadowQuality( RenderPackDescriptor descriptor, RenderQualityPreset preset, IReadOnlyDictionary userSettingOverrides) { DirectionalShadowPreset shadowPreset = preset.Semantic switch { RenderQualitySemantic.Low => DirectionalShadowPreset.Low, RenderQualitySemantic.High => DirectionalShadowPreset.High, _ => DirectionalShadowPreset.Medium, }; DirectionalShadowQuality quality = DirectionalShadowQuality.For(shadowPreset); RenderResourceDeclaration resource = descriptor.Resources.Single(value => value.Semantic == RenderResourceSemantic.DirectionalShadowDepth); RenderExtentDeclaration extent = preset.ResourceOverrides.FirstOrDefault(value => string.Equals( value.ResourceId, resource.Id, StringComparison.OrdinalIgnoreCase))?.Extent ?? resource.Extent ?? throw new NotSupportedException( "The DirectionalShadowDepth semantic resource has no image extent."); if (extent.Mode != RenderExtentMode.AbsolutePixels || extent.Width != extent.Height || extent.Width != Math.Truncate(extent.Width) || extent.Width is < 1 or > 16_384 || extent.Layers is < 1 or > 4) { throw new NotSupportedException( "The DirectionalShadowDepth semantic resource must be a square " + "absolute 1..16384 image with 1..4 array layers."); } float reach = ReadSemanticSetting( descriptor, preset, userSettingOverrides, RenderSettingSemantic.DirectionalShadowReachMetres); int taps = ReadShadowPcfTaps(descriptor, preset, userSettingOverrides); int radius = taps switch { 1 => 0, 9 => 1, 25 => 2, _ => throw new NotSupportedException( "DirectionalShadowPcfTaps must resolve to exactly 1, 9, or 25 samples."), }; int resolution = checked((int)extent.Width); int cascades = extent.Layers; return quality with { CascadeCount = cascades, MapResolution = resolution, MaximumReachMeters = Math.Clamp(reach, 1f, 10_000f), PcfRadiusTexels = radius, ApproximateDepthMapBytes = checked( (long)cascades * resolution * resolution * sizeof(float)), IncrementalGpuP50BudgetMilliseconds = preset.MaxIncrementalGpuMillisecondsP50, IncrementalGpuP99BudgetMilliseconds = preset.MaxIncrementalGpuMillisecondsP99, IncrementalCpuP50BudgetMilliseconds = preset.MaxIncrementalCpuMillisecondsP50, IncrementalCpuP99BudgetMilliseconds = preset.MaxIncrementalCpuMillisecondsP99, PackResidentGpuByteBudget = preset.MaxResidentGpuBytes, }; } private static int ReadShadowPcfTaps( RenderPackDescriptor descriptor, RenderQualityPreset preset, IReadOnlyDictionary userSettingOverrides) { RenderSettingDeclaration setting = descriptor.Settings.Single(value => value.Semantic == RenderSettingSemantic.DirectionalShadowPcfTaps); string value = RenderPackSettingResolution.Resolve( setting, preset, userSettingOverrides); return int.TryParse( value, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out int taps) ? taps : throw new NotSupportedException( "DirectionalShadowPcfTaps must resolve to an integer sample count."); } private static float ReadSemanticSetting( RenderPackDescriptor descriptor, RenderQualityPreset preset, IReadOnlyDictionary userSettingOverrides, RenderSettingSemantic semantic) { RenderSettingDeclaration setting = descriptor.Settings.Single(value => value.Semantic == semantic); string value = RenderPackSettingResolution.Resolve( setting, preset, userSettingOverrides); if (!RenderPackSettingValueCodec.TryEncode(setting, value, out float encoded) || !float.IsFinite(encoded)) { throw new NotSupportedException( $"Setting semantic '{semantic}' did not resolve to a finite value."); } return encoded; } private RenderResourceDeclaration Resource(string id) => _resources.TryGetValue(id, out RenderResourceDeclaration? value) ? value : throw new InvalidOperationException($"Unknown render-pack resource '{id}'."); private static GpuTextureFormat FormatOf(RenderResourceDeclaration resource) => resource.Format switch { RenderFormatClass.HdrColor => GpuTextureFormat.Rgba16FloatRenderTarget, RenderFormatClass.LdrColor or RenderFormatClass.SingleChannel => GpuTextureFormat.Rgba8UnormRenderTarget, _ => throw new NotSupportedException( $"Fullscreen resource '{resource.Id}' has unsupported format '{resource.Format}'."), }; private static GpuTextureFormat ValidateOutput(RenderResourceDeclaration resource) { if (resource.Kind != RenderResourceKind.Image2D || (resource.Usage & RenderResourceUsage.ColorAttachment) == 0 || resource.Extent is null) { throw new NotSupportedException( $"Fullscreen output '{resource.Id}' must be an extent-declared colour Image2D."); } return FormatOf(resource); } private sealed record Node( RenderPassDeclaration Pass, IGpuPipeline Pipeline, RenderPackTextureInput[] Inputs, string TimerName); private sealed class TargetSet : IDisposable { private readonly IGpuDevice _device; private readonly Dictionary _resources; private readonly GpuTextureSlot[] _slots; private readonly string? _mainWorldResourceId; private TargetSet( IGpuDevice device, int width, int height, int sampleCount, IGpuRenderTarget world, GpuTextureSlot worldColor, GpuTextureSlot worldDepth, Dictionary resources, GpuTextureSlot[] slots, string? mainWorldResourceId) { _device = device; Width = width; Height = height; SampleCount = sampleCount; World = world; WorldColor = worldColor; WorldDepth = worldDepth; _resources = resources; _slots = slots; _mainWorldResourceId = mainWorldResourceId; } internal int Width { get; } internal int Height { get; } internal int SampleCount { get; } internal IGpuRenderTarget World { get; } internal GpuTextureSlot WorldColor { get; } internal GpuTextureSlot WorldDepth { get; } internal int ImageCount => checked( 2 + _resources.Count + (SampleCount > 1 ? (WorldDepth.IsAssigned ? 2 : 1) : 0)); internal ResourceTarget Resource(string id) => string.Equals(id, _mainWorldResourceId, StringComparison.OrdinalIgnoreCase) ? new ResourceTarget(World, WorldColor) : _resources.TryGetValue(id, out ResourceTarget? value) ? value : throw new InvalidOperationException($"Resource '{id}' has no produced image."); internal static TargetSet Create( IGpuDevice device, RenderPackDescriptor descriptor, RenderQualityPreset preset, IGpuSampler sampler, int width, int height, int samples) { var targets = new List(); var slots = new List(); try { bool needsDepth = descriptor.Passes.Any(pass => pass.SemanticInputs.Contains(RenderSemanticInput.SceneDepth)); IGpuRenderTarget world = device.CreateRenderTarget(new GpuRenderTargetDescription( $"render-pack-{descriptor.Id}-world-hdr", width, height, GpuTextureFormat.Rgba16FloatRenderTarget, GpuTextureFormat.Depth24Stencil8, samples, needsDepth)); targets.Add(world); GpuTextureSlot worldColor = Register(device, world.ColorTexture, sampler, slots); GpuTextureSlot worldDepth = needsDepth ? Register(device, world.DepthTexture!, sampler, slots) : GpuTextureSlot.Unassigned; var resources = new Dictionary(StringComparer.OrdinalIgnoreCase); string? mainWorldResourceId = descriptor.Resources.SingleOrDefault(resource => resource.Semantic == RenderResourceSemantic.MainWorldHdr)?.Id; HashSet written = descriptor.Passes .SelectMany(pass => pass.ResourceWrites) .ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (RenderResourceDeclaration resource in descriptor.Resources) { if (!written.Contains(resource.Id) || resource.Semantic is RenderResourceSemantic.MainWorldHdr or RenderResourceSemantic.DirectionalShadowDepth) continue; if (resource.Kind != RenderResourceKind.Image2D || (resource.Usage & RenderResourceUsage.ColorAttachment) == 0) throw new NotSupportedException($"Fullscreen resource '{resource.Id}' is not a colour image."); (int resourceWidth, int resourceHeight) = Extent(resource, preset, width, height); IGpuRenderTarget target = device.CreateRenderTarget(new GpuRenderTargetDescription( $"render-pack-{descriptor.Id}-{resource.Id}", resourceWidth, resourceHeight, FormatOf(resource), null, 1)); targets.Add(target); resources.Add(resource.Id, new ResourceTarget( target, Register(device, target.ColorTexture, sampler, slots))); } return new TargetSet( device, width, height, samples, world, worldColor, worldDepth, resources, [.. slots], mainWorldResourceId); } catch { for (int i = slots.Count - 1; i >= 0; i--) device.ReleaseTextureSlot(slots[i]); for (int i = targets.Count - 1; i >= 0; i--) targets[i].Dispose(); throw; } } public void Dispose() { for (int i = _slots.Length - 1; i >= 0; i--) _device.ReleaseTextureSlot(_slots[i]); foreach (ResourceTarget resource in _resources.Values.Reverse()) resource.Target.Dispose(); World.Dispose(); } private static (int Width, int Height) Extent( RenderResourceDeclaration resource, RenderQualityPreset preset, int width, int height) { RenderExtentDeclaration extent = preset.ResourceOverrides.FirstOrDefault(value => string.Equals(value.ResourceId, resource.Id, StringComparison.OrdinalIgnoreCase))?.Extent ?? resource.Extent ?? throw new NotSupportedException($"Image resource '{resource.Id}' has no extent."); return extent.Mode switch { RenderExtentMode.AbsolutePixels => (checked((int)extent.Width), checked((int)extent.Height)), RenderExtentMode.RelativeToMainWorld or RenderExtentMode.RelativeToOutput => (Math.Max(1, (int)Math.Ceiling(width * extent.Width)), Math.Max(1, (int)Math.Ceiling(height * extent.Height))), _ => throw new NotSupportedException($"Resource '{resource.Id}' has unsupported extent mode."), }; } private static GpuTextureSlot Register( IGpuDevice device, IGpuTexture texture, IGpuSampler sampler, List slots) { GpuTextureSlot slot = device.RegisterTexture(texture, sampler); slots.Add(slot); return slot; } } internal sealed record ResourceTarget(IGpuRenderTarget Target, GpuTextureSlot Slot); } internal sealed class DeclaredDirectionalShadowRenderPackGraph : DeclaredFullscreenRenderPackGraph, IDirectionalShadowWorldGraphRuntime { internal DeclaredDirectionalShadowRenderPackGraph( IGpuDevice device, RenderPackDescriptor descriptor, IRenderPackAssets assets, RenderQualityPreset preset, IReadOnlyDictionary userSettingOverrides) : base(device, descriptor, assets, preset, userSettingOverrides) { } internal DeclaredDirectionalShadowRenderPackGraph( IGpuDevice device, RenderPackDescriptor descriptor, ValidatedRenderPackShaderAssets assets, RenderQualityPreset preset, IReadOnlyDictionary userSettingOverrides) : base(device, descriptor, assets, preset, userSettingOverrides) { } public IDirectionalShadowReceiverSource DirectionalShadowReceivers => DeclaredDirectionalShadowReceivers; public DirectionalSunShadowDiagnostics RenderDirectionalShadows( IGpuFrame frame, in RenderFrameFoundation foundation, in WorldRenderFrame world, int activeDayGroup, in RenderSceneQuery scene, WbDrawDispatcher worldMeshes, TerrainModernRenderer terrain) => RenderDeclaredDirectionalShadows( frame, in foundation, in world, activeDayGroup, in scene, worldMeshes, terrain); }