Closes review finding F4 (docs/research/2026-08-22-campaign-ar-review.md): retail's main-world colour, sun rays and volumetric shafts are all gamma-encoded display-space values (the 2013 client has no linear lighting pipeline), but bloom thresholding, ACES (Narkowicz fit), Rec.709 luma saturation, the contrast pivot and the vignette were all operating directly on those gamma values, then writing the result to the UNORM swapchain without re-encoding. - atmospheric_common.glsl gains acdreamDecodeDisplay/acdreamEncodeDisplay (pow(c, 2.2) / pow(c, 1/2.2)). 2.2 is the retail-era CRT/early-LCD display-gamma assumption, deliberately not the sRGB piecewise curve, which would claim a precision retail's authoring pipeline never had. uAtmosphereSunColor's comment is corrected from "authored linear rgb" to "authored display-space rgb (retail has no linear pipeline)". - atmospheric_bloom_downsample.frag, atmospheric_filmic.frag (both the fused-Low and non-fused paths) decode every world/ray/volumetric read before summing/thresholding; atmospheric_bloom_blur.frag is unchanged (it already reads the now-linear bloom buffer); atmospheric_sun_rays.frag and atmospheric_volumetric.frag are documented as writing display-space colour that the consumers decode. - The contrast pivot moves from 0.5 (a gamma-space midpoint) to 0.18 (linear mid-grey, the standard 18%-grey-card exposure convention). The final filmic output is clamped in linear, then re-encoded before the UNORM write. - Bloom threshold/knee are re-derived for linear light: the pre-VM3 gamma-space pair was threshold 1.0 / knee 0.45, i.e. a soft range of [0.55, 1.0] in gamma. Decoding both ends with the same 2.2 assumption gives decode(1.0) = 1.0 (threshold unchanged) and decode(0.55) = 0.55^2.2 ~= 0.27, so linear knee = 1.0 - 0.27 ~= 0.73. Replaced the inline 0.45f literals with named constants BloomThresholdLinear = 1f / BloomKneeLinear = 0.73f on AtmosphericPostProcessGraph. bloom-strength's 0.65 default is untouched. - Exposure stays at its accepted 0.80 default: in linear, encode(acesFitted(0.80 * decode(0.46))) ~= 0.50, reproducing the same accepted midtone the old gamma-space pipeline produced as 0.51 for the same 0.46 input (0.46 * 0.80 fed straight into acesFitted, no decode/encode). Highlights now retain more (gamma 0.9 input moves from ~0.74 to ~0.85 through the full pipeline) and blacks deepen slightly (gamma 0.1 moves from ~0.09 to ~0.05) — the owner's visual gate judges. - Added AtmosphericColorPipeline, a CPU mirror of the GLSL decode/encode/ ACES/grade/filmic math (line-for-line, with a header comment requiring it stay mirrored), and AtmosphericColorPipelineTests: neutral-preset identity within half an 8-bit step for a 0..255 grey sweep (proving the neutral preset is numerically the pack-off image), decode/encode round-trip within 1e-6, monotonic-in-exposure, the pinned midtone/ highlight/shadow numbers above, and the bloom-knee derivation. - Added a shader-source pinning test so a future edit cannot silently drop the colour-space conversions: atmospheric_filmic.frag must contain exactly one acdreamEncodeDisplay( call in main()'s output, atmospheric_bloom_downsample.frag must contain at least three acdreamDecodeDisplay( calls. - Regenerated SPIR-V (tools/compile-shaders.ps1, glslc from the installed Vulkan SDK). Only atmospheric_bloom_downsample.frag.spv and atmospheric_filmic.frag.spv changed in bytes; every other pack shader that includes atmospheric_common.glsl recompiled to a byte-identical binary (the new decode/encode helpers are unreferenced dead code for them). VulkanShaderManifestTests' retail-oracle SHA-256 set (mesh_modern, terrain_modern, mesh_detail, etc.) is untouched and still passes — the retail default path did not change. - Docs: noted the linear-light move in the AR plan's Slice 1 section, and added a "Colour space" section to the render-pack ABI doc (docs/render-packs/semantic-bindings-v1.md) naming which inputs are display-space and pointing at atmospheric_common.glsl as the reference implementation. No ABI version bump — the binding layout is unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1573 lines
66 KiB
C#
1573 lines
66 KiB
C#
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 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);
|
|
|
|
int decodesInDownsample = CountOccurrences(downsample, "acdreamDecodeDisplay(");
|
|
Assert.True(
|
|
decodesInDownsample >= 3,
|
|
$"expected at least 3 acdreamDecodeDisplay( calls in atmospheric_bloom_downsample.frag, found {decodesInDownsample}");
|
|
}
|
|
|
|
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..]);
|
|
}
|
|
}
|
|
}
|