using System.Diagnostics; using System.Numerics; using System.Runtime.InteropServices; using AcDream.App.Rendering.Gpu; using AcDream.Plugin.Abstractions.Rendering; namespace AcDream.App.Rendering.Packs; internal enum VolumetricShaftGateReason : byte { Rendered, DisabledByPreset, NoCurrentDirectionalShadow, NoSceneDepth, Indoor, SunOffScreen, SunBelowHorizon, AtmosphereSuppressed, } internal readonly record struct VolumetricShaftDiagnostics( VolumetricShaftGateReason GateReason, int Width, int Height, int RayMarchSteps, float Density, float Strength, long RetainedGpuBytes, double LastResolvedGpuMilliseconds, bool HasResolvedGpuMeasurement, int DrawCalls); internal readonly record struct VolumetricShaftOutput( GpuTextureSlot TextureSlot, VolumetricShaftDiagnostics Diagnostics) { internal bool HasTexture => TextureSlot.IsAssigned; } /// /// Tier-2+ shadow-map volumetric producer. It consumes only the current frame's /// b5/b6/b8 facts and scene depth, and owns one preset-scaled HDR result. It has /// no clock, weather state, caster traversal, or independent sun policy. /// internal sealed class VolumetricShaftRenderer : IDisposable { internal const string TimerName = "atmospheric-volumetric-shafts"; private readonly IGpuDevice _device; private readonly VolumetricShaftQuality _quality; private readonly float _declaredStrength; private readonly AtmospherePolicyDeclaration _atmospherePolicy; private readonly IReadOnlyDictionary _dayGroupMultipliers; private readonly IGpuSampler _sampler; private readonly IGpuPipeline _pipeline; private readonly PackSettingsUniforms _settings; private readonly RenderPackPerformanceWindow _performance = new(); private Target? _target; private bool _disposed; internal VolumetricShaftRenderer( IGpuDevice device, RenderPackDescriptor descriptor, IRenderPackAssets assets, RenderQualityPreset preset, IReadOnlyDictionary? userSettingOverrides = null) : this( device, descriptor, RenderPackShaderAssets.Validate(descriptor, assets), preset, userSettingOverrides) { } internal VolumetricShaftRenderer( IGpuDevice device, RenderPackDescriptor descriptor, ValidatedRenderPackShaderAssets assets, RenderQualityPreset preset, IReadOnlyDictionary? userSettingOverrides = null) { _device = device ?? throw new ArgumentNullException(nameof(device)); ArgumentNullException.ThrowIfNull(descriptor); ArgumentNullException.ThrowIfNull(assets); ArgumentNullException.ThrowIfNull(preset); _quality = ResolveQuality( descriptor, preset, userSettingOverrides); _declaredStrength = ReadSetting( descriptor, preset, userSettingOverrides, RenderSettingSemantic.VolumetricStrength, 0.35f); _atmospherePolicy = descriptor.AtmospherePolicy ?? throw new NotSupportedException( $"Pack '{descriptor.Id}' declares no atmosphere policy."); if (_atmospherePolicy.VolumetricShaftSunElevationResponse.Count < 2) { throw new NotSupportedException( $"Pack '{descriptor.Id}' declares no volumetric-shaft elevation curve."); } _dayGroupMultipliers = _atmospherePolicy.ActiveDayGroupMultipliers .ToDictionary(value => value.ActiveDayGroup, value => (float)value.Multiplier); _settings = PackSettingsUniforms.Create(descriptor, preset, userSettingOverrides); RenderPassDeclaration pass = descriptor.Passes.FirstOrDefault(value => value.Semantic == RenderPassSemantic.VolumetricShafts) ?? throw new NotSupportedException( $"Pack '{descriptor.Id}' declares no VolumetricShafts pass semantic."); _sampler = device.CreateSampler(GpuSamplerDescription.WorldClamp); _pipeline = device.CreatePipeline(new GpuPipelineDescription { Name = $"render-pack-{descriptor.Id}-volumetric-shafts", Shaders = RenderPackShaderAssets.LoadPass(descriptor, assets, pass), VertexLayout = GpuVertexLayout.None, Blend = GpuBlendMode.None, Depth = GpuDepthState.Disabled, Cull = GpuCullMode.None, ColorFormat = GpuTextureFormat.Rgba16FloatRenderTarget, AllowColorFormatVariants = false, SampleCount = 1, UsesRenderPackShaderAbi = true, }); LastDiagnostics = Disabled(VolumetricShaftGateReason.DisabledByPreset); } internal VolumetricShaftDiagnostics LastDiagnostics { get; private set; } internal VolumetricShaftQuality Quality => _quality; internal RenderPackPerformanceSnapshot Performance => _performance.Snapshot(); /// /// Builds the selected preset's optional shaft target during off-side pack /// activation/resize. A disabled preset owns no target; enabling it later /// through a user override is reflected in . /// internal void PrepareTarget(int outputWidth, int outputHeight) { ObjectDisposedException.ThrowIf(_disposed, this); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(outputWidth); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(outputHeight); if (_declaredStrength > 0f) _ = Prepare(outputWidth, outputHeight); } internal VolumetricShaftOutput Render( IGpuFrame frame, in AtmosphericFrameInputs inputs, in DirectionalShadowFrameBinding shadow, GpuTextureSlot sceneDepth) { ObjectDisposedException.ThrowIf(_disposed, this); ArgumentNullException.ThrowIfNull(frame); VolumetricShaftGateReason reason = Gate(frame, inputs, shadow, sceneDepth); if (reason != VolumetricShaftGateReason.Rendered) { LastDiagnostics = Disabled(reason); return new VolumetricShaftOutput(GpuTextureSlot.Unassigned, LastDiagnostics); } (float density, float strength) = Parameters(inputs); if (strength <= 1e-4f) { LastDiagnostics = Disabled(VolumetricShaftGateReason.AtmosphereSuppressed); return new VolumetricShaftOutput(GpuTextureSlot.Unassigned, LastDiagnostics); } Target target = Prepare(inputs.ViewportWidth, inputs.ViewportHeight); long started = Stopwatch.GetTimestamp(); AtmosphericFrameUniforms atmospheric = FrameUniforms(inputs, strength); GpuRingAllocation frameBlock = frame.AllocateRing( AtmosphericFrameUniforms.SizeInBytes, GpuRingUsage.Uniform); MemoryMarshal.Write(frameBlock.Data, in atmospheric); GpuRingAllocation passBlock = frame.AllocateRing( AtmosphericPackPassUniforms.SizeInBytes, GpuRingUsage.Uniform); var passValues = new AtmosphericPackPassUniforms( new Vector4(density, strength, _quality.RayMarchSteps, 1f), Vector4.Zero, Vector4.Zero, Vector4.Zero); MemoryMarshal.Write(passBlock.Data, in passValues); GpuRingAllocation settingsBlock = frame.AllocateRing( PackSettingsUniforms.SizeInBytes, GpuRingUsage.Uniform); PackSettingsUniforms settings = _settings; MemoryMarshal.Write(settingsBlock.Data, in settings); using (IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription { Name = TimerName, Color = new GpuColorAttachment( target.RenderTarget, GpuLoadOp.Clear, GpuStoreOp.Store, Vector4.Zero), Depth = null, SampleCount = 1, })) using (encoder.BeginTimerScope(TimerName)) { encoder.BindPipeline(_pipeline); encoder.BindUniformBuffer( GpuBindingModel.UniformAtmosphericFrame, frameBlock.Buffer, frameBlock.OffsetBytes, AtmosphericFrameUniforms.SizeInBytes); encoder.BindUniformBuffer( GpuBindingModel.UniformDirectionalShadow, shadow.Buffer!, shadow.OffsetBytes, shadow.SizeBytes); encoder.BindUniformBuffer( GpuBindingModel.UniformPackPass, passBlock.Buffer, passBlock.OffsetBytes, AtmosphericPackPassUniforms.SizeInBytes); encoder.BindUniformBuffer( GpuBindingModel.UniformPackSettings, settingsBlock.Buffer, settingsBlock.OffsetBytes, PackSettingsUniforms.SizeInBytes); GpuPushConstants push = GpuPushConstants.Default; push.TextureIndexA = sceneDepth.Index; push.TextureIndexB = GpuTextureSlot.Unassigned.Index; push.ParamA = BitConverter.UInt32BitsToSingle(GpuTextureSlot.Unassigned.Index); push.ParamB = BitConverter.UInt32BitsToSingle(GpuTextureSlot.Unassigned.Index); encoder.SetPushConstants(in push); encoder.Draw(3, 1, 0, 0); } bool hasGpu = _device.Timers.TryResolve(TimerName, out double milliseconds); LastDiagnostics = new VolumetricShaftDiagnostics( VolumetricShaftGateReason.Rendered, target.RenderTarget.Description.Width, target.RenderTarget.Description.Height, _quality.RayMarchSteps, density, strength, target.RetainedBytes, milliseconds, hasGpu, DrawCalls: 1); _performance.Observe( Stopwatch.GetElapsedTime(started).TotalMilliseconds, absoluteReceiverCpuMilliseconds: 0d, hasGpu, milliseconds, target.RetainedBytes, transientGpuBytes: 0); return new VolumetricShaftOutput(target.TextureSlot, LastDiagnostics); } public void Dispose() { if (_disposed) return; _disposed = true; _target?.Dispose(); _target = null; _pipeline.Dispose(); } private Target Prepare(int outputWidth, int outputHeight) { int width = Math.Max(1, (int)MathF.Ceiling(outputWidth * _quality.ResolutionScale)); int height = Math.Max(1, (int)MathF.Ceiling(outputHeight * _quality.ResolutionScale)); if (_target is { } current && current.RenderTarget.Description.Width == width && current.RenderTarget.Description.Height == height) return current; IGpuRenderTarget? renderTarget = null; GpuTextureSlot slot = GpuTextureSlot.Unassigned; try { renderTarget = _device.CreateRenderTarget(new GpuRenderTargetDescription( "atmospheric-volumetric", width, height, GpuTextureFormat.Rgba16FloatRenderTarget, DepthFormat: null, SampleCount: 1)); slot = _device.RegisterTexture(renderTarget.ColorTexture, _sampler); var candidate = new Target(_device, renderTarget, slot); renderTarget = null; slot = GpuTextureSlot.Unassigned; Target? prior = _target; _target = candidate; prior?.Dispose(); _performance.Reset(); return candidate; } catch { if (slot.IsAssigned) _device.ReleaseTextureSlot(slot); renderTarget?.Dispose(); throw; } } private VolumetricShaftGateReason Gate( IGpuFrame frame, in AtmosphericFrameInputs inputs, in DirectionalShadowFrameBinding shadow, GpuTextureSlot sceneDepth) { if (_declaredStrength <= 0f) return VolumetricShaftGateReason.DisabledByPreset; if (!shadow.IsValidFor(frame)) return VolumetricShaftGateReason.NoCurrentDirectionalShadow; if (!sceneDepth.IsAssigned) return VolumetricShaftGateReason.NoSceneDepth; if (!inputs.IsOutdoor) return VolumetricShaftGateReason.Indoor; if (!inputs.SunIsOnScreen) return VolumetricShaftGateReason.SunOffScreen; return VolumetricShaftGateReason.Rendered; } private (float Density, float Strength) Parameters(in AtmosphericFrameInputs inputs) { float weatherTarget = inputs.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, }; float weatherBlend = Math.Clamp(inputs.WeatherIntensity, 0f, 1f); float weather = 1f + ((weatherTarget - 1f) * weatherBlend); float elevation = RenderPackAtmospherePolicyEvaluation.VolumetricShaft( _atmospherePolicy.VolumetricShaftSunElevationResponse, inputs.SunElevationDegrees); float authoredEnergy = Math.Clamp(inputs.SunDirectionalBrightness, 0f, 4f); float dayGroup = _dayGroupMultipliers.TryGetValue( inputs.ActiveDayGroup, out float declaredDayGroup) ? Math.Clamp(declaredDayGroup, 0f, 4f) : 1f; float strength = Math.Clamp( _declaredStrength * weather * elevation * authoredEnergy * dayGroup, 0f, 1f); return (0.035f * strength, strength); } private AtmosphericFrameUniforms FrameUniforms( in AtmosphericFrameInputs inputs, float strength) => new( new Vector4(inputs.SunScreenUv, strength, inputs.SunElevationDegrees), new Vector4(inputs.SunColor, strength), new Vector4(inputs.ViewportWidth, inputs.ViewportHeight, 1f / inputs.ViewportWidth, 1f / inputs.ViewportHeight), 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, _dayGroupMultipliers.TryGetValue(inputs.ActiveDayGroup, out float dayGroup) ? dayGroup : 1f, RenderPackAtmospherePolicyEvaluation.DirectionalShadow( _atmospherePolicy.DirectionalShadowLightElevationResponse, inputs.SunElevationDegrees), RenderPackAtmospherePolicyEvaluation.VolumetricShaft( _atmospherePolicy.VolumetricShaftSunElevationResponse, inputs.SunElevationDegrees)), inputs.InverseViewProjection, // Campaign VM VM6 ABI v2 additions — the volumetric-shaft pass does // not read foliage wind. Vector4.Zero, Vector4.Zero); private VolumetricShaftDiagnostics Disabled(VolumetricShaftGateReason reason) => new( reason, 0, 0, _quality.RayMarchSteps, 0f, 0f, _target?.RetainedBytes ?? 0L, 0d, false, 0); private static DirectionalShadowPreset PresetOf(RenderQualityPreset preset) => preset.Semantic switch { RenderQualitySemantic.Low => DirectionalShadowPreset.Low, RenderQualitySemantic.High => DirectionalShadowPreset.High, _ => DirectionalShadowPreset.Medium, }; private static VolumetricShaftQuality ResolveQuality( RenderPackDescriptor descriptor, RenderQualityPreset preset, IReadOnlyDictionary? userSettingOverrides) { VolumetricShaftQuality quality = VolumetricShaftQuality.For(PresetOf(preset)); RenderResourceDeclaration resource = descriptor.Resources.Single(value => value.Semantic == RenderResourceSemantic.VolumetricShafts); RenderQualityResourceOverride? resourceOverride = preset.ResourceOverrides .FirstOrDefault(value => string.Equals( value.ResourceId, resource.Id, StringComparison.OrdinalIgnoreCase)); RenderExtentDeclaration extent = resourceOverride?.Extent ?? resource.Extent ?? throw new NotSupportedException( "The VolumetricShafts semantic resource has no image extent."); if (extent.Mode is not RenderExtentMode.RelativeToMainWorld and not RenderExtentMode.RelativeToOutput) { throw new NotSupportedException( "The VolumetricShafts semantic resource must use a relative extent."); } int steps = checked((int)MathF.Round(ReadSetting( descriptor, preset, userSettingOverrides, RenderSettingSemantic.VolumetricRayMarchSteps, quality.RayMarchSteps))); return quality with { ResolutionScale = (float)Math.Clamp(extent.Width, 0.0625, 1.0), RayMarchSteps = Math.Clamp(steps, 8, 64), }; } private static float ReadSetting( RenderPackDescriptor descriptor, RenderQualityPreset preset, IReadOnlyDictionary? userSettingOverrides, RenderSettingSemantic semantic, float fallback) { RenderSettingDeclaration? setting = descriptor.Settings.FirstOrDefault(candidate => candidate.Semantic == semantic); if (setting is null) return fallback; string value = RenderPackSettingResolution.Resolve( setting, preset, userSettingOverrides); return RenderPackSettingValueCodec.TryEncode(setting, value, out float encoded) ? Math.Max(0f, encoded) : fallback; } private sealed class Target( IGpuDevice device, IGpuRenderTarget renderTarget, GpuTextureSlot textureSlot) : IDisposable { internal IGpuRenderTarget RenderTarget { get; } = renderTarget; internal GpuTextureSlot TextureSlot { get; } = textureSlot; internal long RetainedBytes => checked( (long)RenderTarget.Description.Width * RenderTarget.Description.Height * 8L); public void Dispose() { device.ReleaseTextureSlot(TextureSlot); RenderTarget.Dispose(); } } }