feat(render): implement Campaign AR and terrain fidelity
This commit is contained in:
parent
99cf26e00c
commit
7a5f96ede5
368 changed files with 50611 additions and 950 deletions
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
153
src/AcDream.App/Rendering/Packs/AtmosphericCpuStageProfiler.cs
Normal file
153
src/AcDream.App/Rendering/Packs/AtmosphericCpuStageProfiler.cs
Normal 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);
|
||||
}
|
||||
229
src/AcDream.App/Rendering/Packs/AtmosphericFrameInputs.cs
Normal file
229
src/AcDream.App/Rendering/Packs/AtmosphericFrameInputs.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
1731
src/AcDream.App/Rendering/Packs/AtmosphericPostProcessGraph.cs
Normal file
1731
src/AcDream.App/Rendering/Packs/AtmosphericPostProcessGraph.cs
Normal file
File diff suppressed because it is too large
Load diff
183
src/AcDream.App/Rendering/Packs/AuthoredCelestialShadowSource.cs
Normal file
183
src/AcDream.App/Rendering/Packs/AuthoredCelestialShadowSource.cs
Normal 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;
|
||||
}
|
||||
519
src/AcDream.App/Rendering/Packs/BuiltInAtmosphericRenderPack.cs
Normal file
519
src/AcDream.App/Rendering/Packs/BuiltInAtmosphericRenderPack.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
37
src/AcDream.App/Rendering/Packs/RenderPackCatalogSource.cs
Normal file
37
src/AcDream.App/Rendering/Packs/RenderPackCatalogSource.cs
Normal 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);
|
||||
}
|
||||
1404
src/AcDream.App/Rendering/Packs/RenderPackController.cs
Normal file
1404
src/AcDream.App/Rendering/Packs/RenderPackController.cs
Normal file
File diff suppressed because it is too large
Load diff
284
src/AcDream.App/Rendering/Packs/RenderPackDiagnostics.cs
Normal file
284
src/AcDream.App/Rendering/Packs/RenderPackDiagnostics.cs
Normal 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);
|
||||
}
|
||||
166
src/AcDream.App/Rendering/Packs/RenderPackPerformanceWindow.cs
Normal file
166
src/AcDream.App/Rendering/Packs/RenderPackPerformanceWindow.cs
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
71
src/AcDream.App/Rendering/Packs/RenderPackShaderAssets.cs
Normal file
71
src/AcDream.App/Rendering/Packs/RenderPackShaderAssets.cs
Normal 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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
1604
src/AcDream.App/Rendering/Packs/RenderPackValidation.cs
Normal file
1604
src/AcDream.App/Rendering/Packs/RenderPackValidation.cs
Normal file
File diff suppressed because it is too large
Load diff
471
src/AcDream.App/Rendering/Packs/VolumetricShaftRenderer.cs
Normal file
471
src/AcDream.App/Rendering/Packs/VolumetricShaftRenderer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue