Fix round from Opus review of 87677f9c (APPROVE WITH FIXES):
1. (A1/A3) Hardened the atmospheric_filmic.frag / atmospheric_bloom_
downsample.frag shader-source pinning test: asserts the exact decode
call count (6 - three in lowFusedScene, three in main's non-fused
branch), that the 2.2 display-gamma exponent and 1/2.2 inverse in
atmospheric_common.glsl are formatted FROM AtmosphericColorPipeline
.DisplayGamma (so shader literal and CPU-tested value cannot drift),
that the 0.18 contrast pivot in atmospheric_filmic.frag is formatted
from AtmosphericColorPipeline.LinearMidGrey, and pins
AtmosphericPostProcessGraph.BloomKneeLinear/BloomThresholdLinear as
exact literals (0.73f / 1f). Also removed a stray trailing "()" from
an existing comment in atmospheric_filmic.frag that was inflating the
decode-call count to 7.
2. (B1) Re-derived the "vignette-strength" default for the linear-light
post stack. The vignette multiply now happens on linear colour before
the final encode, so a corner factor of (1 - strength) displays as
(1 - strength)^(1/2.2), not (1 - strength) directly. The accepted
look was strength 0.12 under the OLD gamma-space pipeline: a 12%
on-screen corner darkening. Under strength 0.12 in the new linear
pipeline that same 0.88 corner multiplier would only display as
0.88^(1/2.2) ~= 0.9435 (5.6% darkening - visibly weaker). Solving
(1 - strength)^(1/2.2) = 0.88 gives strength = 1 - 0.88^2.2 ~= 0.245,
which reproduces the accepted 12% corner darkening. Since
RenderPackSettingValueCodec requires every declared default to be
step-aligned from the minimum and 0.245 is not a multiple of the old
0.01 step, the step also moves to 0.005 (a finer slider, not
coarser) so the exact derived default is a valid grid point -
verified by running the ExternalTierTwoPackCanRenameEveryOwnedId
AndShaderAsset validation test, which failed with "invalid default
value" before this correction. Also updated the matching fallback in
AtmosphericPostProcessGraph.FromDescriptor (0.12f -> 0.245f) for
consistency, and added
AtmosphericColorPipelineTests.VignetteDefaultReproducesTheAccepted
TwelvePercentCornerDarkening pinning encode(1-0.245) ~= 0.88.
3. (A4) Renamed VolumetricShaftFrameParameters.LinearSunColor ->
AuthoredSunColor in VolumetricShaftQuality.cs (internal, 2 references,
both in that file - safe). Left LightSource.ColorLinear unrenamed:
grep shows 13 files depend on it (GlobalLightPacker, SceneLightingUbo,
LightBake, LightManager, EnvCellRenderer, RenderingDiagnostics, and
several Core tests) across the shared retail default-path lighting
UBO pipeline - renaming it is out of VM3's pack-only scope and would
touch the mandatory-unchanged default path. Added a pointer comment
on the field in LightSource.cs (and a one-line note at its
WorldRenderFrameBuilder.cs call site) documenting the same
display-space-not-linear fact and explaining why the rename is
deferred to its own default-path colour-space pass.
4. (B4) Added a citation beside acesFitted in both atmospheric_filmic
.frag and its C# mirror (AtmosphericColorPipeline.AcesFitted):
Krzysztof Narkowicz, "ACES Filmic Tone Mapping Curve" (2016). The fit
takes linear scene light in and returns linear display light in
[0,1] - it does not itself gamma-encode. Evidence: acesFitted(0.80 *
decode(0.46)) = 0.2064 un-encoded versus the accepted 0.51 on screen.
5. (B2) Rewrote the VM3 section of docs/plans/2026-08-22-visualmaster-
campaign.md with the shipped truth in place of the pre-implementation
guess: exposure stays 0.80 (at exposure 1.0 the linear pipeline maps
gamma-0.5 to 0.6017, essentially the same 0.6163 the owner called too
bright), bloom threshold stays 1.0 (a fixed point of both exponents),
knee moves 0.45 -> 0.73, vignette-strength moves 0.12 -> 0.245. Added
the old-vs-new curve table at exposure 0.80 across ten gamma inputs.
Replaced the acceptance criteria's "new automated test on the
recording RHI" with the CPU mirror + shader-source pins actually
used, and recorded that the real-frame masked capture WAS run
(retail vs High-with-every-effect-neutral, artifacts/vm3):
independently re-verified by re-running the pixel diff against the
checked-in screenshots - 110,561 px at |delta|=1 and exactly 95
pixels at |delta|>=5, confined to foliage-canopy silhouette edges
against sky with nothing on any ground/building/water surface. Noted
the Stage-1 luminance table re-capture is still owed at the owner
gate.
6. (B3) Corrected docs/plans/2026-08-21-atmospheric-rendering.md's VM3
summary sentence: the bloom intermediate is already linear after
extraction (no separate "bloom read" decode), and the neutral-preset
claim is now phrased as a measured numerical identity (<=1 LSB on a
real frame) rather than an unqualified "is" statement.
7. (A5) Corrected toolchain attribution: tools/compile-shaders.ps1 used
the managed Silk.NET.Shaderc path (shaderc_shared.dll) to compile in
both this round and the original VM3 commit - a Vulkan SDK glslc was
detected and its path recorded, but the managed compiler is what
actually ran. Regenerating this round only changed the atmospheric_
filmic frag stage's manifest hash (comment-only edits); the compiled
.spv bytes are unchanged, and every retail-oracle shader
(mesh_modern, terrain_modern, mesh_detail, etc.) remains untouched.
8. Replaced an invented motive in the atmospheric_filmic.frag contrast-
pivot comment ("rounded up for a stronger gamma-space contrast
feel") with the actual reason: the previous 0.5 was simply the [0,1]
midpoint of the standard contrast formula, not a deliberately chosen
value; in linear the perceptual mid-grey is 0.18.
Verify: Release build 0 warnings / 0 errors. App hermetic-filter tests:
5970 passed / 0 failed / 0 skipped. VulkanShaderManifestTests: 7/7 pass
(retail-oracle SPIR-V byte-identical; only the atmospheric_filmic frag
manifest hash changed, no .spv bytes changed).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1594 lines
68 KiB
C#
1594 lines
68 KiB
C#
using System.Globalization;
|
|
using System.Numerics;
|
|
using System.Runtime.InteropServices;
|
|
using AcDream.App.Plugins;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Gpu;
|
|
using AcDream.App.Rendering.Packs;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.App.Tests.Rendering.Gpu;
|
|
using AcDream.Core.World;
|
|
using AcDream.Plugin.Abstractions.Rendering;
|
|
using AcDream.UI.Abstractions.Panels.Settings;
|
|
|
|
namespace AcDream.App.Tests.Rendering.Packs;
|
|
|
|
public sealed class AtmosphericPostProcessGraphTests
|
|
{
|
|
[Fact]
|
|
public void LowSamplesEveryPostTimerTogetherOnEveryFourthFrame()
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
using var graph = Graph(device, "low");
|
|
IGpuRenderTarget world = graph.PrepareWorldTarget(1280, 720, 1);
|
|
AtmosphericFrameInputs inputs = Inputs(1280, 720);
|
|
|
|
for (int serial = 1; serial <= 4; serial++)
|
|
{
|
|
device.Clear();
|
|
using IGpuFrame frame = device.BeginFrame();
|
|
RecordWorldPass(frame, world);
|
|
graph.RenderPostProcess(frame, in inputs);
|
|
frame.End();
|
|
|
|
string[] measured = device.OfKind<GpuRecordedTimerScope>()
|
|
.Select(static scope => scope.Name)
|
|
.Where(static name => name.StartsWith(
|
|
"atmospheric-",
|
|
StringComparison.Ordinal))
|
|
.ToArray();
|
|
if (serial < AtmosphericGpuTimerSampling.LowIntervalFrames)
|
|
{
|
|
Assert.Empty(measured);
|
|
}
|
|
else
|
|
{
|
|
Assert.Equal(
|
|
[
|
|
"atmospheric-sun-occlusion",
|
|
"atmospheric-sun-rays",
|
|
"atmospheric-bloom-downsample",
|
|
"atmospheric-bloom-blur-horizontal",
|
|
"atmospheric-bloom-blur-vertical",
|
|
"atmospheric-filmic",
|
|
],
|
|
measured);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LowUsesQuarterResolutionSeparableBloomWithoutDroppingHeadlineInputs()
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
using var graph = Graph(device, "low");
|
|
Assert.True(Assert.IsType<DirectionalSunShadowRenderer>(
|
|
graph.DirectionalShadowReceivers).MultiviewCascadesEnabled);
|
|
IGpuRenderTarget world = graph.PrepareWorldTarget(1280, 720, 1);
|
|
device.Clear();
|
|
|
|
using IGpuFrame frame = device.BeginFrame();
|
|
RecordWorldPass(frame, world);
|
|
AtmosphericFrameInputs inputs = Inputs(1280, 720);
|
|
graph.RenderPostProcess(frame, in inputs);
|
|
frame.End();
|
|
|
|
Assert.Equal(
|
|
[
|
|
"test-world-hdr",
|
|
"atmospheric-sun-occlusion",
|
|
"atmospheric-sun-rays",
|
|
"atmospheric-bloom-downsample",
|
|
"atmospheric-bloom-blur-horizontal",
|
|
"atmospheric-bloom-blur-vertical",
|
|
"atmospheric-filmic",
|
|
],
|
|
device.OfKind<GpuRecordedPassBegin>().Select(call => call.Name));
|
|
GpuRecordedUniformBind[] passBlocks = device
|
|
.OfKind<GpuRecordedUniformBind>()
|
|
.Where(call => call.Binding == GpuBindingModel.UniformPackPass)
|
|
.ToArray();
|
|
Assert.Equal(6, passBlocks.Length);
|
|
Assert.All(
|
|
device.OfKind<GpuRecordedUniformBind>().Where(call =>
|
|
call.Binding is GpuBindingModel.UniformAtmosphericFrame
|
|
or GpuBindingModel.UniformPackPass
|
|
or GpuBindingModel.UniformPackSettings),
|
|
call => Assert.Equal(
|
|
0u,
|
|
call.OffsetBytes
|
|
% device.Capabilities.MinUniformBufferOffsetAlignment));
|
|
|
|
AtmosphericPackPassUniforms rays = ReadPass(device, passBlocks[1]);
|
|
Assert.Equal(Vector4.Zero, rays.Params1);
|
|
AtmosphericPackPassUniforms bloom = ReadPass(device, passBlocks[2]);
|
|
Assert.Equal(
|
|
new Vector4(
|
|
graph.Settings.BloomStrength,
|
|
AtmosphericPostProcessGraph.BloomThresholdLinear,
|
|
AtmosphericPostProcessGraph.BloomKneeLinear,
|
|
0f),
|
|
bloom.Params0);
|
|
AtmosphericPackPassUniforms horizontal = ReadPass(device, passBlocks[3]);
|
|
Assert.Equal(new Vector4(1f / 320f, 0f, 0f, 0f), horizontal.Params0);
|
|
AtmosphericPackPassUniforms vertical = ReadPass(device, passBlocks[4]);
|
|
Assert.Equal(new Vector4(0f, 1f / 180f, 0f, 0f), vertical.Params0);
|
|
AtmosphericPackPassUniforms filmic = ReadPass(device, passBlocks[5]);
|
|
Assert.Equal(0f, filmic.Params1.Z);
|
|
Assert.Equal(Vector4.Zero, filmic.Params2);
|
|
Assert.Equal(Vector4.Zero, filmic.Params3);
|
|
Assert.Contains(device.CreatedRenderTargets, target =>
|
|
target.Description.Name == "atmospheric-bloom-a"
|
|
&& target.Description.Width == 320
|
|
&& target.Description.Height == 180);
|
|
Assert.Contains(device.CreatedRenderTargets, target =>
|
|
target.Description.Name == "atmospheric-bloom-b"
|
|
&& target.Description.Width == 320
|
|
&& target.Description.Height == 180);
|
|
|
|
GpuRecordedPushConstants[] pushes = device
|
|
.OfKind<GpuRecordedPushConstants>()
|
|
.ToArray();
|
|
Assert.Equal(6, pushes.Length);
|
|
Assert.All(pushes, push =>
|
|
Assert.NotEqual(uint.MaxValue, push.Constants.TextureIndexA));
|
|
Assert.NotEqual(uint.MaxValue, pushes[2].Constants.TextureIndexB);
|
|
Assert.NotEqual(uint.MaxValue, pushes[5].Constants.TextureIndexB);
|
|
|
|
RenderPackRuntimeDiagnostics diagnostics = graph.CaptureDiagnostics();
|
|
Assert.Equal(6, diagnostics.DrawCalls);
|
|
Assert.Equal(7, diagnostics.ImageCount);
|
|
Assert.Contains(diagnostics.Passes, pass =>
|
|
pass.PassId == "atmospheric-sun-occlusion" && pass.DrawCalls == 1);
|
|
Assert.Contains(diagnostics.Passes, pass =>
|
|
pass.PassId == "atmospheric-sun-rays" && pass.DrawCalls == 1);
|
|
Assert.Contains(diagnostics.Passes, pass =>
|
|
pass.PassId == "atmospheric-bloom-downsample" && pass.DrawCalls == 1);
|
|
Assert.Contains(diagnostics.Passes, pass =>
|
|
pass.PassId == "atmospheric-bloom-blur-horizontal" && pass.DrawCalls == 1);
|
|
Assert.Contains(diagnostics.Passes, pass =>
|
|
pass.PassId == "atmospheric-bloom-blur-vertical" && pass.DrawCalls == 1);
|
|
Assert.Contains(diagnostics.Passes, pass =>
|
|
pass.PassId == "atmospheric-filmic" && pass.DrawCalls == 1);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(
|
|
AuthoredCelestialShadowSourceKind.Sun,
|
|
5,
|
|
0x01001348u,
|
|
0.25f,
|
|
-0.5f,
|
|
0.8291562f,
|
|
0.8291562f)]
|
|
[InlineData(
|
|
AuthoredCelestialShadowSourceKind.DominantMoon,
|
|
3,
|
|
0x01001F6Au,
|
|
-0.6f,
|
|
0.2f,
|
|
0.7745967f,
|
|
0.7745967f)]
|
|
[InlineData(
|
|
AuthoredCelestialShadowSourceKind.SecondaryMoon,
|
|
2,
|
|
0x01001F67u,
|
|
0.4f,
|
|
0.8f,
|
|
0.4472136f,
|
|
0.4472136f)]
|
|
[InlineData(
|
|
AuthoredCelestialShadowSourceKind.None,
|
|
-1,
|
|
0u,
|
|
0f,
|
|
0f,
|
|
1f,
|
|
0f)]
|
|
internal void CaptureDiagnosticsPreservesSunMoonAndNoneSourceMetadata(
|
|
AuthoredCelestialShadowSourceKind sourceKind,
|
|
int sourceObjectIndex,
|
|
uint sourceGfxObjId,
|
|
float directionX,
|
|
float directionY,
|
|
float directionZ,
|
|
float elevationSin)
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
using var graph = Graph(device, "medium");
|
|
IGpuRenderTarget world = graph.PrepareWorldTarget(1280, 720, 1);
|
|
using IGpuFrame frame = device.BeginFrame();
|
|
RecordWorldPass(frame, world);
|
|
AtmosphericFrameInputs inputs = Inputs(1280, 720);
|
|
graph.RenderPostProcess(frame, in inputs);
|
|
frame.End();
|
|
var direction = new Vector3(directionX, directionY, directionZ);
|
|
|
|
// Exercise only the graph's diagnostics projection. This is the exact
|
|
// value object that DirectionalSunShadowRenderer publishes after its
|
|
// separately-covered environment/render path.
|
|
SetLastShadowDiagnostics(
|
|
graph,
|
|
new DirectionalSunShadowDiagnostics(
|
|
GateReason: sourceKind is AuthoredCelestialShadowSourceKind.None
|
|
? DirectionalShadowGateReason.NoVisibleCelestial
|
|
: DirectionalShadowGateReason.Enabled,
|
|
Strength: sourceKind is AuthoredCelestialShadowSourceKind.None
|
|
? 0f
|
|
: 0.75f,
|
|
CascadeCount: 0,
|
|
DrawCalls: 0,
|
|
WorldOpaqueCommands: 0,
|
|
WorldAlphaCutoutCommands: 0,
|
|
TerrainCommands: 0,
|
|
WorldPreparationSequence: 0,
|
|
TerrainPreparationSequence: 0,
|
|
CpuMilliseconds: 0,
|
|
LastResolvedGpuMilliseconds: 0,
|
|
HasResolvedGpuMeasurement: false,
|
|
ResidentDepthBytes: 0,
|
|
SourceKind: sourceKind,
|
|
SourceObjectIndex: sourceObjectIndex,
|
|
SourceGfxObjId: sourceGfxObjId,
|
|
SurfaceToLightDirection: direction,
|
|
LightElevationSin: elevationSin));
|
|
|
|
RenderPackRuntimeDiagnostics diagnostics = graph.CaptureDiagnostics();
|
|
|
|
Assert.Equal(sourceKind, diagnostics.DirectionalShadowSourceKind);
|
|
Assert.Equal(
|
|
sourceObjectIndex,
|
|
diagnostics.DirectionalShadowSourceObjectIndex);
|
|
Assert.Equal(sourceGfxObjId, diagnostics.DirectionalShadowSourceGfxObjId);
|
|
Assert.Equal(
|
|
direction,
|
|
diagnostics.DirectionalShadowSurfaceToLightDirection);
|
|
Assert.Equal(
|
|
elevationSin,
|
|
diagnostics.DirectionalShadowLightElevationSin);
|
|
}
|
|
|
|
[Fact]
|
|
public void GraphRunsTheDeclaredHdrPassOrderAndBindsStablePackAbi()
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
using var graph = Graph(device, "medium");
|
|
IGpuRenderTarget world = graph.PrepareWorldTarget(1280, 720, 4);
|
|
device.Clear();
|
|
|
|
using IGpuFrame frame = device.BeginFrame();
|
|
RecordWorldPass(frame, world);
|
|
AtmosphericFrameInputs inputs = Inputs(1280, 720);
|
|
graph.RenderPostProcess(frame, in inputs);
|
|
frame.End();
|
|
|
|
Assert.Equal(
|
|
[
|
|
"test-world-hdr",
|
|
"atmospheric-sun-occlusion",
|
|
"atmospheric-sun-rays",
|
|
"atmospheric-bloom-downsample",
|
|
"atmospheric-bloom-blur-horizontal",
|
|
"atmospheric-bloom-blur-vertical",
|
|
"atmospheric-filmic",
|
|
],
|
|
device.OfKind<GpuRecordedPassBegin>().Select(call => call.Name));
|
|
Assert.Equal(
|
|
6,
|
|
device.OfKind<GpuRecordedUniformBind>().Count(call =>
|
|
call.Binding == GpuBindingModel.UniformAtmosphericFrame
|
|
&& call.SizeBytes == AtmosphericFrameUniforms.SizeInBytes));
|
|
Assert.Equal(
|
|
6,
|
|
device.OfKind<GpuRecordedUniformBind>().Count(call =>
|
|
call.Binding == GpuBindingModel.UniformPackPass
|
|
&& call.SizeBytes == AtmosphericPackPassUniforms.SizeInBytes));
|
|
Assert.Equal(
|
|
6,
|
|
device.OfKind<GpuRecordedUniformBind>().Count(call =>
|
|
call.Binding == GpuBindingModel.UniformPackSettings
|
|
&& call.SizeBytes == PackSettingsUniforms.SizeInBytes));
|
|
GpuRecordedPushConstants[] pushes = device.OfKind<GpuRecordedPushConstants>().ToArray();
|
|
Assert.All(pushes[..^1], call =>
|
|
{
|
|
Assert.True(call.Constants.TextureIndexA != uint.MaxValue);
|
|
Assert.Equal(uint.MaxValue, BitConverter.SingleToUInt32Bits(call.Constants.ParamA));
|
|
Assert.Equal(uint.MaxValue, BitConverter.SingleToUInt32Bits(call.Constants.ParamB));
|
|
});
|
|
Assert.NotEqual(uint.MaxValue, BitConverter.SingleToUInt32Bits(pushes[^1].Constants.ParamA));
|
|
Assert.Equal(uint.MaxValue, BitConverter.SingleToUInt32Bits(pushes[^1].Constants.ParamB));
|
|
Assert.NotEqual(uint.MaxValue, pushes[2].Constants.TextureIndexB);
|
|
Assert.Equal(uint.MaxValue, BitConverter.SingleToUInt32Bits(pushes[2].Constants.ParamA));
|
|
|
|
RenderPackRuntimeDiagnostics diagnostics = graph.CaptureDiagnostics();
|
|
Assert.Equal(10, diagnostics.ImageCount);
|
|
Assert.Equal(6, diagnostics.DrawCalls);
|
|
Assert.Equal(0, diagnostics.ShadowCasterCount);
|
|
Assert.Equal(0, diagnostics.CascadeDrawCount);
|
|
Assert.Equal(0, diagnostics.CpuClassificationCalls);
|
|
Assert.Equal(8, diagnostics.Passes.Count);
|
|
Assert.Contains(diagnostics.Passes, pass =>
|
|
pass.PassId == RenderPackPerformanceScopeNames.EnhancedWorldReceiver);
|
|
Assert.Contains(diagnostics.Passes, pass =>
|
|
pass.PassId == VolumetricShaftRenderer.TimerName
|
|
&& pass.DrawCalls == 0);
|
|
Assert.True(
|
|
diagnostics.RetainedGpuBytes
|
|
>= DirectionalShadowQuality.For(DirectionalShadowPreset.Medium)
|
|
.ApproximateDepthMapBytes);
|
|
}
|
|
|
|
[Fact]
|
|
public void CurrentShadowRunsShaftsBeforeBloomAndFeedsBloomAndFilmicComposition()
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
using var graph = Graph(device, "medium");
|
|
IGpuRenderTarget world = graph.PrepareWorldTarget(1280, 720, 1);
|
|
|
|
using IGpuFrame frame = device.BeginFrame();
|
|
PublishCurrentShadow(graph, frame);
|
|
device.Clear();
|
|
RecordWorldPass(frame, world);
|
|
AtmosphericFrameInputs inputs = Inputs(1280, 720);
|
|
graph.RenderPostProcess(frame, in inputs);
|
|
frame.End();
|
|
|
|
Assert.Equal(
|
|
[
|
|
"test-world-hdr",
|
|
"atmospheric-sun-occlusion",
|
|
"atmospheric-sun-rays",
|
|
VolumetricShaftRenderer.TimerName,
|
|
"atmospheric-bloom-downsample",
|
|
"atmospheric-bloom-blur-horizontal",
|
|
"atmospheric-bloom-blur-vertical",
|
|
"atmospheric-filmic",
|
|
],
|
|
device.OfKind<GpuRecordedPassBegin>().Select(call => call.Name));
|
|
RenderPassSemantic[] declaredOrder = graph.Descriptor.Passes
|
|
.Where(pass => pass.Hook is RenderPassHook.AtmosphereBeforeToneMap
|
|
or RenderPassHook.ToneMap)
|
|
.Select(pass => pass.Semantic)
|
|
.ToArray();
|
|
RenderPassSemantic[] executedOrder = device.OfKind<GpuRecordedPassBegin>()
|
|
.Skip(1)
|
|
.Select(call => call.Name switch
|
|
{
|
|
"atmospheric-sun-occlusion" => RenderPassSemantic.SunOcclusion,
|
|
"atmospheric-sun-rays" => RenderPassSemantic.SunRays,
|
|
VolumetricShaftRenderer.TimerName => RenderPassSemantic.VolumetricShafts,
|
|
"atmospheric-bloom-downsample" => RenderPassSemantic.BloomDownsample,
|
|
"atmospheric-bloom-blur-horizontal" => RenderPassSemantic.BloomBlurHorizontal,
|
|
"atmospheric-bloom-blur-vertical" => RenderPassSemantic.BloomBlurVertical,
|
|
"atmospheric-filmic" => RenderPassSemantic.FilmicComposite,
|
|
_ => throw new InvalidOperationException($"Unexpected atmospheric pass '{call.Name}'."),
|
|
})
|
|
.ToArray();
|
|
Assert.Equal(declaredOrder, executedOrder);
|
|
RenderPassDeclaration bloom = Assert.Single(
|
|
graph.Descriptor.Passes,
|
|
value => value.Semantic == RenderPassSemantic.BloomDownsample);
|
|
RenderResourceSemantic[] bloomReads = bloom.ResourceReads
|
|
.Select(id => Assert.Single(
|
|
graph.Descriptor.Resources,
|
|
resource => string.Equals(resource.Id, id, StringComparison.OrdinalIgnoreCase)).Semantic)
|
|
.ToArray();
|
|
Assert.Equal(
|
|
[RenderResourceSemantic.SunRays, RenderResourceSemantic.VolumetricShafts],
|
|
bloomReads);
|
|
GpuRecordedPushConstants[] pushes = device
|
|
.OfKind<GpuRecordedPushConstants>()
|
|
.ToArray();
|
|
Assert.Equal(7, pushes.Length);
|
|
Assert.NotEqual(uint.MaxValue, pushes[2].Constants.TextureIndexA);
|
|
Assert.Equal(uint.MaxValue, pushes[2].Constants.TextureIndexB);
|
|
Assert.Equal(uint.MaxValue, BitConverter.SingleToUInt32Bits(pushes[2].Constants.ParamA));
|
|
Assert.Equal(uint.MaxValue, BitConverter.SingleToUInt32Bits(pushes[2].Constants.ParamB));
|
|
Assert.NotEqual(uint.MaxValue, pushes[3].Constants.TextureIndexA);
|
|
Assert.NotEqual(uint.MaxValue, pushes[3].Constants.TextureIndexB);
|
|
Assert.NotEqual(uint.MaxValue, BitConverter.SingleToUInt32Bits(pushes[3].Constants.ParamA));
|
|
Assert.Equal(uint.MaxValue, BitConverter.SingleToUInt32Bits(pushes[3].Constants.ParamB));
|
|
Assert.Equal(
|
|
pushes[3].Constants.TextureIndexB,
|
|
BitConverter.SingleToUInt32Bits(pushes[^1].Constants.ParamA));
|
|
Assert.Equal(
|
|
BitConverter.SingleToUInt32Bits(pushes[3].Constants.ParamA),
|
|
BitConverter.SingleToUInt32Bits(pushes[^1].Constants.ParamB));
|
|
|
|
RenderPackRuntimeDiagnostics diagnostics = graph.CaptureDiagnostics();
|
|
Assert.Contains(diagnostics.Passes, pass =>
|
|
pass.PassId == VolumetricShaftRenderer.TimerName
|
|
&& pass.DrawCalls == 1);
|
|
Assert.Equal(7, diagnostics.DrawCalls);
|
|
Assert.Equal(8, diagnostics.ImageCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void NeutralSettingsReachShaderBlocksWithoutHiddenResidualEffects()
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
using var graph = Graph(
|
|
device,
|
|
"medium",
|
|
AtmosphericPostProcessSettings.Neutral);
|
|
IGpuRenderTarget world = graph.PrepareWorldTarget(800, 600, 1);
|
|
device.Clear();
|
|
|
|
using IGpuFrame frame = device.BeginFrame();
|
|
RecordWorldPass(frame, world);
|
|
AtmosphericFrameInputs inputs = Inputs(800, 600);
|
|
graph.RenderPostProcess(frame, in inputs);
|
|
frame.End();
|
|
|
|
GpuRecordedUniformBind[] passBlocks = device
|
|
.OfKind<GpuRecordedUniformBind>()
|
|
.Where(call => call.Binding == GpuBindingModel.UniformPackPass)
|
|
.ToArray();
|
|
AtmosphericPackPassUniforms bloom = ReadPass(device, passBlocks[2]);
|
|
AtmosphericPackPassUniforms filmic = ReadPass(device, passBlocks[^1]);
|
|
Assert.Equal(0f, bloom.Params0.X);
|
|
Assert.Equal(new Vector4(1f, 1f, 1f, 0f), filmic.Params0);
|
|
Assert.Equal(Vector4.Zero, filmic.Params1);
|
|
|
|
GpuRecordedUniformBind frameBlock = device
|
|
.OfKind<GpuRecordedUniformBind>()
|
|
.First(call => call.Binding == GpuBindingModel.UniformAtmosphericFrame);
|
|
AtmosphericFrameUniforms atmospheric = MemoryMarshal.Read<AtmosphericFrameUniforms>(
|
|
device.RingBytes.Slice((int)frameBlock.OffsetBytes, AtmosphericFrameUniforms.SizeInBytes));
|
|
Assert.Equal(0f, atmospheric.SunScreen.Z);
|
|
}
|
|
|
|
[Fact]
|
|
public void PerformanceSourceSumsOnlyResolvedPackPassTimersWithoutAllocatingDiagnostics()
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
using var graph = Graph(device, "medium");
|
|
IGpuRenderTarget world = graph.PrepareWorldTarget(800, 600, 1);
|
|
using IGpuFrame frame = device.BeginFrame();
|
|
RecordWorldPass(frame, world);
|
|
AtmosphericFrameInputs inputs = Inputs(800, 600);
|
|
graph.RenderPostProcess(frame, in inputs);
|
|
frame.End();
|
|
string[] names =
|
|
[
|
|
RenderPackPerformanceScopeNames.EnhancedWorldReceiver,
|
|
"atmospheric-sun-occlusion",
|
|
"atmospheric-sun-rays",
|
|
"atmospheric-bloom-downsample",
|
|
"atmospheric-bloom-blur-horizontal",
|
|
"atmospheric-bloom-blur-vertical",
|
|
"atmospheric-filmic",
|
|
];
|
|
for (int i = 0; i < names.Length; i++)
|
|
device.RecordingTimers.SetResolved(names[i], i + 1);
|
|
|
|
RenderPackRuntimePerformanceMetrics metrics =
|
|
graph.CapturePerformanceMetrics();
|
|
|
|
Assert.Equal(1, metrics.ResourceGeneration);
|
|
Assert.True(metrics.HasResolvedGpuMeasurement);
|
|
Assert.Equal(28, metrics.InclusiveResolvedGpuMilliseconds);
|
|
Assert.True(metrics.RetainedGpuBytes > 0);
|
|
Assert.Equal(0, metrics.TransientGpuBytes);
|
|
Assert.False(graph.CapturePerformanceMetrics().HasResolvedGpuMeasurement);
|
|
|
|
graph.PrepareWorldTarget(1024, 768, 4);
|
|
Assert.Equal(2, graph.CapturePerformanceMetrics().ResourceGeneration);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResizePublishesOneCompleteReplacementAndRetiresTheOldSet()
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
var graph = Graph(device, "medium");
|
|
int baselineSlots = device.LiveTextureSlotCount;
|
|
|
|
IGpuRenderTarget first = graph.PrepareWorldTarget(1280, 720, 4);
|
|
Assert.Equal(1, graph.ResourceGeneration);
|
|
Assert.Same(first, graph.PrepareWorldTarget(1280, 720, 4));
|
|
Assert.Equal(1, graph.ResourceGeneration);
|
|
Assert.Equal(baselineSlots + 7, device.LiveTextureSlotCount);
|
|
RecordingGpuRenderTarget[] firstSet = device.CreatedRenderTargets.ToArray();
|
|
|
|
IGpuRenderTarget second = graph.PrepareWorldTarget(1920, 1080, 1);
|
|
|
|
Assert.NotSame(first, second);
|
|
Assert.Equal(2, graph.ResourceGeneration);
|
|
Assert.All(firstSet, target => Assert.True(target.IsDisposed));
|
|
Assert.Equal(baselineSlots + 7, device.LiveTextureSlotCount);
|
|
graph.Dispose();
|
|
Assert.Equal(baselineSlots - 1, device.LiveTextureSlotCount);
|
|
Assert.Empty(device.PipelineFormatLeases);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("low", 320, 180)]
|
|
[InlineData("medium", 640, 360)]
|
|
[InlineData("high", 640, 360)]
|
|
public void PresetDeclaredRayScaleControlsMaskAndRayTargets(
|
|
string presetId,
|
|
int expectedWidth,
|
|
int expectedHeight)
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
using var graph = Graph(device, presetId);
|
|
|
|
graph.PrepareWorldTarget(1280, 720, 1);
|
|
|
|
RecordingGpuRenderTarget mask = Assert.Single(device.CreatedRenderTargets, target =>
|
|
target.Description.Name == "atmospheric-sun-mask");
|
|
RecordingGpuRenderTarget rays = Assert.Single(device.CreatedRenderTargets, target =>
|
|
target.Description.Name == "atmospheric-sun-rays");
|
|
Assert.Equal(expectedWidth, mask.Description.Width);
|
|
Assert.Equal(expectedHeight, mask.Description.Height);
|
|
Assert.Equal(expectedWidth, rays.Description.Width);
|
|
Assert.Equal(expectedHeight, rays.Description.Height);
|
|
}
|
|
|
|
[Fact]
|
|
public void PartialTargetAllocationFailureRollsBackAndCanBuildFreshCandidate()
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
using var graph = Graph(device, "medium");
|
|
int baselineSlots = device.LiveTextureSlotCount;
|
|
int allocation = 0;
|
|
device.RenderTargetFailure = _ => ++allocation == 3
|
|
? new InvalidOperationException("injected target failure")
|
|
: null;
|
|
|
|
InvalidOperationException failure = Assert.Throws<InvalidOperationException>(
|
|
() => graph.PrepareWorldTarget(1024, 768, 4));
|
|
|
|
Assert.Equal("injected target failure", failure.Message);
|
|
Assert.Equal(0, graph.ResourceGeneration);
|
|
Assert.Equal(baselineSlots, device.LiveTextureSlotCount);
|
|
Assert.All(device.CreatedRenderTargets, target => Assert.True(target.IsDisposed));
|
|
|
|
device.RenderTargetFailure = null;
|
|
IGpuRenderTarget recovered = graph.PrepareWorldTarget(1024, 768, 4);
|
|
Assert.Equal(GpuTextureFormat.Rgba16FloatRenderTarget, recovered.Description.ColorFormat);
|
|
Assert.Equal(1, graph.ResourceGeneration);
|
|
Assert.Equal(baselineSlots + 7, device.LiveTextureSlotCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void DescriptorPassAssetsAndPresetOverridesDriveTheRuntime()
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
using var graph = Graph(device, "low");
|
|
|
|
Assert.Equal(0.4f, graph.Settings.SunRayStrength);
|
|
Assert.Equal(
|
|
[
|
|
"acdream.atmospheric:sun-occlusion",
|
|
"acdream.atmospheric:sun-rays",
|
|
"acdream.atmospheric:bloom-downsample",
|
|
"acdream.atmospheric:bloom-blur-horizontal",
|
|
"acdream.atmospheric:filmic-composite",
|
|
"acdream.atmospheric:terrain-shadow-caster",
|
|
"acdream.atmospheric:world-shadow-opaque",
|
|
"acdream.atmospheric:world-shadow-cutout",
|
|
"acdream.atmospheric:terrain-shadow-caster-multiview",
|
|
"acdream.atmospheric:world-shadow-opaque-multiview",
|
|
"acdream.atmospheric:world-shadow-cutout-multiview",
|
|
"acdream.atmospheric:volumetric-shafts",
|
|
],
|
|
device.CreatedPipelines.Select(pipeline => pipeline.Description.Shaders.Name));
|
|
Assert.All(
|
|
device.CreatedPipelines,
|
|
pipeline => Assert.True(pipeline.Description.Shaders.HasEmbeddedSpirv));
|
|
Assert.Equal(1, device.PipelineFormatLeases[GpuTextureFormat.Rgba16FloatRenderTarget]);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuiltInSampleCountDeclarationsMatchTheExecutorExactly()
|
|
{
|
|
RenderPackDescriptor descriptor = BuiltInAtmosphericRenderPack.Descriptor;
|
|
RenderSettingDeclaration pcf = Assert.Single(descriptor.Settings, value =>
|
|
value.Semantic == RenderSettingSemantic.DirectionalShadowPcfTaps);
|
|
RenderSettingDeclaration volumetric = Assert.Single(descriptor.Settings, value =>
|
|
value.Semantic == RenderSettingSemantic.VolumetricRayMarchSteps);
|
|
|
|
Assert.Equal(RenderSettingKind.Choice, pcf.Kind);
|
|
Assert.Equal(["1", "9", "25"], pcf.Choices);
|
|
Assert.Equal("9", pcf.DefaultValue);
|
|
Assert.Equal(RenderSettingKind.Integer, volumetric.Kind);
|
|
Assert.Equal(8, volumetric.Minimum);
|
|
Assert.Equal(64, volumetric.Maximum);
|
|
Assert.Equal(8, volumetric.Step);
|
|
}
|
|
|
|
[Fact]
|
|
public void ShadowFilterRejectsAnUndeclaredIntermediateSampleCount()
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
RenderPackDescriptor descriptor = BuiltInAtmosphericRenderPack.Descriptor;
|
|
RenderQualityPreset preset = Assert.Single(descriptor.QualityPresets, value =>
|
|
value.Semantic == RenderQualitySemantic.Medium);
|
|
string settingId = Assert.Single(descriptor.Settings, value =>
|
|
value.Semantic == RenderSettingSemantic.DirectionalShadowPcfTaps).Id;
|
|
|
|
NotSupportedException error = Assert.Throws<NotSupportedException>(() =>
|
|
new AtmosphericPostProcessGraph(
|
|
device,
|
|
descriptor,
|
|
BuiltInAssets(),
|
|
preset,
|
|
userSettingOverrides: new Dictionary<string, string>
|
|
{
|
|
[settingId] = "3",
|
|
}));
|
|
|
|
Assert.Contains("exactly 1, 9, or 25", error.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void SemanticPresetResourcesAndSettingsDriveShadowAndVolumetricQuality()
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
RenderPackDescriptor source = BuiltInAtmosphericRenderPack.Descriptor;
|
|
RenderResourceDeclaration shadowResource = Assert.Single(
|
|
source.Resources,
|
|
value => value.Semantic == RenderResourceSemantic.DirectionalShadowDepth);
|
|
RenderResourceDeclaration volumetricResource = Assert.Single(
|
|
source.Resources,
|
|
value => value.Semantic == RenderResourceSemantic.VolumetricShafts);
|
|
RenderQualityPreset original = Assert.Single(
|
|
source.QualityPresets,
|
|
value => value.Semantic == RenderQualitySemantic.Medium);
|
|
RenderQualityPreset preset = original with
|
|
{
|
|
ResourceOverrides = original.ResourceOverrides.Select(value =>
|
|
string.Equals(value.ResourceId, shadowResource.Id, StringComparison.OrdinalIgnoreCase)
|
|
? value with
|
|
{
|
|
Extent = new RenderExtentDeclaration(
|
|
RenderExtentMode.AbsolutePixels,
|
|
768,
|
|
768,
|
|
Layers: 2),
|
|
EstimatedResidentBytes = 2L * 768 * 768 * sizeof(float),
|
|
}
|
|
: string.Equals(value.ResourceId, volumetricResource.Id,
|
|
StringComparison.OrdinalIgnoreCase)
|
|
? value with
|
|
{
|
|
Extent = new RenderExtentDeclaration(
|
|
RenderExtentMode.RelativeToMainWorld,
|
|
0.375,
|
|
0.375),
|
|
}
|
|
: value).ToArray(),
|
|
};
|
|
string SettingId(RenderSettingSemantic semantic) => Assert.Single(
|
|
source.Settings,
|
|
value => value.Semantic == semantic).Id;
|
|
var overrides = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
[SettingId(RenderSettingSemantic.DirectionalShadowReachMetres)] = "96",
|
|
[SettingId(RenderSettingSemantic.DirectionalShadowPcfTaps)] = "25",
|
|
[SettingId(RenderSettingSemantic.VolumetricRayMarchSteps)] = "64",
|
|
};
|
|
|
|
using var graph = new AtmosphericPostProcessGraph(
|
|
device,
|
|
source,
|
|
BuiltInAssets(),
|
|
preset,
|
|
userSettingOverrides: overrides);
|
|
var shadows = Assert.IsType<DirectionalSunShadowRenderer>(
|
|
graph.DirectionalShadowReceivers);
|
|
Assert.Equal(2, shadows.Quality.CascadeCount);
|
|
Assert.Equal(768, shadows.Quality.MapResolution);
|
|
Assert.Equal(96f, shadows.Quality.MaximumReachMeters);
|
|
Assert.Equal(2, shadows.Quality.PcfRadiusTexels);
|
|
Assert.Equal(64, graph.VolumetricQuality?.RayMarchSteps);
|
|
Assert.Equal(0.375f, graph.VolumetricQuality?.ResolutionScale);
|
|
|
|
graph.PrepareWorldTarget(800, 600, 1);
|
|
RecordingGpuRenderTarget volumetric = Assert.Single(
|
|
device.CreatedRenderTargets,
|
|
value => value.Description.Name == "atmospheric-volumetric");
|
|
Assert.Equal(300, volumetric.Description.Width);
|
|
Assert.Equal(225, volumetric.Description.Height);
|
|
}
|
|
|
|
[Fact]
|
|
public void AuthoredElevationDayGroupAndVisibilityGateSunEffects()
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
using var graph = Graph(device, "medium");
|
|
|
|
AtmosphericFrameInputs dawn = Inputs(1280, 720, elevation: 4f, activeDayGroup: 0);
|
|
AtmosphericFrameInputs dusk = dawn with { ActiveDayGroup = 1 };
|
|
AtmosphericFrameInputs noon = dawn with { SunElevationDegrees = 55f };
|
|
AtmosphericFrameInputs behindCamera = dawn with { SunIsOnScreen = false };
|
|
AtmosphericFrameInputs overcast = dawn with
|
|
{
|
|
Weather = WeatherKind.Overcast,
|
|
WeatherIntensity = 1f,
|
|
};
|
|
AtmosphericFrameInputs indoor = dawn with { IsOutdoor = false };
|
|
|
|
Assert.Equal(1f, graph.EvaluateSunPolicy(in dawn), 3);
|
|
Assert.Equal(0.35f, graph.EvaluateSunPolicy(in dusk), 3);
|
|
Assert.Equal(0f, graph.EvaluateSunPolicy(in noon));
|
|
Assert.Equal(0f, graph.EvaluateSunPolicy(in behindCamera));
|
|
Assert.Equal(0.18f, graph.EvaluateSunPolicy(in overcast), 3);
|
|
Assert.Equal(0f, graph.EvaluateSunPolicy(in indoor));
|
|
}
|
|
|
|
[Fact]
|
|
public void VolumetricPipelineFailureRollsBackGraphCandidate()
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
int baselineSlots = device.LiveTextureSlotCount;
|
|
device.PipelineFailure = description => description.Name.Contains(
|
|
"volumetric-shafts",
|
|
StringComparison.Ordinal)
|
|
? new InvalidOperationException("volumetric pipeline failed")
|
|
: null;
|
|
|
|
InvalidOperationException failure = Assert.Throws<InvalidOperationException>(() =>
|
|
Graph(device, "medium"));
|
|
|
|
Assert.Equal("volumetric pipeline failed", failure.Message);
|
|
Assert.Equal(baselineSlots, device.LiveTextureSlotCount);
|
|
Assert.Empty(device.PipelineFormatLeases);
|
|
Assert.All(device.CreatedPipelines, pipeline => Assert.True(pipeline.IsDisposed));
|
|
Assert.All(device.CreatedDirectionalDepthTargets, target => Assert.True(target.IsDisposed));
|
|
}
|
|
|
|
[Fact]
|
|
public void ExternalTierTwoPackCanRenameEveryOwnedIdAndShaderAsset()
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
RenderPackDescriptor external = RenamedExternalTierTwoDescriptor();
|
|
Assert.True(
|
|
RenderPackValidator.ValidateDescriptor(
|
|
external,
|
|
RenderPackHostCapabilities.Conformance).Success);
|
|
RenderQualityPreset preset = Assert.Single(
|
|
external.QualityPresets,
|
|
value => value.Semantic == RenderQualitySemantic.Medium);
|
|
var factory = new AtmosphericRenderPackRuntimeFactory(device);
|
|
|
|
using IRenderPackRuntime runtime = factory.Build(
|
|
external,
|
|
new RenamedShaderAssets(BuiltInAssets()),
|
|
preset,
|
|
RenderPackSettingOverrides.Empty);
|
|
|
|
AtmosphericPostProcessGraph graph = Assert.IsType<AtmosphericPostProcessGraph>(runtime);
|
|
_ = graph.PrepareWorldTarget(1280, 720, 1);
|
|
Assert.All(device.CreatedPipelines, pipeline =>
|
|
{
|
|
Assert.StartsWith("example.external-atmosphere:", pipeline.Description.Shaders.Name);
|
|
Assert.True(pipeline.Description.Shaders.HasEmbeddedSpirv);
|
|
});
|
|
Assert.DoesNotContain(external.Passes, value =>
|
|
BuiltInAtmosphericRenderPack.Descriptor.Passes.Any(original =>
|
|
string.Equals(original.Id, value.Id, StringComparison.Ordinal)));
|
|
Assert.All(external.Passes, value => Assert.StartsWith("external/", value.VertexShaderAsset));
|
|
Assert.All(external.PipelineVariants, value =>
|
|
Assert.StartsWith("external/", value.FragmentShaderAsset));
|
|
Assert.Same(external, graph.Descriptor);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExternalNoOpPackNeedsNoRendererPrivateRuntime()
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
RenderPackDescriptor descriptor = BuiltInAtmosphericRenderPack.Descriptor with
|
|
{
|
|
Id = "example.no-op",
|
|
Passes = [],
|
|
SceneReplays = [],
|
|
PipelineVariants = [],
|
|
};
|
|
RenderQualityPreset preset = descriptor.QualityPresets[0];
|
|
var factory = new AtmosphericRenderPackRuntimeFactory(device);
|
|
|
|
using IRenderPackRuntime runtime = factory.Build(
|
|
descriptor,
|
|
new RejectingAssets(),
|
|
preset,
|
|
RenderPackSettingOverrides.Empty);
|
|
|
|
Assert.IsType<NoOpRenderPackRuntime>(runtime);
|
|
Assert.Empty(device.CreatedPipelines);
|
|
Assert.Empty(device.PipelineFormatLeases);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExternalTierOneFullscreenGraphSchedulesArbitraryDeclaredPassIdsAndResource()
|
|
{
|
|
var device = new RecordingGpuDevice();
|
|
RenderPackDescriptor descriptor = ExternalTierOneDescriptor();
|
|
RenderQualityPreset preset = Assert.Single(descriptor.QualityPresets);
|
|
var factory = new AtmosphericRenderPackRuntimeFactory(device);
|
|
using IRenderPackRuntime runtime = factory.Build(
|
|
descriptor,
|
|
BuiltInAssets(),
|
|
preset,
|
|
RenderPackSettingOverrides.Empty);
|
|
var graph = Assert.IsType<DeclaredFullscreenRenderPackGraph>(runtime);
|
|
IGpuRenderTarget world = graph.PrepareWorldTarget(1000, 600, 1);
|
|
device.Clear();
|
|
|
|
using IGpuFrame frame = device.BeginFrame();
|
|
RecordWorldPass(frame, world);
|
|
AtmosphericFrameInputs inputs = Inputs(1000, 600);
|
|
graph.RenderPostProcess(frame, in inputs);
|
|
frame.End();
|
|
|
|
Assert.Equal(
|
|
["test-world-hdr", "render-pack-example.generic-tier1-my-threshold",
|
|
"render-pack-example.generic-tier1-my-output"],
|
|
device.OfKind<GpuRecordedPassBegin>().Select(call => call.Name));
|
|
Assert.All(device.CreatedPipelines, pipeline =>
|
|
Assert.True(pipeline.Description.Shaders.HasEmbeddedSpirv));
|
|
Assert.Contains(device.CreatedRenderTargets, target =>
|
|
target.Description.Name.EndsWith("custom-half", StringComparison.Ordinal)
|
|
&& target.Description.Width == 500
|
|
&& target.Description.Height == 300);
|
|
|
|
device.RecordingTimers.SetResolved(
|
|
"render-pack-example.generic-tier1-my-threshold",
|
|
1.25);
|
|
device.RecordingTimers.SetResolved(
|
|
"render-pack-example.generic-tier1-my-output",
|
|
2.75);
|
|
RenderPackRuntimePerformanceMetrics metrics = graph.CapturePerformanceMetrics();
|
|
Assert.Equal(1, metrics.ResourceGeneration);
|
|
Assert.True(metrics.HasResolvedGpuMeasurement);
|
|
Assert.Equal(4, metrics.InclusiveResolvedGpuMilliseconds);
|
|
Assert.True(metrics.RetainedGpuBytes > 0);
|
|
Assert.False(graph.CapturePerformanceMetrics().HasResolvedGpuMeasurement);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExternalTierOnePolicyAndCompleteRuntimeDiagnosticsReachTheController()
|
|
{
|
|
var policy = new AtmospherePolicyDeclaration(
|
|
[
|
|
new SunElevationResponsePoint(-10, 0.2),
|
|
new SunElevationResponsePoint(10, 0.8),
|
|
],
|
|
[new ActiveDayGroupMultiplier(7, 0.4)]);
|
|
RenderPackDescriptor descriptor = ExternalTierOneDescriptor(policy);
|
|
var device = new RecordingGpuDevice();
|
|
using var registry = new BufferedRenderPackRegistry();
|
|
using IDisposable registration = registry.Register(descriptor, BuiltInAssets());
|
|
using var controller = new RenderPackController(
|
|
() => RenderPackCatalog.Build(
|
|
registry.Snapshot(),
|
|
RenderPackHostCapabilities.Conformance),
|
|
new AtmosphericRenderPackRuntimeFactory(device),
|
|
preparationScheduler: InlineRenderPackPreparationScheduler.Instance);
|
|
controller.Request(new RenderPackSelectionSettings(
|
|
descriptor.Id,
|
|
descriptor.PackVersion.ToString(),
|
|
"default"));
|
|
|
|
RenderPackActivationSnapshot activation = controller.ApplyAtFrameBoundary(
|
|
new RenderPackActivationExtent(1000, 600, 1));
|
|
var graph = Assert.IsType<DeclaredFullscreenRenderPackGraph>(
|
|
controller.ActiveRuntime);
|
|
IGpuRenderTarget world = graph.PrepareWorldTarget(1000, 600, 1);
|
|
device.Clear();
|
|
|
|
using IGpuFrame frame = device.BeginFrame();
|
|
RecordWorldPass(frame, world);
|
|
AtmosphericFrameInputs inputs = Inputs(
|
|
1000,
|
|
600,
|
|
elevation: 0f,
|
|
activeDayGroup: 7);
|
|
graph.RenderPostProcess(frame, in inputs);
|
|
frame.End();
|
|
|
|
GpuRecordedUniformBind frameBlock = device
|
|
.OfKind<GpuRecordedUniformBind>()
|
|
.First(call => call.Binding == GpuBindingModel.UniformAtmosphericFrame);
|
|
AtmosphericFrameUniforms atmosphere = MemoryMarshal.Read<AtmosphericFrameUniforms>(
|
|
device.RingBytes.Slice(
|
|
(int)frameBlock.OffsetBytes,
|
|
AtmosphericFrameUniforms.SizeInBytes));
|
|
Assert.Equal(new Vector4(7f, 0.4f, 0.5f, 0f), atmosphere.Policy);
|
|
Assert.Equal(0.2f, atmosphere.SunScreen.Z, 3);
|
|
Assert.Equal(0.2f, atmosphere.SunColor.W, 3);
|
|
|
|
device.RecordingTimers.SetResolved(
|
|
"render-pack-example.generic-tier1-my-threshold",
|
|
1.25);
|
|
device.RecordingTimers.SetResolved(
|
|
"render-pack-example.generic-tier1-my-output",
|
|
2.75);
|
|
RenderPackDiagnosticsSnapshot diagnostics = controller.CaptureDiagnostics();
|
|
|
|
Assert.Equal(RenderPackActivationState.Active, activation.State);
|
|
Assert.Equal("example.generic-tier1", diagnostics.PackId);
|
|
Assert.Equal("default", diagnostics.EffectiveQuality);
|
|
Assert.Equal(8_400_000L, diagnostics.RetainedGpuBytes);
|
|
Assert.Equal(0L, diagnostics.TransientGpuBytes);
|
|
Assert.Equal(3, diagnostics.ImageCount);
|
|
Assert.Equal(0, diagnostics.BufferCount);
|
|
Assert.Equal(2, diagnostics.DrawCalls);
|
|
Assert.Equal(0, diagnostics.DispatchCalls);
|
|
Assert.Equal(0, diagnostics.ShadowCasterCount);
|
|
Assert.Equal(0, diagnostics.CascadeDrawCount);
|
|
Assert.Equal(0, diagnostics.CpuClassificationCalls);
|
|
Assert.Equal(0, diagnostics.SunElevationDegrees);
|
|
Assert.Equal(7, diagnostics.ActiveDayGroup);
|
|
Assert.Equal(WeatherKind.Clear.ToString(), diagnostics.Weather);
|
|
Assert.Equal(0, diagnostics.WeatherIntensity);
|
|
Assert.True(diagnostics.Outdoor);
|
|
Assert.Equal(0, diagnostics.DirectionalShadowStrength);
|
|
Assert.Collection(
|
|
diagnostics.Passes,
|
|
pass =>
|
|
{
|
|
Assert.Equal("my-threshold", pass.PassId);
|
|
Assert.Equal(1.25, pass.GpuMilliseconds);
|
|
Assert.Equal(1, pass.DrawCalls);
|
|
Assert.Equal(0, pass.DispatchCalls);
|
|
},
|
|
pass =>
|
|
{
|
|
Assert.Equal("my-output", pass.PassId);
|
|
Assert.Equal(2.75, pass.GpuMilliseconds);
|
|
Assert.Equal(1, pass.DrawCalls);
|
|
Assert.Equal(0, pass.DispatchCalls);
|
|
});
|
|
string formatted = RenderPackDiagnosticsFormatter.Format(diagnostics);
|
|
Assert.Contains("resources=3i/0b", formatted, StringComparison.Ordinal);
|
|
Assert.Contains("worldTransforms=0used", formatted, StringComparison.Ordinal);
|
|
Assert.Contains("my-threshold:1.250ms/1d/0c", formatted, StringComparison.Ordinal);
|
|
Assert.Contains("atmosphere=0.00deg/day7/Clear:0.000/outdoor=True", formatted,
|
|
StringComparison.Ordinal);
|
|
RenderPackRuntimePerformanceMetrics performance = graph.CapturePerformanceMetrics();
|
|
Assert.True(performance.HasResolvedGpuMeasurement);
|
|
Assert.Equal(4, performance.InclusiveResolvedGpuMilliseconds);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExternalShadowsOnlyTierTwoValidatesAndExecutesDeclaredMovingCelestialGraph()
|
|
{
|
|
var policy = new AtmospherePolicyDeclaration(
|
|
[
|
|
new SunElevationResponsePoint(-90, 1),
|
|
new SunElevationResponsePoint(90, 1),
|
|
],
|
|
[new ActiveDayGroupMultiplier(7, 0.4)])
|
|
{
|
|
DirectionalShadowLightElevationResponse =
|
|
[
|
|
new SunElevationResponsePoint(-90, 0),
|
|
new SunElevationResponsePoint(0, 0),
|
|
new SunElevationResponsePoint(10, 0.1),
|
|
new SunElevationResponsePoint(20, 0.3),
|
|
new SunElevationResponsePoint(90, 0.3),
|
|
],
|
|
VolumetricShaftSunElevationResponse =
|
|
[
|
|
new SunElevationResponsePoint(-90, 0),
|
|
new SunElevationResponsePoint(10, 0.8),
|
|
new SunElevationResponsePoint(20, 0.4),
|
|
new SunElevationResponsePoint(90, 0),
|
|
],
|
|
};
|
|
RenderPackDescriptor descriptor = ExternalShadowsOnlyTierTwoDescriptor(policy);
|
|
RenderPackValidationResult validation = RenderPackValidator.ValidateDescriptor(
|
|
descriptor,
|
|
RenderPackHostCapabilities.Conformance);
|
|
Assert.True(validation.Success, validation.Reason);
|
|
RenderQualityPreset preset = Assert.Single(descriptor.QualityPresets);
|
|
var device = new RecordingGpuDevice();
|
|
var factory = new AtmosphericRenderPackRuntimeFactory(device);
|
|
|
|
using IRenderPackRuntime runtime = factory.Build(
|
|
descriptor,
|
|
BuiltInAssets(),
|
|
preset,
|
|
RenderPackSettingOverrides.Empty);
|
|
var graph = Assert.IsType<DeclaredDirectionalShadowRenderPackGraph>(runtime);
|
|
Assert.False(Assert.IsType<DirectionalSunShadowRenderer>(
|
|
graph.DirectionalShadowReceivers).MultiviewCascadesEnabled);
|
|
IGpuRenderTarget world = graph.PrepareWorldTarget(1000, 600, 1);
|
|
device.Clear();
|
|
|
|
using IGpuFrame frame = device.BeginFrame();
|
|
PublishCurrentShadow(graph.DirectionalShadowReceivers, frame);
|
|
RecordWorldPass(frame, world);
|
|
AtmosphericFrameInputs inputs = Inputs(
|
|
1000,
|
|
600,
|
|
elevation: 15f,
|
|
activeDayGroup: 7);
|
|
graph.RenderPostProcess(frame, in inputs);
|
|
frame.End();
|
|
|
|
GpuRecordedUniformBind frameBlock = device
|
|
.OfKind<GpuRecordedUniformBind>()
|
|
.First(call => call.Binding == GpuBindingModel.UniformAtmosphericFrame);
|
|
AtmosphericFrameUniforms atmosphere = MemoryMarshal.Read<AtmosphericFrameUniforms>(
|
|
device.RingBytes.Slice(
|
|
(int)frameBlock.OffsetBytes,
|
|
AtmosphericFrameUniforms.SizeInBytes));
|
|
Assert.Equal(7f, atmosphere.Policy.X);
|
|
Assert.Equal(0.4f, atmosphere.Policy.Y, 3);
|
|
Assert.Equal(0.20117f, atmosphere.Policy.Z, 5);
|
|
Assert.Equal(0.6f, atmosphere.Policy.W, 3);
|
|
Assert.Equal(0.4f, atmosphere.SunScreen.Z, 3);
|
|
Assert.Equal(0.4f, atmosphere.SunColor.W, 3);
|
|
Assert.Single(descriptor.Passes, value =>
|
|
value.Semantic == RenderPassSemantic.DirectionalShadowDepth);
|
|
Assert.DoesNotContain(descriptor.Passes, value => value.Semantic is
|
|
RenderPassSemantic.BloomDownsample
|
|
or RenderPassSemantic.BloomBlurHorizontal
|
|
or RenderPassSemantic.BloomBlurVertical
|
|
or RenderPassSemantic.SunOcclusion
|
|
or RenderPassSemantic.SunRays
|
|
or RenderPassSemantic.VolumetricShafts
|
|
or RenderPassSemantic.FilmicComposite);
|
|
Assert.Contains(device.OfKind<GpuRecordedPassBegin>(), value =>
|
|
value.Name == "render-pack-example.shadows-only-output-copy");
|
|
}
|
|
|
|
[Fact]
|
|
public void ExternalShadowsOnlyTierTwoFailsSafeWithoutCasterReplayOrDeclaredCurve()
|
|
{
|
|
AtmospherePolicyDeclaration policy = BuiltInAtmosphericRenderPack.Descriptor
|
|
.AtmospherePolicy!;
|
|
RenderPackDescriptor descriptor = ExternalShadowsOnlyTierTwoDescriptor(policy);
|
|
|
|
RenderPackValidationResult missingReplay = RenderPackValidator.ValidateDescriptor(
|
|
descriptor with { SceneReplays = [] },
|
|
RenderPackHostCapabilities.Conformance);
|
|
RenderPackValidationResult missingCurve = RenderPackValidator.ValidateDescriptor(
|
|
descriptor with
|
|
{
|
|
AtmospherePolicy = policy with
|
|
{
|
|
DirectionalShadowLightElevationResponse = [],
|
|
},
|
|
},
|
|
RenderPackHostCapabilities.Conformance);
|
|
|
|
Assert.False(missingReplay.Success);
|
|
Assert.Contains("exactly one outdoor directional-shadow replay", missingReplay.Reason,
|
|
StringComparison.Ordinal);
|
|
Assert.False(missingCurve.Success);
|
|
Assert.Contains("directional-shadow light-elevation response curve", missingCurve.Reason,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExternalDirectionalShadowCurveMustRemainZeroAtAndBelowAuthoredHorizon()
|
|
{
|
|
AtmospherePolicyDeclaration policy = BuiltInAtmosphericRenderPack.Descriptor
|
|
.AtmospherePolicy!;
|
|
RenderPackDescriptor descriptor = ExternalShadowsOnlyTierTwoDescriptor(policy);
|
|
Assert.True(RenderPackValidator.ValidateDescriptor(
|
|
descriptor,
|
|
RenderPackHostCapabilities.Conformance).Success);
|
|
|
|
IReadOnlyList<SunElevationResponsePoint>[] invalidCurves =
|
|
[
|
|
[
|
|
new SunElevationResponsePoint(-90, 0.1),
|
|
new SunElevationResponsePoint(0, 0),
|
|
new SunElevationResponsePoint(90, 1),
|
|
],
|
|
[
|
|
new SunElevationResponsePoint(-90, 0),
|
|
new SunElevationResponsePoint(90, 1),
|
|
],
|
|
[
|
|
new SunElevationResponsePoint(0, 0.1),
|
|
new SunElevationResponsePoint(90, 1),
|
|
],
|
|
];
|
|
|
|
foreach (IReadOnlyList<SunElevationResponsePoint> invalidCurve in invalidCurves)
|
|
{
|
|
RenderPackValidationResult result = RenderPackValidator.ValidateDescriptor(
|
|
descriptor with
|
|
{
|
|
AtmospherePolicy = policy with
|
|
{
|
|
DirectionalShadowLightElevationResponse = invalidCurve,
|
|
},
|
|
},
|
|
RenderPackHostCapabilities.Conformance);
|
|
|
|
Assert.False(result.Success);
|
|
Assert.Contains("zero at and below the 0-degree authored horizon", result.Reason,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
RenderPackValidationResult clampedZero = RenderPackValidator.ValidateDescriptor(
|
|
descriptor with
|
|
{
|
|
AtmospherePolicy = policy with
|
|
{
|
|
DirectionalShadowLightElevationResponse =
|
|
[
|
|
new SunElevationResponsePoint(1, 0),
|
|
new SunElevationResponsePoint(12, 1),
|
|
new SunElevationResponsePoint(90, 1),
|
|
],
|
|
},
|
|
},
|
|
RenderPackHostCapabilities.Conformance);
|
|
Assert.True(clampedZero.Success, clampedZero.Reason);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuiltInShadowStrengthUsesTheDescriptorCurveExactlyOnce()
|
|
{
|
|
RenderPackDescriptor source = BuiltInAtmosphericRenderPack.Descriptor;
|
|
RenderQualityPreset medium = Assert.Single(source.QualityPresets, value =>
|
|
value.Semantic == RenderQualitySemantic.Medium);
|
|
using var baseline = new AtmosphericPostProcessGraph(
|
|
new RecordingGpuDevice(),
|
|
source,
|
|
BuiltInAssets(),
|
|
medium);
|
|
const float elevation = 6f;
|
|
float expectedLegacyElevation =
|
|
(MathF.Sin(elevation * MathF.PI / 180f) - MathF.Sin(MathF.PI / 180f))
|
|
/ (MathF.Sin(12f * MathF.PI / 180f) - MathF.Sin(MathF.PI / 180f));
|
|
Assert.Equal(
|
|
expectedLegacyElevation * 0.72f,
|
|
baseline.EvaluateDirectionalShadowStrength(elevation, activeDayGroup: 0),
|
|
5);
|
|
|
|
RenderPackDescriptor changed = source with
|
|
{
|
|
AtmospherePolicy = source.AtmospherePolicy! with
|
|
{
|
|
DirectionalShadowLightElevationResponse =
|
|
[
|
|
new SunElevationResponsePoint(-90, 0.25),
|
|
new SunElevationResponsePoint(90, 0.25),
|
|
],
|
|
},
|
|
};
|
|
using var declared = new AtmosphericPostProcessGraph(
|
|
new RecordingGpuDevice(),
|
|
changed,
|
|
BuiltInAssets(),
|
|
medium);
|
|
|
|
Assert.Equal(
|
|
0.25f * 0.72f,
|
|
declared.EvaluateDirectionalShadowStrength(elevation, activeDayGroup: 0),
|
|
5);
|
|
}
|
|
|
|
[Fact]
|
|
public void FilmicAndBloomDownsampleShadersKeepTheirColourSpaceConversions()
|
|
{
|
|
// Campaign VM VM3: a future edit to either shader could silently drop
|
|
// the decode/encode calls that make the post stack run in linear
|
|
// light. Pin their presence so that regresses loudly instead of
|
|
// quietly reintroducing F4 (gamma-space bloom/ACES/grade).
|
|
string shaderRoot = Path.Combine(
|
|
RepositoryRoot(),
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"Shaders");
|
|
string common = File.ReadAllText(Path.Combine(shaderRoot, "atmospheric_common.glsl"));
|
|
string filmic = File.ReadAllText(Path.Combine(shaderRoot, "atmospheric_filmic.frag"));
|
|
string downsample = File.ReadAllText(
|
|
Path.Combine(shaderRoot, "atmospheric_bloom_downsample.frag"));
|
|
|
|
int encodeInMainOutput = CountOccurrences(
|
|
filmic[filmic.IndexOf("void main()", StringComparison.Ordinal)..],
|
|
"acdreamEncodeDisplay(");
|
|
Assert.Equal(1, encodeInMainOutput);
|
|
Assert.Contains(
|
|
"oColor = vec4(acdreamEncodeDisplay(clamp(color, 0.0, 1.0)), 1.0);",
|
|
filmic,
|
|
StringComparison.Ordinal);
|
|
|
|
// Three in lowFusedScene (A/B/C) plus three in main's non-fused
|
|
// branch (A/C/D — sampleBloom's B is already linear).
|
|
Assert.Equal(6, CountOccurrences(filmic, "acdreamDecodeDisplay("));
|
|
|
|
int decodesInDownsample = CountOccurrences(downsample, "acdreamDecodeDisplay(");
|
|
Assert.True(
|
|
decodesInDownsample >= 3,
|
|
$"expected at least 3 acdreamDecodeDisplay( calls in atmospheric_bloom_downsample.frag, found {decodesInDownsample}");
|
|
|
|
// The contrast pivot and the decode/encode exponent are formatted
|
|
// from the C# mirror's own constants so the shader literal and the
|
|
// CPU-tested value cannot silently drift apart.
|
|
string gamma = AtmosphericColorPipeline.DisplayGamma.ToString(CultureInfo.InvariantCulture);
|
|
Assert.Contains($"vec3({gamma})", common, StringComparison.Ordinal);
|
|
Assert.Contains($"vec3(1.0 / {gamma})", common, StringComparison.Ordinal);
|
|
string pivot = AtmosphericColorPipeline.LinearMidGrey.ToString(CultureInfo.InvariantCulture);
|
|
Assert.Contains($"const float LinearMidGrey = {pivot}", filmic, StringComparison.Ordinal);
|
|
|
|
// The linear bloom threshold/knee the graph writes into Params0/2 are
|
|
// pinned by their exact literal values here too (AtmosphericColorPipelineTests
|
|
// separately proves they derive correctly from the pre-VM3 gamma pair).
|
|
Assert.Equal(0.73f, AtmosphericPostProcessGraph.BloomKneeLinear);
|
|
Assert.Equal(1f, AtmosphericPostProcessGraph.BloomThresholdLinear);
|
|
}
|
|
|
|
private static int CountOccurrences(string haystack, string needle)
|
|
{
|
|
int count = 0;
|
|
int index = 0;
|
|
while ((index = haystack.IndexOf(needle, index, StringComparison.Ordinal)) >= 0)
|
|
{
|
|
count++;
|
|
index += needle.Length;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
private static AtmosphericPostProcessGraph Graph(
|
|
RecordingGpuDevice device,
|
|
string presetId,
|
|
AtmosphericPostProcessSettings? settings = null)
|
|
{
|
|
RenderPackDescriptor descriptor = BuiltInAtmosphericRenderPack.Descriptor;
|
|
RenderQualityPreset preset = Assert.Single(
|
|
descriptor.QualityPresets,
|
|
value => string.Equals(value.Id, presetId, StringComparison.Ordinal));
|
|
return new AtmosphericPostProcessGraph(
|
|
device,
|
|
descriptor,
|
|
BuiltInAssets(),
|
|
preset,
|
|
settings);
|
|
}
|
|
|
|
private static RenderPackDescriptor ExternalTierOneDescriptor(
|
|
AtmospherePolicyDeclaration? policy = null)
|
|
{
|
|
var intermediate = new RenderResourceDeclaration(
|
|
"custom-half",
|
|
RenderResourceKind.Image2D,
|
|
RenderFormatClass.HdrColor,
|
|
new RenderExtentDeclaration(RenderExtentMode.RelativeToMainWorld, 0.5, 0.5),
|
|
SizeBytes: 0,
|
|
RenderResourceUsage.Sampled | RenderResourceUsage.ColorAttachment,
|
|
RenderResourceLifetime.ActivePack,
|
|
EstimatedResidentBytes: 8 * 1024 * 1024);
|
|
RenderPassDeclaration[] passes =
|
|
[
|
|
new RenderPassDeclaration(
|
|
"my-threshold",
|
|
RenderPassHook.AtmosphereBeforeToneMap,
|
|
"atmospheric_bloom_blur.vert.spv",
|
|
"atmospheric_bloom_blur.frag.spv",
|
|
[
|
|
RenderSemanticInput.WorldColor,
|
|
RenderSemanticInput.SunScreenPosition,
|
|
RenderSemanticInput.ActiveDayGroup,
|
|
RenderSemanticInput.Weather,
|
|
],
|
|
[],
|
|
[intermediate.Id]),
|
|
new RenderPassDeclaration(
|
|
"my-output",
|
|
RenderPassHook.ToneMap,
|
|
"atmospheric_bloom_blur.vert.spv",
|
|
"atmospheric_bloom_blur.frag.spv",
|
|
[],
|
|
[intermediate.Id],
|
|
[]),
|
|
];
|
|
var preset = new RenderQualityPreset(
|
|
"default", "Default", [], [], [],
|
|
32 * 1024 * 1024, 2, 3, 0.1, 0.2);
|
|
return BuiltInAtmosphericRenderPack.Descriptor with
|
|
{
|
|
Id = "example.generic-tier1",
|
|
DisplayName = "Generic Tier 1",
|
|
HighestTier = RenderPackTier.Tier1,
|
|
RequiredCapabilities =
|
|
[
|
|
RenderCapability.MainWorldColorIntermediate,
|
|
RenderCapability.FullscreenPasses,
|
|
RenderCapability.AuthoredSunScreenPosition,
|
|
RenderCapability.AuthoredWeather,
|
|
],
|
|
OptionalCapabilities = [],
|
|
Resources = [intermediate],
|
|
Passes = passes,
|
|
SceneReplays = [],
|
|
PipelineVariants = [],
|
|
QualityPresets = [preset],
|
|
Settings = [],
|
|
AtmospherePolicy = policy,
|
|
};
|
|
}
|
|
|
|
private static RenderPackDescriptor ExternalShadowsOnlyTierTwoDescriptor(
|
|
AtmospherePolicyDeclaration policy)
|
|
{
|
|
RenderPackDescriptor source = BuiltInAtmosphericRenderPack.Descriptor;
|
|
RenderResourceDeclaration shadowResource = source.Resources.Single(value =>
|
|
value.Semantic == RenderResourceSemantic.DirectionalShadowDepth) with
|
|
{
|
|
Id = "external-shadow-map",
|
|
};
|
|
RenderPassDeclaration shadowPass = source.Passes.Single(value =>
|
|
value.Semantic == RenderPassSemantic.DirectionalShadowDepth) with
|
|
{
|
|
Id = "external-shadow-depth",
|
|
ResourceWrites = [shadowResource.Id],
|
|
};
|
|
var outputCopy = new RenderPassDeclaration(
|
|
"output-copy",
|
|
RenderPassHook.ToneMap,
|
|
"atmospheric_bloom_blur.vert.spv",
|
|
"atmospheric_bloom_blur.frag.spv",
|
|
[RenderSemanticInput.WorldColor],
|
|
[],
|
|
[]);
|
|
RenderSettingSemantic[] settingSemantics =
|
|
[
|
|
RenderSettingSemantic.DirectionalShadowStrength,
|
|
RenderSettingSemantic.DirectionalShadowReachMetres,
|
|
RenderSettingSemantic.DirectionalShadowPcfTaps,
|
|
];
|
|
RenderSettingDeclaration[] settings = source.Settings
|
|
.Where(value => settingSemantics.Contains(value.Semantic))
|
|
.ToArray();
|
|
var preset = new RenderQualityPreset(
|
|
"medium",
|
|
"Medium",
|
|
[],
|
|
[],
|
|
[],
|
|
MaxResidentGpuBytes: 64L * 1024 * 1024,
|
|
MaxIncrementalGpuMillisecondsP50: 2.0,
|
|
MaxIncrementalGpuMillisecondsP99: 3.0,
|
|
MaxIncrementalCpuMillisecondsP50: 0.2,
|
|
MaxIncrementalCpuMillisecondsP99: 0.5)
|
|
{
|
|
Semantic = RenderQualitySemantic.Medium,
|
|
};
|
|
return new RenderPackDescriptor(
|
|
"example.shadows-only",
|
|
"External Shadows Only",
|
|
new Version(1, 0, 0),
|
|
RenderPackApi.Current,
|
|
RenderPackTier.Tier2,
|
|
[
|
|
RenderCapability.MainWorldColorIntermediate,
|
|
RenderCapability.FullscreenPasses,
|
|
RenderCapability.AuthoredSunDirection,
|
|
RenderCapability.AuthoredCelestialDirectionalLight,
|
|
RenderCapability.AuthoredWeather,
|
|
RenderCapability.DirectionalShadowMaps,
|
|
RenderCapability.OutdoorDirectionalShadowCasterReplay,
|
|
RenderCapability.AnimatedCasterTransforms,
|
|
RenderCapability.AlphaCutoutShadowCasters,
|
|
],
|
|
[RenderCapability.GpuTimestampQueries],
|
|
[shadowResource],
|
|
[shadowPass, outputCopy],
|
|
source.SceneReplays,
|
|
source.PipelineVariants.Where(value => value.Semantic is
|
|
RenderPipelineVariantSemantic.TerrainDirectionalShadowCaster
|
|
or RenderPipelineVariantSemantic.WorldOpaqueDirectionalShadowCaster
|
|
or RenderPipelineVariantSemantic.WorldAlphaCutoutDirectionalShadowCaster
|
|
or RenderPipelineVariantSemantic.TerrainDirectionalShadowReceiver
|
|
or RenderPipelineVariantSemantic.WorldDirectionalShadowReceiver).ToArray(),
|
|
[preset],
|
|
settings,
|
|
policy)
|
|
{
|
|
FeatureSummary = "Selected-celestial shadows with an HDR output copy and no post stack.",
|
|
};
|
|
}
|
|
|
|
private static void RecordWorldPass(IGpuFrame frame, IGpuRenderTarget world)
|
|
{
|
|
using IGpuPassEncoder _ = frame.BeginPass(new GpuPassDescription
|
|
{
|
|
Name = "test-world-hdr",
|
|
Color = new GpuColorAttachment(
|
|
world,
|
|
GpuLoadOp.Clear,
|
|
world.Description.SampleCount > 1 ? GpuStoreOp.Resolve : GpuStoreOp.Store,
|
|
Vector4.Zero),
|
|
Depth = new GpuDepthAttachment(
|
|
GpuLoadOp.Clear,
|
|
GpuStoreOp.Store,
|
|
1f,
|
|
0),
|
|
SampleCount = world.Description.SampleCount,
|
|
});
|
|
}
|
|
|
|
private static void PublishCurrentShadow(
|
|
AtmosphericPostProcessGraph graph,
|
|
IGpuFrame frame) => PublishCurrentShadow(
|
|
graph.DirectionalShadowReceivers,
|
|
frame);
|
|
|
|
private static void PublishCurrentShadow(
|
|
IDirectionalShadowReceiverSource receiverSource,
|
|
IGpuFrame frame)
|
|
{
|
|
var renderer = Assert.IsType<DirectionalSunShadowRenderer>(
|
|
receiverSource);
|
|
GpuRingAllocation transformAllocation = frame.AllocateRing(
|
|
checked((int)WorldTransformCapacityPolicy.InitialBindingSizeBytes),
|
|
GpuRingUsage.Storage);
|
|
var sharedTransforms = new WorldTransformFrameSlice(
|
|
frame.Serial,
|
|
transformAllocation.Buffer,
|
|
transformAllocation.OffsetBytes,
|
|
WorldTransformCapacityPolicy.InitialBindingSizeBytes,
|
|
FirstInstance: 0,
|
|
InstanceCount: 0);
|
|
DirectionalSunShadowDiagnostics diagnostics = renderer.RenderPrepared(
|
|
frame,
|
|
new DirectionalShadowEnvironmentState(
|
|
DirectionalShadowGateReason.Enabled,
|
|
Vector3.Normalize(new Vector3(0.2f, 0.3f, 1f)),
|
|
LightElevationSin: 0.94f,
|
|
Strength: 0.8f,
|
|
SoftnessMultiplier: 1.25f,
|
|
SourceKind: AuthoredCelestialShadowSourceKind.Sun),
|
|
Matrix4x4.Identity,
|
|
Matrix4x4.CreatePerspectiveFieldOfView(
|
|
MathF.PI / 3f,
|
|
16f / 9f,
|
|
0.1f,
|
|
500f),
|
|
cameraNearMeters: 0.1f,
|
|
casterDepthPaddingMeters: 48f,
|
|
worldDraws: new DirectionalShadowPreparedDraws(),
|
|
terrainDraws: new DirectionalShadowTerrainPreparedDraws(),
|
|
worldGeometry: null,
|
|
terrainGeometry: null,
|
|
sharedTransforms);
|
|
Assert.True(diagnostics.CascadeCount > 0);
|
|
}
|
|
|
|
private static void SetLastShadowDiagnostics(
|
|
AtmosphericPostProcessGraph graph,
|
|
DirectionalSunShadowDiagnostics diagnostics)
|
|
{
|
|
System.Reflection.FieldInfo field = typeof(AtmosphericPostProcessGraph)
|
|
.GetField(
|
|
"_lastShadowDiagnostics",
|
|
System.Reflection.BindingFlags.Instance
|
|
| System.Reflection.BindingFlags.NonPublic)
|
|
?? throw new InvalidOperationException(
|
|
"Atmospheric graph no longer owns its directional-shadow diagnostics.");
|
|
field.SetValue(graph, diagnostics);
|
|
}
|
|
|
|
private static AtmosphericFrameInputs Inputs(
|
|
int width,
|
|
int height,
|
|
float elevation = 4f,
|
|
int activeDayGroup = 0) => new(
|
|
new Vector2(0.5f, 0.35f),
|
|
SunIsOnScreen: true,
|
|
elevation,
|
|
new Vector3(1f, 0.85f, 0.65f),
|
|
Vector3.Normalize(new Vector3(0.2f, 0.5f, 0.8f)),
|
|
SunDirectionalBrightness: 1f,
|
|
Matrix4x4.Identity,
|
|
activeDayGroup,
|
|
WeatherKind.Clear,
|
|
WeatherIntensity: 0f,
|
|
DeltaSeconds: 1d / 60d,
|
|
width,
|
|
height,
|
|
IsOutdoor: true);
|
|
|
|
private static AtmosphericPackPassUniforms ReadPass(
|
|
RecordingGpuDevice device,
|
|
GpuRecordedUniformBind binding) =>
|
|
MemoryMarshal.Read<AtmosphericPackPassUniforms>(device.RingBytes.Slice(
|
|
(int)binding.OffsetBytes,
|
|
AtmosphericPackPassUniforms.SizeInBytes));
|
|
|
|
private static IRenderPackAssets BuiltInAssets() =>
|
|
BuiltInAtmosphericRenderPack.CreateAssets(Path.Combine(
|
|
RepositoryRoot(),
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"Shaders",
|
|
"spv"));
|
|
|
|
private static RenderPackDescriptor RenamedExternalTierTwoDescriptor()
|
|
{
|
|
RenderPackDescriptor source = BuiltInAtmosphericRenderPack.Descriptor;
|
|
Dictionary<string, string> resources = source.Resources
|
|
.Select((value, index) => (value.Id, Renamed: $"external-resource-{index}"))
|
|
.ToDictionary(static value => value.Id, static value => value.Renamed,
|
|
StringComparer.OrdinalIgnoreCase);
|
|
Dictionary<string, string> settings = source.Settings
|
|
.Select((value, index) => (value.Id, Renamed: $"external-setting-{index}"))
|
|
.ToDictionary(static value => value.Id, static value => value.Renamed,
|
|
StringComparer.OrdinalIgnoreCase);
|
|
string Shader(string asset) => $"external/{asset}";
|
|
|
|
return source with
|
|
{
|
|
Id = "example.external-atmosphere",
|
|
Resources = source.Resources.Select(value => value with
|
|
{
|
|
Id = resources[value.Id],
|
|
}).ToArray(),
|
|
Passes = source.Passes.Select((value, index) => value with
|
|
{
|
|
Id = $"external-pass-{index}",
|
|
VertexShaderAsset = Shader(value.VertexShaderAsset),
|
|
FragmentShaderAsset = Shader(value.FragmentShaderAsset),
|
|
ResourceReads = value.ResourceReads.Select(id => resources[id]).ToArray(),
|
|
ResourceWrites = value.ResourceWrites.Select(id => resources[id]).ToArray(),
|
|
}).ToArray(),
|
|
SceneReplays = source.SceneReplays.Select((value, index) => value with
|
|
{
|
|
Id = $"external-replay-{index}",
|
|
}).ToArray(),
|
|
PipelineVariants = source.PipelineVariants.Select((value, index) => value with
|
|
{
|
|
Id = $"external-variant-{index}",
|
|
VertexShaderAsset = Shader(value.VertexShaderAsset),
|
|
FragmentShaderAsset = Shader(value.FragmentShaderAsset),
|
|
}).ToArray(),
|
|
QualityPresets = source.QualityPresets.Select((value, index) => value with
|
|
{
|
|
Id = $"external-quality-{index}",
|
|
ResourceOverrides = value.ResourceOverrides.Select(resource => resource with
|
|
{
|
|
ResourceId = resources[resource.ResourceId],
|
|
}).ToArray(),
|
|
SettingOverrides = value.SettingOverrides.Select(setting => setting with
|
|
{
|
|
SettingId = settings[setting.SettingId],
|
|
}).ToArray(),
|
|
}).ToArray(),
|
|
Settings = source.Settings.Select((value, index) => value with
|
|
{
|
|
Id = settings[value.Id],
|
|
}).ToArray(),
|
|
};
|
|
}
|
|
|
|
private static string RepositoryRoot()
|
|
{
|
|
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (directory is not null
|
|
&& !File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
|
directory = directory.Parent;
|
|
return directory?.FullName
|
|
?? throw new InvalidOperationException("Could not locate repository root.");
|
|
}
|
|
|
|
private sealed class RejectingAssets : IRenderPackAssets
|
|
{
|
|
public Stream OpenRead(string assetKey) =>
|
|
throw new InvalidOperationException("A no-op pack must not open shader assets.");
|
|
}
|
|
|
|
private sealed class RenamedShaderAssets(IRenderPackAssets inner) : IRenderPackAssets
|
|
{
|
|
public Stream OpenRead(string assetKey)
|
|
{
|
|
const string prefix = "external/";
|
|
if (!assetKey.StartsWith(prefix, StringComparison.Ordinal))
|
|
throw new InvalidOperationException($"Unexpected external asset '{assetKey}'.");
|
|
return inner.OpenRead(assetKey[prefix.Length..]);
|
|
}
|
|
}
|
|
}
|