feat(render): implement Campaign AR and terrain fidelity

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

View file

@ -0,0 +1,228 @@
namespace AcDream.App.Rendering.Packs;
using AcDream.Plugin.Abstractions.Rendering;
internal enum AtmosphericQualityLevel : byte
{
Low,
Medium,
High,
}
internal readonly record struct AtmosphericQualityMeasurement(
double InclusivePackGpuMillisecondsP99,
double IncrementalCpuMillisecondsP99,
long ResidentGpuBytes,
bool StableFrameBoundary);
internal readonly record struct AtmosphericAutoQualitySnapshot(
AtmosphericQualityLevel Current,
int ConsecutiveOverBudgetFrames,
int ConsecutiveHeadroomFrames,
int CooldownFramesRemaining,
long ChangeGeneration,
bool SafeFallbackToRetailRequested);
internal readonly record struct AtmosphericQualityBudget(
double GpuMillisecondsP99,
double CpuMillisecondsP99,
long ResidentGpuBytes)
{
internal static AtmosphericQualityBudget FromPreset(RenderQualityPreset preset) => new(
preset.MaxIncrementalGpuMillisecondsP99,
preset.MaxIncrementalCpuMillisecondsP99,
preset.MaxResidentGpuBytes);
}
/// <summary>
/// Long-hysteresis automatic quality policy. It changes only resolution,
/// cascade count/reach, and post-process sampling through one stable preset
/// swap. If even Low remains over its declared budget, it requests an atomic
/// whole-pack fallback to retail instead of silently dropping caster classes.
/// </summary>
internal sealed class AtmosphericAutoQualityController
{
internal const int DowngradeHysteresisFrames = 180;
internal const int UpgradeHysteresisFrames = 900;
internal const int ChangeCooldownFrames = 300;
private AtmosphericQualityLevel _current;
private readonly AtmosphericQualityLevel _minimum;
private readonly AtmosphericQualityLevel _maximum;
private readonly AtmosphericQualityBudget[] _budgets;
private int _overBudget;
private int _headroom;
private int _cooldown;
private long _generation;
private bool _safeFallbackToRetailRequested;
internal AtmosphericAutoQualityController(
AtmosphericQualityLevel initial = AtmosphericQualityLevel.Medium,
AtmosphericQualityLevel minimum = AtmosphericQualityLevel.Low,
AtmosphericQualityLevel maximum = AtmosphericQualityLevel.High)
: this(DefaultBudgets(), initial, minimum, maximum)
{
}
internal AtmosphericAutoQualityController(
IReadOnlyList<AtmosphericQualityBudget> budgets,
AtmosphericQualityLevel initial = AtmosphericQualityLevel.Medium,
AtmosphericQualityLevel minimum = AtmosphericQualityLevel.Low,
AtmosphericQualityLevel maximum = AtmosphericQualityLevel.High)
{
ArgumentNullException.ThrowIfNull(budgets);
if (budgets.Count != 3)
throw new ArgumentException("Auto quality requires Low, Medium, and High budgets.", nameof(budgets));
if (minimum > initial || initial > maximum)
throw new ArgumentOutOfRangeException(nameof(initial));
_budgets = budgets.ToArray();
foreach (AtmosphericQualityBudget budget in _budgets)
{
if (!double.IsFinite(budget.GpuMillisecondsP99)
|| budget.GpuMillisecondsP99 < 0d
|| !double.IsFinite(budget.CpuMillisecondsP99)
|| budget.CpuMillisecondsP99 < 0d
|| budget.ResidentGpuBytes < 0)
{
throw new ArgumentOutOfRangeException(
nameof(budgets),
"Automatic-quality budgets must be finite and non-negative.");
}
}
_minimum = minimum;
_maximum = maximum;
_current = initial;
}
internal AtmosphericAutoQualitySnapshot Snapshot => new(
_current,
_overBudget,
_headroom,
_cooldown,
_generation,
_safeFallbackToRetailRequested);
internal AtmosphericQualityBudget CurrentBudget => _budgets[(int)_current];
internal AtmosphericAutoQualitySnapshot Observe(
in AtmosphericQualityMeasurement measurement)
{
Validate(in measurement);
if (!measurement.StableFrameBoundary)
return Snapshot;
if (_safeFallbackToRetailRequested)
return Snapshot;
if (_cooldown > 0)
{
_cooldown--;
_overBudget = 0;
_headroom = 0;
return Snapshot;
}
AtmosphericQualityBudget budget = _budgets[(int)_current];
bool over = measurement.InclusivePackGpuMillisecondsP99
> budget.GpuMillisecondsP99
|| measurement.IncrementalCpuMillisecondsP99
> budget.CpuMillisecondsP99
|| measurement.ResidentGpuBytes > budget.ResidentGpuBytes;
if (over)
{
_overBudget++;
_headroom = 0;
if (_overBudget >= DowngradeHysteresisFrames)
{
if (_current != _minimum)
Change((AtmosphericQualityLevel)((int)_current - 1));
else
RequestSafeFallback();
}
return Snapshot;
}
_overBudget = 0;
if (_current == _maximum)
{
_headroom = 0;
return Snapshot;
}
AtmosphericQualityLevel next =
(AtmosphericQualityLevel)((int)_current + 1);
AtmosphericQualityBudget nextBudget = _budgets[(int)next];
bool hasHeadroom = measurement.InclusivePackGpuMillisecondsP99
<= nextBudget.GpuMillisecondsP99 * 0.70
&& measurement.IncrementalCpuMillisecondsP99
<= nextBudget.CpuMillisecondsP99 * 0.70
&& measurement.ResidentGpuBytes
<= (long)(nextBudget.ResidentGpuBytes * 0.70);
if (!hasHeadroom)
{
_headroom = 0;
return Snapshot;
}
_headroom++;
if (_headroom >= UpgradeHysteresisFrames)
Change(next);
return Snapshot;
}
internal void Reset(AtmosphericQualityLevel level)
{
_current = level;
_overBudget = 0;
_headroom = 0;
_cooldown = 0;
_safeFallbackToRetailRequested = false;
_generation = checked(_generation + 1);
}
private void Change(AtmosphericQualityLevel value)
{
_current = value;
_overBudget = 0;
_headroom = 0;
_cooldown = ChangeCooldownFrames;
_generation = checked(_generation + 1);
}
private void RequestSafeFallback()
{
_overBudget = DowngradeHysteresisFrames;
_headroom = 0;
_cooldown = 0;
_safeFallbackToRetailRequested = true;
_generation = checked(_generation + 1);
}
private static AtmosphericQualityBudget[] DefaultBudgets() =>
[
From(DirectionalShadowPreset.Low),
From(DirectionalShadowPreset.Medium),
From(DirectionalShadowPreset.High),
];
private static AtmosphericQualityBudget From(DirectionalShadowPreset preset)
{
DirectionalShadowQuality quality = DirectionalShadowQuality.For(preset);
return new AtmosphericQualityBudget(
quality.IncrementalGpuP99BudgetMilliseconds,
quality.IncrementalCpuP99BudgetMilliseconds,
quality.PackResidentGpuByteBudget);
}
private static void Validate(in AtmosphericQualityMeasurement value)
{
if (!double.IsFinite(value.InclusivePackGpuMillisecondsP99)
|| value.InclusivePackGpuMillisecondsP99 < 0
|| !double.IsFinite(value.IncrementalCpuMillisecondsP99)
|| value.IncrementalCpuMillisecondsP99 < 0
|| value.ResidentGpuBytes < 0)
{
throw new ArgumentOutOfRangeException(
nameof(value),
"Atmospheric quality measurements must be finite and non-negative.");
}
}
}

View file

@ -0,0 +1,153 @@
using System.Diagnostics;
using AcDream.App.Diagnostics;
namespace AcDream.App.Rendering.Packs;
internal readonly record struct AtmosphericCpuStageFrame(
long FrameSerial,
long ShadowCasterBuildTicks,
long ShadowEnvironmentTicks,
long ShadowPreparedDrawsAndTransformsTicks,
long ShadowFitAndUniformTicks,
long ShadowLayeredPassRecordingTicks,
long ShadowBookkeepingTicks,
long PostSetupAndOtherTicks,
long PostSunRaysTicks,
long PostFilmicTicks);
internal readonly record struct RenderPackCpuStageDiagnostics(
string Stage,
int SampleCount,
double CpuMillisecondsP50,
double CpuMillisecondsP95,
double CpuMillisecondsP99);
/// <summary>
/// Temporary Low-only structural profiler for the incremental CPU budget. It
/// samples the same one-in-four frames as Low GPU timestamps, keeping fewer
/// than half of the ordinary performance window instrumented while retaining
/// enough observations for a short physical run. Every hot-path buffer is
/// fixed at construction and observation is allocation-free.
/// </summary>
internal sealed class AtmosphericCpuStageProfiler
{
internal const int SampleIntervalFrames = AtmosphericGpuTimerSampling.LowIntervalFrames;
private static readonly string[] StageNames =
[
"target-preparation",
"shadow-caster-build",
"shadow-environment",
"shadow-prepared-draws-and-transforms",
"shadow-fit-and-uniform",
"shadow-layered-pass-recording",
"shadow-bookkeeping",
"post-setup-and-other",
"post-sun-rays",
"post-filmic",
"performance-observe-bookkeeping",
"measured-pack-total",
"measured-pack-unattributed",
];
private readonly FrameStatsBuffer[] _microseconds;
internal AtmosphericCpuStageProfiler(int capacity = RenderPackPerformanceWindow.DefaultCapacity)
{
_microseconds = new FrameStatsBuffer[StageNames.Length];
for (int i = 0; i < _microseconds.Length; i++)
_microseconds[i] = new FrameStatsBuffer(capacity);
}
internal static bool ShouldMeasure(long frameSerial)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(frameSerial);
return frameSerial % SampleIntervalFrames == 0;
}
internal void Observe(
in AtmosphericCpuStageFrame frame,
long targetPreparationTicks,
long measuredPackTotalTicks,
long observeBookkeepingTicks)
{
if (frame.FrameSerial <= 0)
throw new ArgumentOutOfRangeException(nameof(frame));
ArgumentOutOfRangeException.ThrowIfNegative(targetPreparationTicks);
ArgumentOutOfRangeException.ThrowIfNegative(measuredPackTotalTicks);
ArgumentOutOfRangeException.ThrowIfNegative(observeBookkeepingTicks);
long attributedTicks = checked(
targetPreparationTicks
+ frame.ShadowCasterBuildTicks
+ frame.ShadowEnvironmentTicks
+ frame.ShadowPreparedDrawsAndTransformsTicks
+ frame.ShadowFitAndUniformTicks
+ frame.ShadowLayeredPassRecordingTicks
+ frame.ShadowBookkeepingTicks
+ frame.PostSetupAndOtherTicks
+ frame.PostSunRaysTicks
+ frame.PostFilmicTicks);
long unattributedTicks = Math.Max(0L, measuredPackTotalTicks - attributedTicks);
Push(0, targetPreparationTicks);
Push(1, frame.ShadowCasterBuildTicks);
Push(2, frame.ShadowEnvironmentTicks);
Push(3, frame.ShadowPreparedDrawsAndTransformsTicks);
Push(4, frame.ShadowFitAndUniformTicks);
Push(5, frame.ShadowLayeredPassRecordingTicks);
Push(6, frame.ShadowBookkeepingTicks);
Push(7, frame.PostSetupAndOtherTicks);
Push(8, frame.PostSunRaysTicks);
Push(9, frame.PostFilmicTicks);
Push(10, observeBookkeepingTicks);
Push(11, measuredPackTotalTicks);
Push(12, unattributedTicks);
}
internal IReadOnlyList<RenderPackCpuStageDiagnostics> Snapshot()
{
var result = new RenderPackCpuStageDiagnostics[StageNames.Length];
for (int i = 0; i < result.Length; i++)
{
FrameStatsBuffer samples = _microseconds[i];
result[i] = new RenderPackCpuStageDiagnostics(
StageNames[i],
samples.Count,
samples.Percentile(0.50) / 1000d,
samples.Percentile(0.95) / 1000d,
samples.Percentile(0.99) / 1000d);
}
return result;
}
internal void Reset()
{
for (int i = 0; i < _microseconds.Length; i++)
_microseconds[i].Reset();
}
private void Push(int stage, long ticks)
{
long microseconds = checked((long)Math.Round(
ticks * 1_000_000d / Stopwatch.Frequency,
MidpointRounding.AwayFromZero));
_microseconds[stage].Push(microseconds);
}
}
/// <summary>
/// Optional production-frame seam. Only Low's built-in graph implements it;
/// retail, Medium, High, and declared graphs never enter the profiling path.
/// </summary>
internal interface IAtmosphericCpuStageProfileRuntime
{
bool ShouldProfileCpuFrame(long frameSerial);
void CompleteCpuProfile(
long frameSerial,
long targetPreparationTicks,
long measuredPackTotalTicks,
long observeBookkeepingTicks,
bool stableFrameBoundary);
}

