acdream/src/AcDream.App/Rendering/Packs/DeclaredFullscreenRenderPackGraph.cs
Erik 132395e6f7 fix #425: pack resident budget is a 1080p ceiling that scales with pixel count; an explicit Apply retries a failed selection
Live Holtburg at 2560x1440: the Low preset needed 67,368,164 resident bytes
(screen-sized HDR/depth/ray targets are 44 MB of that) against an absolute
64 MiB ceiling that had only been validated at 1080p, so Options -> Apply
fell back to the default path; every later Apply was then refused by the
controller's failure memo, which treated the user's deliberate choice like
automatic re-activation.

RenderPackResidentBudget.Effective scales the declared 1080p figure by the
viewport's pixel-count ratio (never below 1), still capped by the hardware
MaxPackResidentBytes; both pack graphs use it and the performance-matrix
tool judges its resident column by the same rule (contract test updated).
RenderPackController.Request gains explicitUserChoice, which clears the
memo for that selection; RenderPackSelectionBinding passes it on every
display edge (Apply, including resolution changes) and keeps the memo for
the startup request.

Tests: RenderPackResidentBudgetTests (1080p/720p keep the declared
ceiling, 1440p = 16/9x, 4K = 4x, hardware cap wins, zero extent rejected);
controller explicit-retry; the binding test now proves the user's next
Apply activates once the cause is gone. App hermetic lane 6,068/0 (Release).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 10:00:11 +02:00

974 lines
41 KiB
C#

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;
/// <summary>
/// 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.
/// </summary>
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<string, RenderResourceDeclaration> _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<string, string> userSettingOverrides)
: this(
device,
descriptor,
RenderPackShaderAssets.Validate(descriptor, assets),
preset,
userSettingOverrides)
{
}
internal DeclaredFullscreenRenderPackGraph(
IGpuDevice device,
RenderPackDescriptor descriptor,
ValidatedRenderPackShaderAssets assets,
RenderQualityPreset preset,
IReadOnlyDictionary<string, string> 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<Node>(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<SunElevationResponsePoint> shadowCurve =
Descriptor.AtmospherePolicy?.DirectionalShadowLightElevationResponse ?? [];
IReadOnlyList<SunElevationResponsePoint> 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<GpuTextureSlot> 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<SunElevationResponsePoint>? 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<string, string> 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<string, string> 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<string, string> 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<string, ResourceTarget> _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<string, ResourceTarget> 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<IGpuRenderTarget>();
var slots = new List<GpuTextureSlot>();
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<string, ResourceTarget>(StringComparer.OrdinalIgnoreCase);
string? mainWorldResourceId = descriptor.Resources.SingleOrDefault(resource =>
resource.Semantic == RenderResourceSemantic.MainWorldHdr)?.Id;
HashSet<string> 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<GpuTextureSlot> 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<string, string> userSettingOverrides)
: base(device, descriptor, assets, preset, userSettingOverrides)
{
}
internal DeclaredDirectionalShadowRenderPackGraph(
IGpuDevice device,
RenderPackDescriptor descriptor,
ValidatedRenderPackShaderAssets assets,
RenderQualityPreset preset,
IReadOnlyDictionary<string, string> 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);
}