View file

@ -0,0 +1,229 @@
using System.Numerics;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using AcDream.Core.World;
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.App.Rendering.Packs;
/// <summary>
/// Immutable authored atmosphere and exact camera projection captured from the
/// normal world frame. This value owns no gameplay or renderer objects and is
/// valid after the wrapped world renderer returns.
/// </summary>
internal readonly record struct AtmosphericFrameInputs(
Vector2 SunScreenUv,
bool SunIsOnScreen,
float SunElevationDegrees,
Vector3 SunColor,
Vector3 SunDirection,
float SunDirectionalBrightness,
Matrix4x4 InverseViewProjection,
int ActiveDayGroup,
WeatherKind Weather,
float WeatherIntensity,
double DeltaSeconds,
int ViewportWidth,
int ViewportHeight,
bool IsOutdoor);
internal interface IAtmosphericWorldFrameSink
{
void Publish(
in RenderFrameFoundation foundation,
in WorldRenderFrame world,
int activeDayGroup);
}
/// <summary>
/// One-frame handoff between <see cref="WorldSceneRenderer"/>, which owns the
/// canonical camera build, and the post graph. Reset happens before the world
/// pass so an intentionally skipped world can never reuse a prior camera.
/// </summary>
internal sealed class AtmosphericFrameInputState : IAtmosphericWorldFrameSink
{
private RenderFrameInput _host;
private RenderFrameFoundation _foundation;
private AtmosphericFrameInputs _current;
private bool _published;
internal void BeginFrame(
in RenderFrameInput host,
in RenderFrameFoundation foundation)
{
_host = host;
_foundation = foundation;
_current = default;
_published = false;
}
public void Publish(
in RenderFrameFoundation foundation,
in WorldRenderFrame world,
int activeDayGroup)
{
Vector3 direction = SkyStateProvider.SunDirectionFromKeyframe(foundation.Sky);
Vector3 sunPoint = world.Camera.Position + (direction * 10_000f);
Vector4 clip = Vector4.Transform(
new Vector4(sunPoint, 1f),
world.Camera.ViewProjection);
bool finite = float.IsFinite(clip.X)
&& float.IsFinite(clip.Y)
&& float.IsFinite(clip.W)
&& clip.W > 1e-5f;
Vector2 uv = finite
? new Vector2(
(clip.X / clip.W * 0.5f) + 0.5f,
0.5f - (clip.Y / clip.W * 0.5f))
: new Vector2(-1f, -1f);
bool onScreen = finite
&& uv.X >= 0f && uv.X <= 1f
&& uv.Y >= 0f && uv.Y <= 1f;
Matrix4x4 inverseViewProjection = Matrix4x4.Invert(
world.Camera.ViewProjection,
out Matrix4x4 inverse)
? inverse
: Matrix4x4.Identity;
_current = new AtmosphericFrameInputs(
uv,
onScreen,
foundation.Sky.SunPitchDeg,
foundation.Sky.SunColor,
direction,
foundation.Sky.DirBright,
inverseViewProjection,
activeDayGroup,
foundation.Atmosphere.Kind,
Math.Clamp(foundation.Atmosphere.Intensity, 0f, 1f),
_host.DeltaSeconds,
_host.ViewportWidth,
_host.ViewportHeight,
IsOutdoor: world.Roots.RenderSky && !world.Roots.CameraInsideCell);
_published = true;
}
internal AtmosphericFrameInputs Snapshot()
{
if (_published)
return _current;
// A portal/login frame deliberately skipped the normal world. Preserve
// its authored colour inputs but suppress every directional effect.
return new AtmosphericFrameInputs(
new Vector2(-1f, -1f),
SunIsOnScreen: false,
_foundation.Sky.SunPitchDeg,
_foundation.Sky.SunColor,
SkyStateProvider.SunDirectionFromKeyframe(_foundation.Sky),
_foundation.Sky.DirBright,
Matrix4x4.Identity,
-1,
_foundation.Atmosphere.Kind,
Math.Clamp(_foundation.Atmosphere.Intensity, 0f, 1f),
_host.DeltaSeconds,
_host.ViewportWidth,
_host.ViewportHeight,
IsOutdoor: false);
}
}
/// <summary>
/// Shader ABI SSOT for opt-in set 3 binding 5. Six std140 vec4 values followed by one
/// mat4, 160 bytes.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 4)]
internal readonly struct AtmosphericFrameUniforms
{
internal const int SizeInBytes = 160;
internal AtmosphericFrameUniforms(
Vector4 sunScreen,
Vector4 sunColor,
Vector4 viewport,
Vector4 weather,
Vector4 sunDirection,
Vector4 policy,
Matrix4x4 inverseViewProjection)
{
SunScreen = sunScreen;
SunColor = sunColor;
Viewport = viewport;
Weather = weather;
SunDirection = sunDirection;
Policy = policy;
InverseViewProjection = inverseViewProjection;
}
internal readonly Vector4 SunScreen;
internal readonly Vector4 SunColor;
internal readonly Vector4 Viewport;
internal readonly Vector4 Weather;
internal readonly Vector4 SunDirection;
internal readonly Vector4 Policy;
internal readonly Matrix4x4 InverseViewProjection;
}
/// <summary>
/// Shader ABI SSOT for opt-in set 3 binding 7. Passes assign meanings to four std140
/// vec4 values without changing the shared descriptor layout.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 4)]
internal readonly struct AtmosphericPackPassUniforms
{
internal const int SizeInBytes = 64;
internal AtmosphericPackPassUniforms(
Vector4 params0,
Vector4 params1,
Vector4 params2,
Vector4 params3)
{
Params0 = params0;
Params1 = params1;
Params2 = params2;
Params3 = params3;
}
internal readonly Vector4 Params0;
internal readonly Vector4 Params1;
internal readonly Vector4 Params2;
internal readonly Vector4 Params3;
internal static AtmosphericPackPassUniforms From(Vector4 params0) =>
new(params0, Vector4.Zero, Vector4.Zero, Vector4.Zero);
}
/// <summary>
/// Shader ABI SSOT for opt-in set 3 binding 8. API v1 exposes 64 scalar values in
/// descriptor declaration order, physically grouped as sixteen std140 vec4s.
/// </summary>
[InlineArray(RenderPackShaderAbi.PackSettingScalarCapacity)]
internal struct PackSettingsUniforms
{
internal const int SizeInBytes = RenderPackShaderAbi.PackSettingsSizeBytes;
private float _element0;
internal static PackSettingsUniforms Create(
RenderPackDescriptor descriptor,
RenderQualityPreset preset,
IReadOnlyDictionary<string, string>? userSettingOverrides = null)
{
var result = new PackSettingsUniforms();
int count = Math.Min(
descriptor.Settings.Count,
RenderPackShaderAbi.PackSettingScalarCapacity);
for (int i = 0; i < count; i++)
{
RenderSettingDeclaration setting = descriptor.Settings[i];
string value = RenderPackSettingResolution.Resolve(
setting,
preset,
userSettingOverrides);
result[i] = RenderPackSettingValueCodec.TryEncode(setting, value, out float encoded)
? encoded
: 0f;
}
return result;
}
}

View file

@ -0,0 +1,24 @@
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.App.Rendering.Packs;
/// <summary>
/// Detailed per-pass GPU timestamps are diagnostic commands, not visual work.
/// Low samples one complete frame in four so its tight median CPU budget is not
/// dominated by instrumentation; sampled frames still include every receiver,
/// shadow, and post-process scope and therefore preserve the inclusive GPU
/// measurement contract. Medium and High retain continuous measurement.
/// </summary>
internal static class AtmosphericGpuTimerSampling
{
internal const int LowIntervalFrames = 4;
internal static bool ShouldMeasure(
RenderQualitySemantic quality,
long frameSerial)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(frameSerial);
return quality is not RenderQualitySemantic.Low
|| frameSerial % LowIntervalFrames == 0;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,183 @@
using System.Numerics;
using AcDream.Core.World;
namespace AcDream.App.Rendering.Packs;
/// <summary>
/// Pack-only identity for the one celestial direction selected to cast the
/// current directional shadow map. Retail exposes one authored directional
/// colour/energy channel; moon meshes contribute direction only.
/// </summary>
internal enum AuthoredCelestialShadowSourceKind : uint
{
None = 0,
Sun = 1,
DominantMoon = 2,
SecondaryMoon = 3,
}
internal readonly record struct AuthoredCelestialShadowSource(
AuthoredCelestialShadowSourceKind Kind,
int ObjectIndex,
uint GfxObjId,
Vector3 SurfaceToLightDirection,
float ElevationSin,
float AuthoredEnergy)
{
internal static AuthoredCelestialShadowSource None(float authoredEnergy = 0f) =>
new(
AuthoredCelestialShadowSourceKind.None,
-1,
0u,
Vector3.UnitZ,
0f,
Math.Clamp(authoredEnergy, 0f, 1f));
internal bool IsAvailable =>
Kind is not AuthoredCelestialShadowSourceKind.None;
}
/// <summary>
/// Resolves the visible Dereth sun/moons from retail DAT sky objects and uses
/// the identical transform as <c>SkyRenderer</c>. This is an opt-in render-pack
/// enhancement; it never changes retail SceneLighting or world state.
/// </summary>
internal static class AuthoredCelestialShadowSourceResolver
{
internal const uint SunGfxObjId = 0x01001348u;
internal const uint DominantMoonGfxObjId = 0x01001F6Au;
internal const uint SecondaryMoonGfxObjId = 0x01001F67u;
internal static AuthoredCelestialShadowSource Resolve(
DayGroupData? dayGroup,
float dayFraction,
in SkyKeyframe sky)
{
float energy = Math.Clamp(
MathF.Max(sky.SunColor.X, MathF.Max(sky.SunColor.Y, sky.SunColor.Z)),
0f,
1f);
if (dayGroup is null || !float.IsFinite(dayFraction))
return AuthoredCelestialShadowSource.None(energy);
if (TryResolve(
dayGroup,
dayFraction,
SunGfxObjId,
AuthoredCelestialShadowSourceKind.Sun,
energy,
out var source)
|| TryResolve(
dayGroup,
dayFraction,
DominantMoonGfxObjId,
AuthoredCelestialShadowSourceKind.DominantMoon,
energy,
out source)
|| TryResolve(
dayGroup,
dayFraction,
SecondaryMoonGfxObjId,
AuthoredCelestialShadowSourceKind.SecondaryMoon,
energy,
out source))
{
return source;
}
return AuthoredCelestialShadowSource.None(energy);
}
private static bool TryResolve(
DayGroupData dayGroup,
float dayFraction,
uint roleGfxObjId,
AuthoredCelestialShadowSourceKind kind,
float energy,
out AuthoredCelestialShadowSource source)
{
for (int index = 0; index < dayGroup.SkyObjects.Count; index++)
{
SkyObjectData skyObject = dayGroup.SkyObjects[index];
if (skyObject.GfxObjId != roleGfxObjId
|| !skyObject.IsVisible(dayFraction))
{
continue;
}
SkyObjectReplaceData? replace = ActiveReplace(
dayGroup,
dayFraction,
checked((uint)index));
if (replace is not null && replace.Transparent >= 1f - 1e-5f)
continue;
uint effectiveGfxObjId = replace is { GfxObjId: not 0u }
? replace.GfxObjId
: skyObject.GfxObjId;
Vector3 anchor = replace is { GfxObjId: not 0u }
? replace.AuthoredSortCenter
: skyObject.AuthoredSortCenter;
if (!IsFiniteDirection(anchor))
continue;
float headingRadians = (replace?.Rotate ?? 0f) * (MathF.PI / 180f);
float rotationRadians = skyObject.CurrentAngle(dayFraction)
* (MathF.PI / 180f);
Matrix4x4 model = Matrix4x4.CreateRotationZ(-headingRadians)
* Matrix4x4.CreateRotationY(-rotationRadians);
Vector3 transformed = Vector3.TransformNormal(anchor, model);
float length = transformed.Length();
if (!float.IsFinite(length) || length <= 1e-5f)
continue;
Vector3 direction = transformed / length;
if (!IsFiniteDirection(direction) || direction.Z <= 0f)
continue;
source = new AuthoredCelestialShadowSource(
kind,
index,
effectiveGfxObjId,
direction,
direction.Z,
energy);
return true;
}
source = default;
return false;
}
private static SkyObjectReplaceData? ActiveReplace(
DayGroupData dayGroup,
float dayFraction,
uint objectIndex)
{
if (dayGroup.SkyTimes.Count == 0)
return null;
DatSkyKeyframeData active = dayGroup.SkyTimes[^1];
for (int i = 0; i < dayGroup.SkyTimes.Count; i++)
{
if (dayGroup.SkyTimes[i].Keyframe.Begin <= dayFraction)
active = dayGroup.SkyTimes[i];
else
break;
}
SkyObjectReplaceData? result = null;
foreach (SkyObjectReplaceData replace in active.Replaces)
{
if (replace.ObjectIndex == objectIndex)
result = replace;
}
return result;
}
private static bool IsFiniteDirection(Vector3 value) =>
float.IsFinite(value.X)
&& float.IsFinite(value.Y)
&& float.IsFinite(value.Z)
&& value.LengthSquared() > 1e-10f;
}

View file

@ -0,0 +1,519 @@
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.App.Rendering.Packs;
/// <summary>
/// The built-in Atmospheric Rendering pack is expressed through the same
/// public declaration consumed by third-party packs. Renderer implementation
/// code resolves public enum semantics and never recognizes this pack's IDs,
/// so the built-in receives no private capability or lifecycle shortcut.
/// </summary>
internal static class BuiltInAtmosphericRenderPack
{
internal const string Id = "acdream.atmospheric";
internal static RenderPackDescriptor Descriptor { get; } = new RenderPackDescriptor(
Id,
"Atmospheric Rendering",
new Version(1, 0, 0),
RenderPackApi.Current,
RenderPackTier.Tier2Plus,
[
RenderCapability.MainWorldColorIntermediate,
RenderCapability.FullscreenPasses,
RenderCapability.SceneDepthSampling,
RenderCapability.AuthoredSunDirection,
RenderCapability.AuthoredSunScreenPosition,
RenderCapability.AuthoredWeather,
RenderCapability.DirectionalShadowMaps,
RenderCapability.OutdoorDirectionalShadowCasterReplay,
RenderCapability.AnimatedCasterTransforms,
RenderCapability.AlphaCutoutShadowCasters,
RenderCapability.AuthoredCelestialDirectionalLight,
],
[RenderCapability.GpuTimestampQueries],
Resources(),
Passes(),
SceneReplays(),
PipelineVariants(),
QualityPresets(),
Settings(),
AtmospherePolicy())
{
FeatureSummary = "Filmic HDR atmosphere, moving sun-and-moon shadows from terrain, "
+ "trees, buildings, players, and monsters, plus optional volumetric shafts.",
};
internal static IRenderPackAssets CreateAssets(string shaderDirectory) =>
new DirectoryRenderPackAssets(shaderDirectory);
private static IReadOnlyList<RenderResourceDeclaration> Resources() =>
[
Image("world-hdr", RenderResourceSemantic.MainWorldHdr,
RenderFormatClass.HdrColor, 1.0, 1.0, 32L * 1024 * 1024),
Image("bloom-a", RenderResourceSemantic.BloomPing,
RenderFormatClass.HdrColor, 0.5, 0.5, 8L * 1024 * 1024),
Image("bloom-b", RenderResourceSemantic.BloomPong,
RenderFormatClass.HdrColor, 0.5, 0.5, 8L * 1024 * 1024),
Image("sun-mask", RenderResourceSemantic.SunOcclusionMask,
RenderFormatClass.SingleChannel, 0.25, 0.25, 2L * 1024 * 1024),
Image("sun-rays", RenderResourceSemantic.SunRays,
RenderFormatClass.HdrColor, 0.25, 0.25, 2L * 1024 * 1024),
new RenderResourceDeclaration(
"directional-shadow-depth",
RenderResourceKind.Image2DArray,
RenderFormatClass.DirectionalDepth,
new RenderExtentDeclaration(RenderExtentMode.AbsolutePixels, 1024, 1024, Layers: 2),
SizeBytes: 0,
RenderResourceUsage.Sampled | RenderResourceUsage.DepthAttachment,
RenderResourceLifetime.ActivePack,
EstimatedResidentBytes: 8L * 1024 * 1024)
with { Semantic = RenderResourceSemantic.DirectionalShadowDepth },
Image("volumetric", RenderResourceSemantic.VolumetricShafts,
RenderFormatClass.HdrColor, 0.25, 0.25, 2L * 1024 * 1024),
];
private static IReadOnlyList<RenderPassDeclaration> Passes() =>
[
Pass(
"directional-shadow-depth",
RenderPassSemantic.DirectionalShadowDepth,
RenderPassHook.ShadowDepthBeforeWorld,
"directional_shadow_world_opaque.vert.spv",
"directional_shadow_world_opaque.frag.spv",
[RenderSemanticInput.CameraMatrices,
RenderSemanticInput.SelectedCelestialDirectionalLight,
RenderSemanticInput.ShadowCasterTransforms, RenderSemanticInput.ActiveDayGroup,
RenderSemanticInput.Weather],
[],
["directional-shadow-depth"]),
Pass(
"sun-occlusion",
RenderPassSemantic.SunOcclusion,
RenderPassHook.AtmosphereBeforeToneMap,
"atmospheric_sun_occlusion.vert.spv",
"atmospheric_sun_occlusion.frag.spv",
[RenderSemanticInput.SceneDepth, RenderSemanticInput.SunScreenPosition,
RenderSemanticInput.ActiveDayGroup, RenderSemanticInput.Weather],
[],
["sun-mask"]),
Pass(
"sun-rays",
RenderPassSemantic.SunRays,
RenderPassHook.AtmosphereBeforeToneMap,
"atmospheric_sun_rays.vert.spv",
"atmospheric_sun_rays.frag.spv",
[RenderSemanticInput.SunScreenPosition, RenderSemanticInput.FrameTime],
["sun-mask"],
["sun-rays"]),
Pass(
"volumetric-shafts",
RenderPassSemantic.VolumetricShafts,
RenderPassHook.AtmosphereBeforeToneMap,
"atmospheric_volumetric.vert.spv",
"atmospheric_volumetric.frag.spv",
[RenderSemanticInput.SceneDepth, RenderSemanticInput.CameraMatrices,
RenderSemanticInput.SunDirection, RenderSemanticInput.DirectionalShadowMaps,
RenderSemanticInput.ActiveDayGroup, RenderSemanticInput.Weather],
["directional-shadow-depth"],
["volumetric"]),
Pass(
"bloom-downsample",
RenderPassSemantic.BloomDownsample,
RenderPassHook.AtmosphereBeforeToneMap,
"atmospheric_bloom_downsample.vert.spv",
"atmospheric_bloom_downsample.frag.spv",
[RenderSemanticInput.WorldColor],
["sun-rays", "volumetric"],
["bloom-a"]),
Pass(
"bloom-blur-horizontal",
RenderPassSemantic.BloomBlurHorizontal,
RenderPassHook.AtmosphereBeforeToneMap,
"atmospheric_bloom_blur.vert.spv",
"atmospheric_bloom_blur.frag.spv",
[RenderSemanticInput.FrameTime],
["bloom-a"],
["bloom-b"]),
Pass(
"bloom-blur-vertical",
RenderPassSemantic.BloomBlurVertical,
RenderPassHook.AtmosphereBeforeToneMap,
"atmospheric_bloom_blur.vert.spv",
"atmospheric_bloom_blur.frag.spv",
[RenderSemanticInput.FrameTime],
["bloom-b"],
["bloom-a"]),
Pass(
"filmic-composite",
RenderPassSemantic.FilmicComposite,
RenderPassHook.ToneMap,
"atmospheric_filmic.vert.spv",
"atmospheric_filmic.frag.spv",
[RenderSemanticInput.WorldColor, RenderSemanticInput.FrameTime],
["bloom-a", "sun-rays", "volumetric"],
[]),
];
private static IReadOnlyList<SceneReplayDeclaration> SceneReplays() =>
[
new SceneReplayDeclaration(
"outdoor-directional-shadow-casters",
RenderSceneReplaySemantic.OutdoorDirectionalShadowCasters,
RenderCasterClass.Terrain
| RenderCasterClass.OpaqueWorld
| RenderCasterClass.AlphaCutoutWorld
| RenderCasterClass.AnimatedOpaque
| RenderCasterClass.AnimatedAlphaCutout,
ViewCount: 4),
];
private static IReadOnlyList<PipelineVariantDeclaration> PipelineVariants() =>
[
Variant("terrain-shadow-caster", RenderPipelineVariantSemantic.TerrainDirectionalShadowCaster,
RenderPipelineBaseSemantic.Terrain,
"directional_shadow_terrain.vert.spv", "directional_shadow_terrain.frag.spv",
RenderMaterialClass.Opaque,
[RenderSemanticInput.CameraMatrices]),
Variant("world-shadow-opaque", RenderPipelineVariantSemantic.WorldOpaqueDirectionalShadowCaster,
RenderPipelineBaseSemantic.WorldMesh,
"directional_shadow_world_opaque.vert.spv", "directional_shadow_world_opaque.frag.spv",
RenderMaterialClass.Opaque | RenderMaterialClass.AnimatedOpaque,
[RenderSemanticInput.CameraMatrices, RenderSemanticInput.ShadowCasterTransforms]),
Variant("world-shadow-cutout", RenderPipelineVariantSemantic.WorldAlphaCutoutDirectionalShadowCaster,
RenderPipelineBaseSemantic.WorldMesh,
"directional_shadow_world_cutout.vert.spv", "directional_shadow_world_cutout.frag.spv",
RenderMaterialClass.AlphaCutout | RenderMaterialClass.AnimatedAlphaCutout,
[RenderSemanticInput.CameraMatrices, RenderSemanticInput.ShadowCasterTransforms]),
Variant("terrain-shadow-caster-multiview", RenderPipelineVariantSemantic.TerrainMultiviewDirectionalShadowCaster,
RenderPipelineBaseSemantic.Terrain,
"directional_shadow_terrain_multiview.vert.spv", "directional_shadow_terrain_multiview.frag.spv",
RenderMaterialClass.Opaque,
[RenderSemanticInput.CameraMatrices]),
Variant("world-shadow-opaque-multiview", RenderPipelineVariantSemantic.WorldOpaqueMultiviewDirectionalShadowCaster,
RenderPipelineBaseSemantic.WorldMesh,
"directional_shadow_world_opaque_multiview.vert.spv", "directional_shadow_world_opaque_multiview.frag.spv",
RenderMaterialClass.Opaque | RenderMaterialClass.AnimatedOpaque,
[RenderSemanticInput.CameraMatrices, RenderSemanticInput.ShadowCasterTransforms]),
Variant("world-shadow-cutout-multiview", RenderPipelineVariantSemantic.WorldAlphaCutoutMultiviewDirectionalShadowCaster,
RenderPipelineBaseSemantic.WorldMesh,
"directional_shadow_world_cutout_multiview.vert.spv", "directional_shadow_world_cutout_multiview.frag.spv",
RenderMaterialClass.AlphaCutout | RenderMaterialClass.AnimatedAlphaCutout,
[RenderSemanticInput.CameraMatrices, RenderSemanticInput.ShadowCasterTransforms]),
Variant("terrain-shadow-receiver", RenderPipelineVariantSemantic.TerrainDirectionalShadowReceiver,
RenderPipelineBaseSemantic.Terrain,
"terrain_atmospheric.vert.spv", "terrain_atmospheric.frag.spv",
RenderMaterialClass.Opaque,
[RenderSemanticInput.DirectionalShadowMaps,
RenderSemanticInput.SelectedCelestialDirectionalLight]),
Variant("world-shadow-receiver", RenderPipelineVariantSemantic.WorldDirectionalShadowReceiver,
RenderPipelineBaseSemantic.WorldMesh,
"mesh_atmospheric.vert.spv", "mesh_atmospheric.frag.spv",
RenderMaterialClass.Opaque | RenderMaterialClass.AlphaCutout
| RenderMaterialClass.AnimatedOpaque | RenderMaterialClass.AnimatedAlphaCutout,
[RenderSemanticInput.DirectionalShadowMaps,
RenderSemanticInput.SelectedCelestialDirectionalLight]),
];
private static IReadOnlyList<RenderQualityPreset> QualityPresets() =>
[
Preset("low", "Low", RenderQualitySemantic.Low,
64, 2.0, 3.0, 0.15, 0.50, 768, 2, 72, 0.25) with
{
ExecutionHints =
RenderQualityExecutionHints.MultiviewDirectionalShadowCascades,
},
Preset("medium", "Medium", RenderQualitySemantic.Medium,
128, 3.25, 4.50, 0.25, 0.75, 1536, 3, 144, 0.5),
Preset("high", "High", RenderQualitySemantic.High,
256, 4.50, 6.00, 0.35, 1.00, 2048, 4, 240, 0.5),
Preset("auto", "Auto", RenderQualitySemantic.Automatic,
128, 3.25, 4.50, 0.25, 0.75, 1536, 3, 144, 0.5)
with
{
SettingOverrides =
[
new RenderQualitySettingOverride("automatic-quality", "true"),
new RenderQualitySettingOverride("volumetric-strength", "0.35"),
new RenderQualitySettingOverride("volumetric-ray-steps", "40"),
new RenderQualitySettingOverride("sun-shadow-strength", "0.72"),
new RenderQualitySettingOverride("sun-shadow-reach-metres", "144"),
new RenderQualitySettingOverride("sun-shadow-pcf-taps", "9"),
new RenderQualitySettingOverride("sun-ray-strength", "0.55"),
],
AutoEligible = false,
},
];
private static IReadOnlyList<RenderSettingDeclaration> Settings() =>
[
Float("bloom-strength", "Bloom strength", RenderSettingSemantic.BloomStrength,
0.65, 0, 2, 0.05),
Float("filmic-strength", "Filmic tonemap strength", RenderSettingSemantic.FilmicStrength,
1.0, 0, 1, 0.05),
Float("exposure", "Exposure", RenderSettingSemantic.Exposure,
0.80, 0.25, 4, 0.05),
Float("grade-saturation", "Colour saturation", RenderSettingSemantic.GradeSaturation,
1.0, 0, 2, 0.05),
Float("grade-contrast", "Colour contrast", RenderSettingSemantic.GradeContrast,
1.0, 0.5, 2, 0.05),
Float("vignette-strength", "Vignette strength", RenderSettingSemantic.VignetteStrength,
0.12, 0, 1, 0.01),
Float("sun-ray-strength", "Sun-ray strength", RenderSettingSemantic.SunRayStrength,
0.55, 0, 2, 0.05),
Float("sun-shadow-strength", "Directional-shadow strength",
RenderSettingSemantic.DirectionalShadowStrength, 0.72, 0, 1, 0.02),
Integer("sun-shadow-reach-metres", "Directional-shadow reach (metres)",
RenderSettingSemantic.DirectionalShadowReachMetres, 240, 16, 240, 1),
Choice("sun-shadow-pcf-taps", "Directional-shadow filter taps",
RenderSettingSemantic.DirectionalShadowPcfTaps, "9", ["1", "9", "25"]),
Float("volumetric-strength", "Volumetric-shaft strength",
RenderSettingSemantic.VolumetricStrength, 0.35, 0, 1, 0.01),
Integer("volumetric-ray-steps", "Volumetric ray-march steps",
RenderSettingSemantic.VolumetricRayMarchSteps, 40, 8, 64, 8),
new RenderSettingDeclaration(
"automatic-quality",
"Automatic quality",
RenderSettingKind.Boolean,
"false",
null,
null,
null,
[])
with { Semantic = RenderSettingSemantic.AutomaticQuality },
];
private static AtmospherePolicyDeclaration AtmospherePolicy() => new(
[
new SunElevationResponsePoint(-90, 0),
new SunElevationResponsePoint(-3, 0),
new SunElevationResponsePoint(4, 1),
new SunElevationResponsePoint(22, 0.75),
new SunElevationResponsePoint(55, 0),
new SunElevationResponsePoint(90, 0),
],
[
new ActiveDayGroupMultiplier(0, 1.0),
new ActiveDayGroupMultiplier(1, 0.35),
new ActiveDayGroupMultiplier(2, 0.20),
])
{
DirectionalShadowLightElevationResponse =
[
new SunElevationResponsePoint(-90, 0),
new SunElevationResponsePoint(1, 0),
new SunElevationResponsePoint(12, 1),
new SunElevationResponsePoint(90, 1),
],
VolumetricShaftSunElevationResponse =
[
new SunElevationResponsePoint(-90, 0),
new SunElevationResponsePoint(0, 0),
new SunElevationResponsePoint(6, 1),
new SunElevationResponsePoint(18, 1),
new SunElevationResponsePoint(70, 0),
new SunElevationResponsePoint(90, 0),
],
};
private static RenderResourceDeclaration Image(
string id,
RenderResourceSemantic semantic,
RenderFormatClass format,
double widthScale,
double heightScale,
long estimatedBytes) => new RenderResourceDeclaration(
id,
RenderResourceKind.Image2D,
format,
new RenderExtentDeclaration(
RenderExtentMode.RelativeToMainWorld,
widthScale,
heightScale),
SizeBytes: 0,
RenderResourceUsage.Sampled | RenderResourceUsage.ColorAttachment,
RenderResourceLifetime.ActivePack,
estimatedBytes)
{ Semantic = semantic };
private static RenderPassDeclaration Pass(
string id,
RenderPassSemantic semantic,
RenderPassHook hook,
string vertex,
string fragment,
IReadOnlyList<RenderSemanticInput> semantics,
IReadOnlyList<string> reads,
IReadOnlyList<string> writes) =>
new(id, hook, vertex, fragment, semantics, reads, writes)
{
Semantic = semantic,
};
private static PipelineVariantDeclaration Variant(
string id,
RenderPipelineVariantSemantic variantSemantic,
RenderPipelineBaseSemantic semantic,
string vertex,
string fragment,
RenderMaterialClass materials,
IReadOnlyList<RenderSemanticInput> inputs) =>
new(id, semantic, vertex, fragment, materials, inputs)
{
Semantic = variantSemantic,
};
private static RenderQualityPreset Preset(
string id,
string displayName,
RenderQualitySemantic semantic,
long maxMiB,
double gpuP50,
double gpuP99,
double cpuP50,
double cpuP99,
int shadowResolution,
int cascades,
int shadowReachMetres,
double postScale) => new RenderQualityPreset(
id,
displayName,
semantic == RenderQualitySemantic.Low
? [RenderCapability.DirectionalShadowMaps,
RenderCapability.MultiviewDirectionalShadowCascades]
: [RenderCapability.DirectionalShadowMaps],
[
Override("directional-shadow-depth", shadowResolution, shadowResolution, cascades,
4L * shadowResolution * shadowResolution * cascades),
RelativeOverride("bloom-a", postScale),
RelativeOverride("bloom-b", postScale),
RelativeOverride("sun-mask", id == "low" ? 0.25 : 0.5),
RelativeOverride("sun-rays", id == "low" ? 0.25 : 0.5),
RelativeOverride("volumetric", id == "high" ? 0.5 : 0.25),
],
[
new RenderQualitySettingOverride("automatic-quality", "false"),
new RenderQualitySettingOverride("volumetric-strength", id == "low" ? "0" : "0.35"),
new RenderQualitySettingOverride(
"volumetric-ray-steps",
semantic switch
{
RenderQualitySemantic.Low => "24",
RenderQualitySemantic.High => "56",
_ => "40",
}),
new RenderQualitySettingOverride("sun-shadow-strength", "0.72"),
new RenderQualitySettingOverride("sun-shadow-reach-metres", shadowReachMetres.ToString()),
new RenderQualitySettingOverride(
"sun-shadow-pcf-taps",
semantic switch
{
RenderQualitySemantic.Low => "1",
RenderQualitySemantic.High => "25",
_ => "9",
}),
// The renderer recognizes this bounded preset fact; it remains
// visible here instead of becoming a hidden cascade constant.
new RenderQualitySettingOverride("sun-ray-strength", id == "low" ? "0.4" : "0.55"),
],
maxMiB * 1024 * 1024,
gpuP50,
gpuP99,
cpuP50,
cpuP99)
{ Semantic = semantic };
private static RenderQualityResourceOverride Override(
string id,
int width,
int height,
int layers,
long bytes) => new(
id,
new RenderExtentDeclaration(RenderExtentMode.AbsolutePixels, width, height, layers),
SizeBytes: 0,
EstimatedResidentBytes: bytes);
private static RenderQualityResourceOverride RelativeOverride(string id, double scale) =>
new(
id,
new RenderExtentDeclaration(RenderExtentMode.RelativeToMainWorld, scale, scale),
SizeBytes: 0,
EstimatedResidentBytes: 0);
private static RenderSettingDeclaration Float(
string id,
string displayName,
RenderSettingSemantic semantic,
double defaultValue,
double min,
double max,
double step) => new RenderSettingDeclaration(
id,
displayName,
RenderSettingKind.Float,
defaultValue.ToString(System.Globalization.CultureInfo.InvariantCulture),
min,
max,
step,
[])
{ Semantic = semantic };
private static RenderSettingDeclaration Integer(
string id,
string displayName,
RenderSettingSemantic semantic,
int defaultValue,
int min,
int max,
int step) => new RenderSettingDeclaration(
id,
displayName,
RenderSettingKind.Integer,
defaultValue.ToString(System.Globalization.CultureInfo.InvariantCulture),
min,
max,
step,
[])
{ Semantic = semantic };
private static RenderSettingDeclaration Choice(
string id,
string displayName,
RenderSettingSemantic semantic,
string defaultValue,
IReadOnlyList<string> choices) => new RenderSettingDeclaration(
id,
displayName,
RenderSettingKind.Choice,
defaultValue,
null,
null,
null,
choices)
{ Semantic = semantic };
}
internal sealed class DirectoryRenderPackAssets : IRenderPackAssets
{
private readonly string _root;
internal DirectoryRenderPackAssets(string root)
{
ArgumentException.ThrowIfNullOrWhiteSpace(root);
_root = Path.GetFullPath(root);
}
public Stream OpenRead(string assetKey)
{
ArgumentException.ThrowIfNullOrWhiteSpace(assetKey);
string normalized = assetKey.Replace('/', Path.DirectorySeparatorChar);
string path = Path.GetFullPath(Path.Combine(_root, normalized));
string relative = Path.GetRelativePath(_root, path);
if (Path.IsPathRooted(relative)
|| relative == ".."
|| relative.StartsWith($"..{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
throw new UnauthorizedAccessException("The asset key escapes the render-pack root.");
return File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
}
}

View file

@ -0,0 +1,965 @@
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 = Math.Min(
Preset.MaxResidentGpuBytes,
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);
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);
}

View file

@ -0,0 +1,87 @@
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.App.Rendering.Packs;
/// <summary>
/// Host evaluation for the public data-only atmosphere curves. Keeping the
/// three interpolation contracts here prevents a pack declaration from being
/// reinterpreted differently by the declared, shadow, and volumetric graphs.
/// </summary>
internal static class RenderPackAtmospherePolicyEvaluation
{
internal static DirectionalShadowAtmospherePolicy NeutralDirectionalShadowElevation { get; } =
DirectionalShadowAtmospherePolicy.BuiltIn with
{
MinimumLightElevationSin = -1.001f,
FullStrengthLightElevationSin = -1f,
};
internal static float Ray(
IReadOnlyList<SunElevationResponsePoint>? points,
float elevationDegrees,
float fallback = 1f) => Evaluate(
points,
elevationDegrees,
static value => (float)value,
static value => (float)value,
fallback);
internal static float DirectionalShadow(
IReadOnlyList<SunElevationResponsePoint>? points,
float elevationDegrees,
float fallback = 0f) => DirectionalShadowFromSin(
points,
MathF.Sin(elevationDegrees * (MathF.PI / 180f)),
fallback);
internal static float DirectionalShadowFromSin(
IReadOnlyList<SunElevationResponsePoint>? points,
float lightElevationSin,
float fallback = 0f) => Evaluate(
points,
Math.Clamp(lightElevationSin, -1f, 1f),
static degrees => MathF.Sin((float)degrees * (MathF.PI / 180f)),
static value => (float)value,
fallback);
internal static float VolumetricShaft(
IReadOnlyList<SunElevationResponsePoint>? points,
float elevationDegrees,
float fallback = 0f) => Evaluate(
points,
elevationDegrees,
static value => (float)value,
static value => value * value * (3f - (2f * value)),
fallback);
private static float Evaluate(
IReadOnlyList<SunElevationResponsePoint>? points,
float input,
Func<double, float> transformPoint,
Func<float, float> transformInterpolation,
float fallback)
{
if (points is null || points.Count == 0)
return fallback;
float first = transformPoint(points[0].ElevationDegrees);
if (input <= first)
return (float)points[0].Multiplier;
for (int i = 1; i < points.Count; i++)
{
SunElevationResponsePoint upper = points[i];
float upperInput = transformPoint(upper.ElevationDegrees);
if (input > upperInput)
continue;
SunElevationResponsePoint lower = points[i - 1];
float lowerInput = transformPoint(lower.ElevationDegrees);
float span = upperInput - lowerInput;
float t = span <= 0f
? 0f
: Math.Clamp((input - lowerInput) / span, 0f, 1f);
t = transformInterpolation(t);
return (float)(lower.Multiplier
+ ((upper.Multiplier - lower.Multiplier) * t));
}
return (float)points[^1].Multiplier;
}
}

View file

@ -0,0 +1,66 @@
using AcDream.App.Rendering.Gpu;
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.App.Rendering.Packs;
internal static class RenderPackCapabilityResolver
{
internal const long AbsoluteResidentByteCeiling = 256L * 1024 * 1024;
internal const long AbsoluteTransientByteCeiling = 512L * 1024 * 1024;
internal const int DeviceLocalShareDenominator = 8;
internal static RenderPackHostCapabilities Resolve(GpuCapabilityRecord gpu)
{
ArgumentNullException.ThrowIfNull(gpu);
var available = new HashSet<RenderCapability>
{
RenderCapability.FullscreenPasses,
RenderCapability.AuthoredSunDirection,
RenderCapability.AuthoredCelestialDirectionalLight,
RenderCapability.AuthoredSunScreenPosition,
RenderCapability.AuthoredWeather,
RenderCapability.OutdoorDirectionalShadowCasterReplay,
RenderCapability.AnimatedCasterTransforms,
RenderCapability.AlphaCutoutShadowCasters,
};
if (gpu.SupportsRgba16FloatRenderTargets)
available.Add(RenderCapability.MainWorldColorIntermediate);
if (gpu.SupportsSampledDepth)
{
available.Add(RenderCapability.SceneDepthSampling);
available.Add(RenderCapability.DirectionalShadowMaps);
}
if (gpu.SupportsTimestampQueries)
available.Add(RenderCapability.GpuTimestampQueries);
if (gpu.SupportsMultiview)
available.Add(RenderCapability.MultiviewDirectionalShadowCascades);
long residentBytes = DeviceLocalShare(
gpu.DeviceLocalMemoryBytes,
AbsoluteResidentByteCeiling);
long transientBytes = DeviceLocalShare(
gpu.DeviceLocalMemoryBytes,
AbsoluteTransientByteCeiling);
return new RenderPackHostCapabilities(
available,
MaxImageDimension2D: checked((int)Math.Min(
gpu.MaxImageDimension2D,
(uint)int.MaxValue)),
MaxImageArrayLayers: checked((int)Math.Min(
gpu.MaxImageArrayLayers,
(uint)int.MaxValue)),
MaxPackResidentBytes: residentBytes,
MaxPackTransientBytes: transientBytes,
MemoryPolicyDescription:
$"one eighth of {gpu.DeviceLocalMemoryBytes} device-local bytes, "
+ $"capped at {AbsoluteResidentByteCeiling} resident and "
+ $"{AbsoluteTransientByteCeiling} transient bytes");
}
private static long DeviceLocalShare(ulong deviceLocalBytes, long ceiling)
{
ulong share = deviceLocalBytes / DeviceLocalShareDenominator;
return (long)Math.Min(share, checked((ulong)ceiling));
}
}

View file

@ -0,0 +1,37 @@
using AcDream.App.Plugins;
namespace AcDream.App.Rendering.Packs;
/// <summary>
/// Production catalog authority shared by retained UI and the activation
/// controller. It deliberately retains no catalog snapshot (and therefore no
/// plugin asset source): withdrawal immediately releases the registry's last
/// catalog reference, while consumers rebuild only after the revision event or
/// an explicit UI interaction.
/// </summary>
internal sealed class RenderPackCatalogSource
{
private readonly BufferedRenderPackRegistry _registry;
private readonly RenderPackHostCapabilities _capabilities;
internal RenderPackCatalogSource(
BufferedRenderPackRegistry registry,
RenderPackHostCapabilities capabilities)
{
_registry = registry ?? throw new ArgumentNullException(nameof(registry));
_capabilities = capabilities
?? throw new ArgumentNullException(nameof(capabilities));
}
internal long Revision => _registry.Revision;
internal event Action<long> Changed
{
add => _registry.Changed += value;
remove => _registry.Changed -= value;
}
internal RenderPackCatalog Snapshot() => RenderPackCatalog.Build(
_registry.Snapshot(),
_capabilities);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,284 @@
using System.Numerics;
namespace AcDream.App.Rendering.Packs;
internal readonly record struct RenderPackPassDiagnostics(
string PassId,
double GpuMilliseconds,
int DrawCalls,
int DispatchCalls);
/// <summary>
/// Pack-owned facts sampled after a successful frame. Implementations expose
/// already-resolved asynchronous timestamp results; capturing this value must
/// never wait for the GPU.
/// </summary>
internal sealed record RenderPackRuntimeDiagnostics(
string EffectiveQuality,
long RetainedGpuBytes,
long TransientGpuBytes,
int ImageCount,
int BufferCount,
int DrawCalls,
int DispatchCalls,
int ShadowCasterCount,
int CascadeDrawCount,
int CpuClassificationCalls,
double SunElevationDegrees,
int ActiveDayGroup,
string Weather,
double WeatherIntensity,
bool Outdoor,
double DirectionalShadowStrength,
IReadOnlyList<RenderPackPassDiagnostics> Passes)
{
/// <summary>
/// Number of matrices addressed through the one shared world-transform
/// binding after the enhanced world receiver has appended its ordinary
/// draws to the directional-shadow prefix. Zero means that no shared
/// directional-shadow frame was active for the sampled frame.
/// </summary>
public uint SharedWorldTransformUsedInstances { get; init; }
public IReadOnlyList<RenderPackCpuStageDiagnostics> CpuStages { get; init; } = [];
public AuthoredCelestialShadowSourceKind DirectionalShadowSourceKind
{
get;
init;
}
public int DirectionalShadowSourceObjectIndex { get; init; } = -1;
public uint DirectionalShadowSourceGfxObjId { get; init; }
public Vector3 DirectionalShadowSurfaceToLightDirection { get; init; }
public float DirectionalShadowLightElevationSin { get; init; }
public DirectionalShadowTransformChurnDiagnostics ShadowTransformChurn
{
get;
init;
}
internal static RenderPackRuntimeDiagnostics Empty(string quality) => new(
quality,
RetainedGpuBytes: 0,
TransientGpuBytes: 0,
ImageCount: 0,
BufferCount: 0,
DrawCalls: 0,
DispatchCalls: 0,
ShadowCasterCount: 0,
CascadeDrawCount: 0,
CpuClassificationCalls: 0,
SunElevationDegrees: 0,
ActiveDayGroup: -1,
Weather: "unknown",
WeatherIntensity: 0,
Outdoor: false,
DirectionalShadowStrength: 0,
Passes: []);
}
internal interface IRenderPackRuntimeDiagnosticsSource
{
RenderPackRuntimeDiagnostics CaptureDiagnostics();
}
internal sealed record RenderPackDiagnosticsSnapshot(
RenderPackActivationState State,
string PackId,
string? PackVersion,
string PresetId,
string EffectiveQuality,
string? FailureReason,
long ActivationGeneration,
long RetainedGpuBytes,
long TransientGpuBytes,
int ImageCount,
int BufferCount,
int DrawCalls,
int DispatchCalls,
int ShadowCasterCount,
int CascadeDrawCount,
int CpuClassificationCalls,
double SunElevationDegrees,
int ActiveDayGroup,
string Weather,
double WeatherIntensity,
bool Outdoor,
double DirectionalShadowStrength,
IReadOnlyList<RenderPackPassDiagnostics> Passes,
RenderPackPerformanceSnapshot Performance = default)
{
public uint SharedWorldTransformUsedInstances { get; init; }
public IReadOnlyList<RenderPackCpuStageDiagnostics> CpuStages { get; init; } = [];
public AuthoredCelestialShadowSourceKind DirectionalShadowSourceKind
{
get;
init;
}
public int DirectionalShadowSourceObjectIndex { get; init; } = -1;
public uint DirectionalShadowSourceGfxObjId { get; init; }
public Vector3 DirectionalShadowSurfaceToLightDirection { get; init; }
public float DirectionalShadowLightElevationSin { get; init; }
public DirectionalShadowTransformChurnDiagnostics ShadowTransformChurn
{
get;
init;
}
internal static RenderPackDiagnosticsSnapshot Retail { get; } = new(
RenderPackActivationState.Retail,
PackId: "retail",
PackVersion: null,
PresetId: "off",
EffectiveQuality: "off",
FailureReason: null,
ActivationGeneration: 0,
RetainedGpuBytes: 0,
TransientGpuBytes: 0,
ImageCount: 0,
BufferCount: 0,
DrawCalls: 0,
DispatchCalls: 0,
ShadowCasterCount: 0,
CascadeDrawCount: 0,
CpuClassificationCalls: 0,
SunElevationDegrees: 0,
ActiveDayGroup: -1,
Weather: "unknown",
WeatherIntensity: 0,
Outdoor: false,
DirectionalShadowStrength: 0,
Passes: []);
internal bool IsRetail =>
string.Equals(PackId, "retail", StringComparison.Ordinal);
}
internal interface IRenderPackDiagnosticsSnapshotSource
{
RenderPackDiagnosticsSnapshot CaptureDiagnostics();
}
/// <summary>
/// Construction-order bridge used by screenshot and retained-UI diagnostics.
/// Until the render-thread controller is composed, it reports the exact
/// resource-free retail selection.
/// </summary>
internal sealed class DeferredRenderPackDiagnosticsSource
: IRenderPackDiagnosticsSnapshotSource
{
private IRenderPackDiagnosticsSnapshotSource? _target;
public RenderPackDiagnosticsSnapshot CaptureDiagnostics() =>
_target?.CaptureDiagnostics() ?? RenderPackDiagnosticsSnapshot.Retail;
internal IDisposable BindOwned(IRenderPackDiagnosticsSnapshotSource target)
{
ArgumentNullException.ThrowIfNull(target);
if (_target is not null && !ReferenceEquals(_target, target))
throw new InvalidOperationException("Render-pack diagnostics are already bound.");
_target = target;
return new Binding(this, target);
}
private void Unbind(IRenderPackDiagnosticsSnapshotSource target)
{
if (ReferenceEquals(_target, target))
_target = null;
}
private sealed class Binding(
DeferredRenderPackDiagnosticsSource owner,
IRenderPackDiagnosticsSnapshotSource target) : IDisposable
{
private DeferredRenderPackDiagnosticsSource? _owner = owner;
public void Dispose() =>
Interlocked.Exchange(ref _owner, null)?.Unbind(target);
}
}
internal static class RenderPackDiagnosticsFormatter
{
internal static string Format(RenderPackDiagnosticsSnapshot value) =>
$"[render-pack] state={value.State} "
+ $"pack={value.PackId}@{value.PackVersion ?? "(missing)"} "
+ $"preset={value.PresetId} effective={value.EffectiveQuality} "
+ $"generation={value.ActivationGeneration} "
+ $"gpuBytes={value.RetainedGpuBytes}/{value.TransientGpuBytes} "
+ $"resources={value.ImageCount}i/{value.BufferCount}b "
+ $"submit={value.DrawCalls}d/{value.DispatchCalls}c "
+ $"worldTransforms={value.SharedWorldTransformUsedInstances}used "
+ $"shadow={value.ShadowCasterCount}casters/{value.CascadeDrawCount}cascadeDraws/"
+ $"{value.CpuClassificationCalls}classify "
+ $"shadowSource={value.DirectionalShadowSourceKind}/"
+ $"obj{value.DirectionalShadowSourceObjectIndex}/"
+ $"0x{value.DirectionalShadowSourceGfxObjId:X8}/"
+ $"dir({Invariant(value.DirectionalShadowSurfaceToLightDirection.X, "F4")},"
+ $"{Invariant(value.DirectionalShadowSurfaceToLightDirection.Y, "F4")},"
+ $"{Invariant(value.DirectionalShadowSurfaceToLightDirection.Z, "F4")})/"
+ $"elevSin={Invariant(value.DirectionalShadowLightElevationSin, "F4")} "
+ $"atmosphere={Invariant(value.SunElevationDegrees, "F2")}deg/day{value.ActiveDayGroup}/"
+ $"{value.Weather}:{Invariant(value.WeatherIntensity, "F3")}/outdoor={value.Outdoor}/"
+ $"shadowStrength={Invariant(value.DirectionalShadowStrength, "F3")} "
+ $"perf=cpu-added:{Invariant(value.Performance.IncrementalCpuMillisecondsP50, "F3")}/"
+ $"{Invariant(value.Performance.IncrementalCpuMillisecondsP95, "F3")}/"
+ $"{Invariant(value.Performance.IncrementalCpuMillisecondsP99, "F3")}ms,"
+ $"receiver-cpu-absolute:{Invariant(value.Performance.AbsoluteReceiverCpuMillisecondsP50, "F3")}/"
+ $"{Invariant(value.Performance.AbsoluteReceiverCpuMillisecondsP95, "F3")}/"
+ $"{Invariant(value.Performance.AbsoluteReceiverCpuMillisecondsP99, "F3")}ms,"
+ $"gpu-inclusive:{Invariant(value.Performance.InclusiveGpuMillisecondsP50, "F3")}/"
+ $"{Invariant(value.Performance.InclusiveGpuMillisecondsP95, "F3")}/"
+ $"{Invariant(value.Performance.InclusiveGpuMillisecondsP99, "F3")}ms "
+ $"passes={FormatPasses(value.Passes)} "
+ $"cpuStages={FormatCpuStages(value.CpuStages)} "
+ $"shadowTransformChurn={FormatShadowTransformChurn(value.ShadowTransformChurn)} "
+ $"reason={value.FailureReason ?? "none"}";
private static string FormatShadowTransformChurn(
DirectionalShadowTransformChurnDiagnostics value) =>
$"scene={value.CopiedSceneChanges}[transform={value.UpdateTransformChanges},"
+ $"appearance={value.UpdateAppearanceChanges},sync={value.DynamicSynchronizationChanges};"
+ $"animated={value.ActiveAnimatedStaticChanges},live={value.LiveDynamicRootChanges},"
+ $"equipped={value.EquippedChildChanges}]/"
+ $"casters={value.DedupedCasterSlots}/sceneFallback={value.SceneJournalFullRefresh}/"
+ $"densityBulk={value.DensityBulkRefresh}/batchCopies={value.BatchedProjectionCopyCalls}/"
+ $"matrices={value.ChangedMatrixSlots}/flightCurrent={value.FlightCurrentChangedMatrices}/"
+ $"flightReplay={value.FlightPendingReplayMatrices}/uploaded={value.FlightUploadedMatrices}/"
+ $"ranges={value.FlightUploadRanges}/bytes={value.FlightBytesWritten}/"
+ $"flightFallback={value.FlightFullDynamicFallback}/denseDirect={value.DenseDirectUpload}/"
+ $"denseReplay={value.DenseFlightReplay}/"
+ $"classes=[terrain={value.CasterClasses.TerrainCommands},"
+ $"outdoorStatic={value.CasterClasses.OutdoorStatics},"
+ $"building={value.CasterClasses.Buildings},"
+ $"animated={value.CasterClasses.AnimatedStatics},"
+ $"localPlayer={value.CasterClasses.LocalPlayers},"
+ $"remotePlayer={value.CasterClasses.RemotePlayers},"
+ $"nonPlayerCreature={value.CasterClasses.NonPlayerCreatures},"
+ $"otherLive={value.CasterClasses.OtherLiveDynamics},"
+ $"equipped={value.CasterClasses.EquippedChildren}]";
private static string FormatPasses(IReadOnlyList<RenderPackPassDiagnostics> passes) =>
passes.Count == 0
? "none"
: string.Join(
',',
passes.Select(static pass =>
$"{pass.PassId}:{Invariant(pass.GpuMilliseconds, "F3")}ms/"
+ $"{pass.DrawCalls}d/{pass.DispatchCalls}c"));
private static string FormatCpuStages(
IReadOnlyList<RenderPackCpuStageDiagnostics> stages) =>
stages.Count == 0
? "none"
: string.Join(
',',
stages.Select(static stage =>
$"{stage.Stage}:{stage.SampleCount}n/"
+ $"{Invariant(stage.CpuMillisecondsP50, "F3")}/"
+ $"{Invariant(stage.CpuMillisecondsP95, "F3")}/"
+ $"{Invariant(stage.CpuMillisecondsP99, "F3")}ms"));
private static string Invariant(double value, string format) =>
value.ToString(format, System.Globalization.CultureInfo.InvariantCulture);
}

View file

@ -0,0 +1,166 @@
using AcDream.App.Diagnostics;
namespace AcDream.App.Rendering.Packs;
internal readonly record struct RenderPackPerformanceSnapshot(
int CpuSampleCount,
int AbsoluteReceiverCpuSampleCount,
int GpuSampleCount,
double IncrementalCpuMillisecondsP50,
double IncrementalCpuMillisecondsP95,
double IncrementalCpuMillisecondsP99,
double AbsoluteReceiverCpuMillisecondsP50,
double AbsoluteReceiverCpuMillisecondsP95,
double AbsoluteReceiverCpuMillisecondsP99,
double InclusiveGpuMillisecondsP50,
double InclusiveGpuMillisecondsP95,
double InclusiveGpuMillisecondsP99,
long ResidentGpuBytes,
long TransientGpuBytes)
{
internal bool HasStableAutoWindow(int minimumSamples) =>
minimumSamples > 0
&& CpuSampleCount >= minimumSamples
&& GpuSampleCount >= minimumSamples;
}
/// <summary>
/// Allocation-free facts captured from the active runtime after it has
/// submitted one complete frame. GPU time is the inclusive sum of already-
/// resolved asynchronous pack timers, including the enhanced-world receiver
/// pass; this contract never waits for the device.
/// </summary>
internal readonly record struct RenderPackRuntimePerformanceMetrics(
long ResourceGeneration,
bool HasResolvedGpuMeasurement,
double InclusiveResolvedGpuMilliseconds,
long RetainedGpuBytes,
long TransientGpuBytes);
internal interface IRenderPackRuntimePerformanceSource
{
RenderPackRuntimePerformanceMetrics CapturePerformanceMetrics();
}
internal readonly record struct RenderPackFramePerformanceObservation(
double PackAddedCpuMilliseconds,
bool StableFrameBoundary,
int ViewportWidth,
int ViewportHeight,
int SampleCount,
double AbsoluteEnhancedWorldReceiverCpuMilliseconds = 0d);
internal static class RenderPackPerformanceScopeNames
{
/// <summary>
/// The enhanced main-world pass uses the pack's receiver pipelines. Its
/// timestamp is intentionally part of the same total consumed by
/// diagnostics and Auto; measuring only the extra shadow/post passes would
/// hide the receiver shader's GPU cost.
/// </summary>
internal const string EnhancedWorldReceiver = "atmospheric-world-receiver";
}
/// <summary>
/// Allocation-free rolling evidence for one active pack runtime. Incremental
/// CPU samples bracket only work added by the pack. The complete enhanced-world
/// receiver recording is retained as a separate absolute diagnostic because it
/// is not an incremental delta and must never be compared with the pack's
/// incremental CPU budget. GPU samples are the already-resolved asynchronous
/// total including the receiver pass for the frame that issued them. The owner
/// resets this window on activation or quality generation changes so Auto can
/// never compare measurements from mixed resource layouts.
/// </summary>
internal sealed class RenderPackPerformanceWindow
{
internal const int DefaultCapacity = 2048;
private readonly FrameStatsBuffer _cpuMicroseconds;
private readonly FrameStatsBuffer _absoluteReceiverCpuMicroseconds;
private readonly FrameStatsBuffer _gpuMicroseconds;
private long _residentGpuBytes;
private long _transientGpuBytes;
internal RenderPackPerformanceWindow(int capacity = DefaultCapacity)
{
if (capacity <= 0)
throw new ArgumentOutOfRangeException(nameof(capacity));
_cpuMicroseconds = new FrameStatsBuffer(capacity);
_absoluteReceiverCpuMicroseconds = new FrameStatsBuffer(capacity);
_gpuMicroseconds = new FrameStatsBuffer(capacity);
}
internal void Observe(
double incrementalCpuMilliseconds,
double absoluteReceiverCpuMilliseconds,
bool hasResolvedGpuMeasurement,
double inclusiveResolvedGpuMilliseconds,
long residentGpuBytes,
long transientGpuBytes)
{
if (!double.IsFinite(incrementalCpuMilliseconds) || incrementalCpuMilliseconds < 0d)
throw new ArgumentOutOfRangeException(nameof(incrementalCpuMilliseconds));
if (!double.IsFinite(absoluteReceiverCpuMilliseconds)
|| absoluteReceiverCpuMilliseconds < 0d)
{
throw new ArgumentOutOfRangeException(nameof(absoluteReceiverCpuMilliseconds));
}
if (hasResolvedGpuMeasurement
&& (!double.IsFinite(inclusiveResolvedGpuMilliseconds)
|| inclusiveResolvedGpuMilliseconds < 0d))
{
throw new ArgumentOutOfRangeException(nameof(inclusiveResolvedGpuMilliseconds));
}
if (residentGpuBytes < 0)
throw new ArgumentOutOfRangeException(nameof(residentGpuBytes));
if (transientGpuBytes < 0)
throw new ArgumentOutOfRangeException(nameof(transientGpuBytes));
_cpuMicroseconds.Push(ToMicroseconds(incrementalCpuMilliseconds));
_absoluteReceiverCpuMicroseconds.Push(
ToMicroseconds(absoluteReceiverCpuMilliseconds));
if (hasResolvedGpuMeasurement)
_gpuMicroseconds.Push(ToMicroseconds(inclusiveResolvedGpuMilliseconds));
_residentGpuBytes = residentGpuBytes;
_transientGpuBytes = transientGpuBytes;
}
internal RenderPackPerformanceSnapshot Snapshot() => new(
_cpuMicroseconds.Count,
_absoluteReceiverCpuMicroseconds.Count,
_gpuMicroseconds.Count,
ToMilliseconds(_cpuMicroseconds.Percentile(0.50)),
ToMilliseconds(_cpuMicroseconds.Percentile(0.95)),
ToMilliseconds(_cpuMicroseconds.Percentile(0.99)),
ToMilliseconds(_absoluteReceiverCpuMicroseconds.Percentile(0.50)),
ToMilliseconds(_absoluteReceiverCpuMicroseconds.Percentile(0.95)),
ToMilliseconds(_absoluteReceiverCpuMicroseconds.Percentile(0.99)),
ToMilliseconds(_gpuMicroseconds.Percentile(0.50)),
ToMilliseconds(_gpuMicroseconds.Percentile(0.95)),
ToMilliseconds(_gpuMicroseconds.Percentile(0.99)),
_residentGpuBytes,
_transientGpuBytes);
internal int MinimumSampleCount => Math.Min(
_cpuMicroseconds.Count,
Math.Min(
_absoluteReceiverCpuMicroseconds.Count,
_gpuMicroseconds.Count));
internal void Reset()
{
_cpuMicroseconds.Reset();
_absoluteReceiverCpuMicroseconds.Reset();
_gpuMicroseconds.Reset();
_residentGpuBytes = 0;
_transientGpuBytes = 0;
}
private static long ToMicroseconds(double milliseconds) =>
checked((long)Math.Round(
milliseconds * 1000d,
MidpointRounding.AwayFromZero));
private static double ToMilliseconds(long microseconds) =>
microseconds / 1000d;
}

View file

@ -0,0 +1,49 @@
namespace AcDream.App.Rendering.Packs;
/// <summary>
/// Schedules one complete, unpublished render-pack candidate preparation.
/// Production uses the worker scheduler so shader I/O/validation and Vulkan
/// resource creation cannot block the render-frame boundary. Tests can inject
/// a deterministic scheduler without adding sleeps or timing races.
/// </summary>
internal interface IRenderPackPreparationScheduler
{
Task Schedule(Action preparation);
}
internal sealed class ThreadPoolRenderPackPreparationScheduler :
IRenderPackPreparationScheduler
{
internal static ThreadPoolRenderPackPreparationScheduler Instance { get; } = new();
private ThreadPoolRenderPackPreparationScheduler()
{
}
public Task Schedule(Action preparation)
{
ArgumentNullException.ThrowIfNull(preparation);
return Task.Run(preparation);
}
}
/// <summary>
/// Synchronous fixture scheduler. Production composition must use
/// <see cref="ThreadPoolRenderPackPreparationScheduler"/>.
/// </summary>
internal sealed class InlineRenderPackPreparationScheduler :
IRenderPackPreparationScheduler
{
internal static InlineRenderPackPreparationScheduler Instance { get; } = new();
private InlineRenderPackPreparationScheduler()
{
}
public Task Schedule(Action preparation)
{
ArgumentNullException.ThrowIfNull(preparation);
preparation();
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,124 @@
using AcDream.App.Rendering.Wb;
namespace AcDream.App.Rendering.Packs;
/// <summary>
/// One complete, unpublished receiver-pipeline product. Candidate resources
/// stay owned here until the render-pack controller commits them at a stable
/// frame boundary.
/// </summary>
internal interface IRenderPackReceiverPipelineCandidate : IDisposable
{
}
internal interface IRenderPackReceiverPipelineCoordinator
{
IRenderPackReceiverPipelineCandidate Prepare(
IDirectionalShadowReceiverSource? source,
int sampleCount);
void Publish(IRenderPackReceiverPipelineCandidate candidate);
void Clear();
}
/// <summary>
/// Couples terrain and world-mesh receiver pipelines into the same activation
/// transaction as their producing render-pack runtime. Preparation may compile
/// pipelines, publication only swaps already-complete state objects, and old
/// pipelines retire after both renderer owners point at the new generation.
/// </summary>
internal sealed class RenderPackReceiverPipelineCoordinator(
TerrainModernRenderer terrain,
WbDrawDispatcher worldMeshes) : IRenderPackReceiverPipelineCoordinator
{
private readonly TerrainModernRenderer _terrain = terrain
?? throw new ArgumentNullException(nameof(terrain));
private readonly WbDrawDispatcher _worldMeshes = worldMeshes
?? throw new ArgumentNullException(nameof(worldMeshes));
public IRenderPackReceiverPipelineCandidate Prepare(
IDirectionalShadowReceiverSource? source,
int sampleCount)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(sampleCount);
TerrainModernRenderer.DirectionalShadowReceiverPipelineState? terrainState =
_terrain.PrepareDirectionalShadowReceiver(source, sampleCount);
try
{
WbDrawDispatcher.DirectionalShadowReceiverPipelineState? worldState =
_worldMeshes.PrepareDirectionalShadowReceiver(source, sampleCount);
return new Candidate(this, terrainState, worldState);
}
catch
{
terrainState?.Dispose();
throw;
}
}
public void Publish(IRenderPackReceiverPipelineCandidate candidate)
{
ArgumentNullException.ThrowIfNull(candidate);
if (candidate is not Candidate prepared || !ReferenceEquals(prepared.Owner, this))
throw new ArgumentException("Receiver candidate belongs to another coordinator.", nameof(candidate));
(TerrainModernRenderer.DirectionalShadowReceiverPipelineState? terrainState,
WbDrawDispatcher.DirectionalShadowReceiverPipelineState? worldState) = prepared.Take();
TerrainModernRenderer.DirectionalShadowReceiverPipelineState? oldTerrain =
_terrain.SwapDirectionalShadowReceiver(terrainState);
WbDrawDispatcher.DirectionalShadowReceiverPipelineState? oldWorld =
_worldMeshes.SwapDirectionalShadowReceiver(worldState);
// Vulkan pipeline disposal is flight-fence retirement. Do this only
// after both owners publish the complete new generation.
oldTerrain?.Dispose();
oldWorld?.Dispose();
}
public void Clear()
{
TerrainModernRenderer.DirectionalShadowReceiverPipelineState? oldTerrain =
_terrain.SwapDirectionalShadowReceiver(null);
WbDrawDispatcher.DirectionalShadowReceiverPipelineState? oldWorld =
_worldMeshes.SwapDirectionalShadowReceiver(null);
oldTerrain?.Dispose();
oldWorld?.Dispose();
}
private sealed class Candidate(
RenderPackReceiverPipelineCoordinator owner,
TerrainModernRenderer.DirectionalShadowReceiverPipelineState? terrain,
WbDrawDispatcher.DirectionalShadowReceiverPipelineState? world) :
IRenderPackReceiverPipelineCandidate
{
private TerrainModernRenderer.DirectionalShadowReceiverPipelineState? _terrain = terrain;
private WbDrawDispatcher.DirectionalShadowReceiverPipelineState? _world = world;
private bool _taken;
internal RenderPackReceiverPipelineCoordinator Owner { get; } = owner;
internal (TerrainModernRenderer.DirectionalShadowReceiverPipelineState?,
WbDrawDispatcher.DirectionalShadowReceiverPipelineState?) Take()
{
ObjectDisposedException.ThrowIf(_taken, this);
_taken = true;
TerrainModernRenderer.DirectionalShadowReceiverPipelineState? terrainState = _terrain;
WbDrawDispatcher.DirectionalShadowReceiverPipelineState? worldState = _world;
_terrain = null;
_world = null;
return (terrainState, worldState);
}
public void Dispose()
{
if (_taken)
return;
_taken = true;
_terrain?.Dispose();
_world?.Dispose();
_terrain = null;
_world = null;
}
}
}

View file

@ -0,0 +1,270 @@
using AcDream.Plugin.Abstractions.Rendering;
using AcDream.App.Rendering.Wb;
namespace AcDream.App.Rendering.Packs;
internal readonly record struct RenderPackResourceBudget(
long RetainedGpuBytes,
long MultisampleGpuBytes,
int LargestImageWidth,
int LargestImageHeight,
int LargestImageLayerCount)
{
internal long TotalGpuBytes => checked(RetainedGpuBytes + MultisampleGpuBytes);
}
/// <summary>
/// Resolves declaration extents against the real main-world size before an
/// executor allocates any size-dependent image. Declared byte estimates are
/// useful during discovery, but cannot prove a 1080p/1440p/4K preset ceiling.
/// This is the allocation-time authority for the images API-v1 executors
/// actually keep alive.
/// </summary>
internal static class RenderPackResourceBudgetPlanner
{
private const int HdrColorBytesPerPixel = 8;
private const int LdrColorBytesPerPixel = 4;
private const int DirectionalDepthBytesPerPixel = 4;
private const int MainWorldDepthBytesPerPixel = 4;
// Production Vulkan owns two frame-flight slots. Directional shadows
// materialize one shared demand-growth N.5 transform arena in each slot before an
// ordinary world frame can consume the pack, so admission must include
// those mandatory buffers rather than discovering them after the first
// shadow pass has already published a borrow.
private const int DirectionalShadowTransformFlightSlots = 2;
internal static RenderPackResourceBudget Resolve(
RenderPackDescriptor descriptor,
RenderQualityPreset preset,
int mainWorldWidth,
int mainWorldHeight,
int sampleCount)
{
ArgumentNullException.ThrowIfNull(descriptor);
ArgumentNullException.ThrowIfNull(preset);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(mainWorldWidth);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(mainWorldHeight);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(sampleCount);
// Every executable graph replaces the main world attachment with one
// RGBA16F colour image and one D24S8 depth image. The resolve images
// remain alive for the complete active target set.
long mainPixels = checked((long)mainWorldWidth * mainWorldHeight);
long retained = checked(mainPixels
* (HdrColorBytesPerPixel + MainWorldDepthBytesPerPixel));
long multisample = sampleCount > 1
? checked(mainPixels
* (HdrColorBytesPerPixel + MainWorldDepthBytesPerPixel)
* sampleCount)
: 0L;
int largestWidth = mainWorldWidth;
int largestHeight = mainWorldHeight;
int largestLayers = 1;
HashSet<string> writtenResources = descriptor.Passes
.SelectMany(static pass => pass.ResourceWrites)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (RenderResourceDeclaration resource in descriptor.Resources)
{
if (resource.Semantic == RenderResourceSemantic.MainWorldHdr
|| !writtenResources.Contains(resource.Id))
{
continue;
}
if (UsesFusedAtmosphericPostProcess(preset)
&& resource.Semantic is RenderResourceSemantic.BloomPing
or RenderResourceSemantic.BloomPong)
{
// The fused Low filmic shader evaluates the declared bloom
// extraction/filter directly from world colour + sun rays.
// These ping/pong images have no executing writer or reader.
continue;
}
if (resource.Kind is not RenderResourceKind.Image2D
and not RenderResourceKind.Image2DArray)
{
throw new NotSupportedException(
$"Resource '{resource.Id}' is not an API-v1 image resource.");
}
RenderQualityResourceOverride? resourceOverride = preset.ResourceOverrides
.FirstOrDefault(value => string.Equals(
value.ResourceId,
resource.Id,
StringComparison.OrdinalIgnoreCase));
RenderExtentDeclaration extent = resourceOverride?.Extent
?? resource.Extent
?? throw new NotSupportedException(
$"Image resource '{resource.Id}' has no extent.");
(int width, int height) = ResolveExtent(
resource.Id,
extent,
mainWorldWidth,
mainWorldHeight);
int layers = extent.Layers;
if (layers <= 0)
{
throw new NotSupportedException(
$"Image resource '{resource.Id}' has no image layers.");
}
int bytesPerPixel = resource.Format switch
{
RenderFormatClass.HdrColor => HdrColorBytesPerPixel,
RenderFormatClass.LdrColor or RenderFormatClass.SingleChannel =>
LdrColorBytesPerPixel,
RenderFormatClass.DirectionalDepth => DirectionalDepthBytesPerPixel,
_ => throw new NotSupportedException(
$"Image resource '{resource.Id}' has unsupported format "
+ $"'{resource.Format}'."),
};
retained = checked(retained
+ ((long)width * height * layers * bytesPerPixel));
largestWidth = Math.Max(largestWidth, width);
largestHeight = Math.Max(largestHeight, height);
largestLayers = Math.Max(largestLayers, layers);
}
if (descriptor.Passes.Any(pass =>
pass.Semantic == RenderPassSemantic.DirectionalShadowDepth))
{
retained = checked(
retained
+ DirectionalShadowTransformFlightSlots
* WorldTransformCapacityPolicy.InitialBindingSizeBytes);
}
return new RenderPackResourceBudget(
retained,
multisample,
largestWidth,
largestHeight,
largestLayers);
}
private static bool UsesFusedAtmosphericPostProcess(
RenderQualityPreset preset) =>
(preset.ExecutionHints
& RenderQualityExecutionHints.FusedAtmosphericPostProcess) != 0;
internal static RenderPackResourceBudget RequireWithinPreset(
RenderPackDescriptor descriptor,
RenderQualityPreset preset,
int mainWorldWidth,
int mainWorldHeight,
int sampleCount)
{
RenderPackResourceBudget budget = Resolve(
descriptor,
preset,
mainWorldWidth,
mainWorldHeight,
sampleCount);
if (budget.RetainedGpuBytes > preset.MaxResidentGpuBytes)
{
throw new NotSupportedException(
$"Render pack preset '{preset.Id}' needs "
+ $"{budget.RetainedGpuBytes} resident GPU bytes at "
+ $"{mainWorldWidth}x{mainWorldHeight}; its declared ceiling is "
+ $"{preset.MaxResidentGpuBytes}. Select a compatible preset or "
+ "reduce the main-world resolution.");
}
return budget;
}
/// <summary>
/// Allocation-time gate against the selected adapter and the host's
/// explicit optional-memory share. Catalog checks can reject absolute
/// preset extents, but only this point knows the resolved viewport-relative
/// sizes and multisample attachment bytes.
/// </summary>
internal static RenderPackResourceBudget RequireWithinHost(
RenderPackDescriptor descriptor,
RenderQualityPreset preset,
int mainWorldWidth,
int mainWorldHeight,
int sampleCount,
RenderPackHostCapabilities capabilities)
{
ArgumentNullException.ThrowIfNull(capabilities);
RenderPackResourceBudget budget = RequireWithinPreset(
descriptor,
preset,
mainWorldWidth,
mainWorldHeight,
sampleCount);
if (budget.LargestImageWidth > capabilities.MaxImageDimension2D
|| budget.LargestImageHeight > capabilities.MaxImageDimension2D)
{
throw new NotSupportedException(
$"Render pack preset '{preset.Id}' resolves an image to "
+ $"{budget.LargestImageWidth}x{budget.LargestImageHeight} at "
+ $"{mainWorldWidth}x{mainWorldHeight}; this device's maximum "
+ $"2-D image edge is {capabilities.MaxImageDimension2D}.");
}
if (budget.LargestImageLayerCount > capabilities.MaxImageArrayLayers)
{
throw new NotSupportedException(
$"Render pack preset '{preset.Id}' needs "
+ $"{budget.LargestImageLayerCount} image-array layers; this "
+ $"device provides {capabilities.MaxImageArrayLayers}.");
}
if (budget.RetainedGpuBytes > capabilities.MaxPackResidentBytes)
{
throw new NotSupportedException(
$"Render pack preset '{preset.Id}' needs "
+ $"{budget.RetainedGpuBytes} resident GPU bytes at "
+ $"{mainWorldWidth}x{mainWorldHeight}; this host permits "
+ $"{capabilities.MaxPackResidentBytes} under its "
+ $"{capabilities.MemoryPolicyDescription} policy.");
}
if (budget.MultisampleGpuBytes > capabilities.MaxPackTransientBytes)
{
throw new NotSupportedException(
$"Render pack preset '{preset.Id}' needs "
+ $"{budget.MultisampleGpuBytes} transient multisample GPU bytes "
+ $"at {mainWorldWidth}x{mainWorldHeight} x{sampleCount}; this "
+ $"host permits {capabilities.MaxPackTransientBytes} under its "
+ $"{capabilities.MemoryPolicyDescription} policy.");
}
return budget;
}
private static (int Width, int Height) ResolveExtent(
string resourceId,
RenderExtentDeclaration extent,
int mainWorldWidth,
int mainWorldHeight)
{
if (!double.IsFinite(extent.Width)
|| !double.IsFinite(extent.Height)
|| extent.Width <= 0d
|| extent.Height <= 0d)
{
throw new NotSupportedException(
$"Image resource '{resourceId}' has an invalid extent.");
}
try
{
return extent.Mode switch
{
RenderExtentMode.AbsolutePixels =>
(checked((int)extent.Width), checked((int)extent.Height)),
RenderExtentMode.RelativeToMainWorld or RenderExtentMode.RelativeToOutput =>
(Math.Max(1, checked((int)Math.Ceiling(mainWorldWidth * extent.Width))),
Math.Max(1, checked((int)Math.Ceiling(mainWorldHeight * extent.Height)))),
_ => throw new NotSupportedException(
$"Image resource '{resourceId}' has unsupported extent mode "
+ $"'{extent.Mode}'."),
};
}
catch (OverflowException error)
{
throw new NotSupportedException(
$"Image resource '{resourceId}' extent overflows the host image range.",
error);
}
}
}

View file

@ -0,0 +1,85 @@
using AcDream.App.Settings;
using AcDream.UI.Abstractions.Panels.Settings;
namespace AcDream.App.Rendering.Packs;
/// <summary>
/// Bridges committed Display settings to the render-thread controller. The
/// controller performs all GPU work at the explicit frame boundary; this
/// binding only queues stable logical selections and persists a safe retail
/// fallback once per failed activation generation.
/// </summary>
internal sealed class RenderPackSelectionBinding : IDisposable
{
private readonly RuntimeSettingsController _settings;
private readonly RenderPackController _controller;
private readonly Action<string> _log;
private long _fallbackPersistedGeneration = -1;
private bool _suppressDisplayEdge;
private bool _disposed;
internal RenderPackSelectionBinding(
RuntimeSettingsController settings,
RenderPackController controller,
Action<string>? log = null)
{
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
_controller = controller ?? throw new ArgumentNullException(nameof(controller));
_log = log ?? (_ => { });
_settings.DisplayChanged += OnDisplayChanged;
_controller.Request(_settings.Display.RenderPack);
}
internal RenderPackActivationSnapshot ApplyAtFrameBoundary(
RenderPackActivationExtent extent)
{
ObjectDisposedException.ThrowIf(_disposed, this);
RenderPackActivationSnapshot snapshot = _controller.ApplyAtFrameBoundary(extent);
if (snapshot.State != RenderPackActivationState.FailedToRetail
|| snapshot.ActivationGeneration == _fallbackPersistedGeneration
|| _settings.Display.RenderPack.IsRetail)
return snapshot;
_fallbackPersistedGeneration = snapshot.ActivationGeneration;
_suppressDisplayEdge = true;
try
{
_settings.SaveDisplay(_settings.Display with
{
RenderPack = RenderPackSelectionSettings.Retail,
});
}
finally
{
_suppressDisplayEdge = false;
}
if (_settings.Display.RenderPack.IsRetail)
{
_log(
$"[render-pack] selection failed; persisted acdream default (retail-faithful): "
+ snapshot.Reason);
}
else
{
_log(
$"[render-pack] selection failed and retail fallback could not be persisted: "
+ snapshot.Reason);
}
return snapshot;
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_settings.DisplayChanged -= OnDisplayChanged;
}
private void OnDisplayChanged(DisplaySettings display)
{
if (!_disposed && !_suppressDisplayEdge)
_controller.Request(display.RenderPack);
}
}

View file

@ -0,0 +1,77 @@
using AcDream.Plugin.Abstractions.Rendering;
using AcDream.UI.Abstractions.Panels.Settings;
namespace AcDream.App.Rendering.Packs;
internal static class RenderPackSettingResolution
{
internal static RenderPackValidationResult ValidateUserOverrides(
RenderPackDescriptor descriptor,
RenderPackSettingOverrides overrides)
{
ArgumentNullException.ThrowIfNull(descriptor);
if (overrides is null)
return Invalid($"Render pack '{descriptor.Id}' has a null user-setting override map.");
Dictionary<string, RenderSettingDeclaration> settings = descriptor.Settings
.ToDictionary(setting => setting.Id, StringComparer.OrdinalIgnoreCase);
foreach ((string id, string value) in overrides)
{
if (!settings.TryGetValue(id, out RenderSettingDeclaration? setting))
{
return Invalid(
$"Render pack '{descriptor.Id}' has a user override for unknown "
+ $"setting '{id}'.");
}
if (!RenderPackSettingValueCodec.TryEncode(setting, value, out _))
{
return Invalid(
$"Render pack '{descriptor.Id}' user override '{id}' has invalid "
+ $"{setting.Kind} value '{value}'.");
}
}
return RenderPackValidationResult.Valid();
}
internal static string Resolve(
RenderSettingDeclaration setting,
RenderQualityPreset preset,
IReadOnlyDictionary<string, string>? userOverrides)
{
ArgumentNullException.ThrowIfNull(setting);
ArgumentNullException.ThrowIfNull(preset);
if (TryGet(userOverrides, setting.Id, out string? user))
return user;
RenderQualitySettingOverride? presetValue = preset.SettingOverrides
.FirstOrDefault(value => string.Equals(
value.SettingId,
setting.Id,
StringComparison.OrdinalIgnoreCase));
return presetValue?.Value ?? setting.DefaultValue;
}
private static bool TryGet(
IReadOnlyDictionary<string, string>? values,
string id,
out string value)
{
if (values is not null && values.TryGetValue(id, out value!))
return true;
if (values is not null)
{
foreach ((string key, string candidate) in values)
{
if (string.Equals(key, id, StringComparison.OrdinalIgnoreCase))
{
value = candidate;
return true;
}
}
}
value = string.Empty;
return false;
}
private static RenderPackValidationResult Invalid(string reason) =>
RenderPackValidationResult.Invalid(reason);
}

View file

@ -0,0 +1,71 @@
using System.Collections.Immutable;
using AcDream.App.Rendering.Gpu;
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.App.Rendering.Packs;
internal static class RenderPackShaderAssets
{
internal static ValidatedRenderPackShaderAssets Validate(
RenderPackDescriptor descriptor,
IRenderPackAssets assets)
{
RenderPackValidationResult result = RenderPackValidator.ValidateSelectedAssets(
descriptor,
assets,
out ValidatedRenderPackShaderAssets? validated);
if (!result.Success)
throw new InvalidDataException(result.Reason);
return validated!;
}
internal static GpuShaderSet LoadPass(
RenderPackDescriptor descriptor,
ValidatedRenderPackShaderAssets assets,
RenderPassDeclaration pass) => new(
$"{descriptor.Id}:{pass.Id}",
assets.Copy(pass.VertexShaderAsset),
assets.Copy(pass.FragmentShaderAsset));
internal static GpuShaderSet LoadVariant(
RenderPackDescriptor descriptor,
ValidatedRenderPackShaderAssets assets,
PipelineVariantDeclaration variant) => new(
$"{descriptor.Id}:{variant.Id}",
assets.Copy(variant.VertexShaderAsset),
assets.Copy(variant.FragmentShaderAsset));
}
/// <summary>
/// Candidate-owned immutable shader snapshot. The plugin asset provider is
/// read exactly once during selected-candidate validation; pipeline creation
/// only copies bytes from this snapshot and cannot reopen a mutable plugin
/// stream or resolve a second path.
/// </summary>
internal sealed class ValidatedRenderPackShaderAssets
{
private readonly IReadOnlyDictionary<string, ImmutableArray<byte>> _assets;
internal ValidatedRenderPackShaderAssets(
IReadOnlyDictionary<string, byte[]> assets)
{
ArgumentNullException.ThrowIfNull(assets);
var owned = new Dictionary<string, ImmutableArray<byte>>(
assets.Count,
StringComparer.Ordinal);
foreach ((string key, byte[] bytes) in assets)
{
ArgumentException.ThrowIfNullOrWhiteSpace(key);
ArgumentNullException.ThrowIfNull(bytes);
owned.Add(key, [.. bytes]);
}
_assets = owned;
}
internal byte[] Copy(string key)
{
if (!_assets.TryGetValue(key, out ImmutableArray<byte> bytes))
throw new InvalidDataException($"Validated render-pack shader '{key}' is missing.");
return [.. bytes];
}
}

View file

@ -0,0 +1,54 @@
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.App.Rendering.Packs;
internal readonly record struct RenderPackTextureInput(
RenderSemanticInput? Semantic,
string? ResourceId)
{
internal static RenderPackTextureInput FromSemantic(RenderSemanticInput value) =>
new(value, null);
internal static RenderPackTextureInput FromResource(string value) =>
new(null, value);
}
/// <summary>
/// Binary API-v1 texture-slot rule. Ordinary sampled inputs occupy push
/// TextureIndexA..D in declaration order: sampled semantic inputs first, then
/// declared resource reads. Directional depth uses its dedicated binding-6
/// texture slot and therefore does not consume A..D.
/// </summary>
internal static class RenderPackTextureBindingResolver
{
internal static IReadOnlyList<RenderPackTextureInput> Resolve(
RenderPassDeclaration pass,
IReadOnlyDictionary<string, RenderResourceDeclaration> resources)
{
ArgumentNullException.ThrowIfNull(pass);
ArgumentNullException.ThrowIfNull(resources);
var result = new List<RenderPackTextureInput>(4);
foreach (RenderSemanticInput semantic in pass.SemanticInputs)
{
if (semantic is RenderSemanticInput.WorldColor
or RenderSemanticInput.SceneDepth
or RenderSemanticInput.SceneNormals)
result.Add(RenderPackTextureInput.FromSemantic(semantic));
}
foreach (string resourceId in pass.ResourceReads)
{
if (!resources.TryGetValue(resourceId, out RenderResourceDeclaration? resource))
throw new InvalidOperationException($"Unknown render-pack resource '{resourceId}'.");
if (resource.Format == RenderFormatClass.DirectionalDepth
&& pass.SemanticInputs.Contains(RenderSemanticInput.DirectionalShadowMaps))
continue;
result.Add(RenderPackTextureInput.FromResource(resourceId));
}
if (result.Count > 4)
{
throw new InvalidOperationException(
$"Render-pack pass '{pass.Id}' exceeds the four API-v1 texture slots.");
}
return result;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,471 @@
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;
}
/// <summary>
/// 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.
/// </summary>
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<int, float> _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<string, string>? userSettingOverrides = null)
: this(
device,
descriptor,
RenderPackShaderAssets.Validate(descriptor, assets),
preset,
userSettingOverrides)
{
}
internal VolumetricShaftRenderer(
IGpuDevice device,
RenderPackDescriptor descriptor,
ValidatedRenderPackShaderAssets assets,
RenderQualityPreset preset,
IReadOnlyDictionary<string, string>? 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();
/// <summary>
/// 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 <see cref="_declaredStrength"/>.
/// </summary>
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);
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<string, string>? 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<string, string>? 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();
}
}
}