acdream/tests/AcDream.App.Tests/Rendering/Packs/VolumetricShaftRendererTests.cs

369 lines
14 KiB
C#

using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Packs;
using AcDream.App.Tests.Rendering.Gpu;
using AcDream.Core.World;
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.App.Tests.Rendering.Packs;
public sealed class VolumetricShaftRendererTests
{
[Fact]
public void MediumConsumesCurrentB5B6B8AndWritesQuarterResolutionHdr()
{
var device = new RecordingGpuDevice();
using var renderer = Renderer(device, "medium");
GpuTextureSlot depth = TextureSlot(device, "scene-depth");
using IGpuFrame frame = device.BeginFrame();
DirectionalShadowFrameBinding shadow = Shadow(frame, device.DefaultTextureSlot);
AtmosphericFrameInputs inputs = Inputs(800, 600);
device.Clear();
VolumetricShaftOutput output = renderer.Render(frame, in inputs, in shadow, depth);
frame.End();
Assert.True(output.HasTexture);
Assert.Equal(VolumetricShaftGateReason.Rendered, output.Diagnostics.GateReason);
Assert.Equal(200, output.Diagnostics.Width);
Assert.Equal(150, output.Diagnostics.Height);
Assert.Equal(40, output.Diagnostics.RayMarchSteps);
Assert.Equal(200L * 150L * 8L, output.Diagnostics.RetainedGpuBytes);
Assert.Equal(
[
GpuBindingModel.UniformAtmosphericFrame,
GpuBindingModel.UniformDirectionalShadow,
GpuBindingModel.UniformPackPass,
GpuBindingModel.UniformPackSettings,
],
device.OfKind<GpuRecordedUniformBind>().Select(call => call.Binding));
Assert.Equal(depth.Index,
Assert.Single(device.OfKind<GpuRecordedPushConstants>()).Constants.TextureIndexA);
Assert.Equal(1, renderer.Performance.CpuSampleCount);
Assert.Equal(0, renderer.Performance.GpuSampleCount);
}
[Fact]
public void LowDefaultsOffWithoutAllocatingTargetOrRecordingPass()
{
var device = new RecordingGpuDevice();
using var renderer = Renderer(device, "low");
using IGpuFrame frame = device.BeginFrame();
DirectionalShadowFrameBinding shadow = Shadow(frame, device.DefaultTextureSlot);
AtmosphericFrameInputs inputs = Inputs(800, 600);
int targets = device.CreatedRenderTargets.Count;
VolumetricShaftOutput output = renderer.Render(
frame,
in inputs,
in shadow,
device.DefaultTextureSlot);
frame.End();
Assert.False(output.HasTexture);
Assert.Equal(VolumetricShaftGateReason.DisabledByPreset, output.Diagnostics.GateReason);
Assert.Equal(targets, device.CreatedRenderTargets.Count);
Assert.Empty(device.OfKind<GpuRecordedPassBegin>());
Assert.Equal(default, renderer.Performance);
}
[Fact]
public void LowUserOverrideEnablesQuarterResolutionTwentyFourStepShafts()
{
var device = new RecordingGpuDevice();
RenderPackDescriptor descriptor = BuiltInAtmosphericRenderPack.Descriptor;
RenderQualityPreset low = Assert.Single(
descriptor.QualityPresets,
value => string.Equals(value.Id, "low", StringComparison.Ordinal));
using var renderer = new VolumetricShaftRenderer(
device,
descriptor,
Assets(),
low,
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["volumetric-strength"] = "0.25",
});
GpuTextureSlot depth = TextureSlot(device, "depth");
RenderOne(renderer, device, Inputs(800, 600), depth);
Assert.Equal(VolumetricShaftGateReason.Rendered, renderer.LastDiagnostics.GateReason);
Assert.Equal(200, renderer.LastDiagnostics.Width);
Assert.Equal(150, renderer.LastDiagnostics.Height);
Assert.Equal(24, renderer.LastDiagnostics.RayMarchSteps);
Assert.True(renderer.LastDiagnostics.Strength > 0f);
}
[Fact]
public void StaleShadowBindingAndIndoorFrameFailClosedWithoutSamplingOldOutput()
{
var device = new RecordingGpuDevice();
using var renderer = Renderer(device, "high");
AtmosphericFrameInputs inputs = Inputs(1280, 720);
using IGpuFrame frame = device.BeginFrame();
var stale = new DirectionalShadowFrameBinding(
frame.Serial - 1,
true,
device.RingBuffer,
0,
DirectionalShadowUniforms.SizeInBytes,
device.DefaultTextureSlot,
4);
VolumetricShaftOutput staleOutput = renderer.Render(
frame,
in inputs,
in stale,
device.DefaultTextureSlot);
DirectionalShadowFrameBinding current = Shadow(frame, device.DefaultTextureSlot);
AtmosphericFrameInputs indoor = inputs with { IsOutdoor = false };
VolumetricShaftOutput indoorOutput = renderer.Render(
frame,
in indoor,
in current,
device.DefaultTextureSlot);
frame.End();
Assert.Equal(VolumetricShaftGateReason.NoCurrentDirectionalShadow,
staleOutput.Diagnostics.GateReason);
Assert.Equal(VolumetricShaftGateReason.Indoor, indoorOutput.Diagnostics.GateReason);
Assert.False(staleOutput.HasTexture);
Assert.False(indoorOutput.HasTexture);
Assert.Empty(device.CreatedRenderTargets);
}
[Fact]
public void ResizeAtomicallyReplacesTargetAndResetsMixedResolutionPerformanceWindow()
{
var device = new RecordingGpuDevice();
using var renderer = Renderer(device, "high");
GpuTextureSlot depth = TextureSlot(device, "depth");
RenderOne(renderer, device, Inputs(800, 600), depth);
RecordingGpuRenderTarget first = Assert.Single(device.CreatedRenderTargets);
Assert.Equal(400, first.Description.Width);
Assert.Equal(1, renderer.Performance.CpuSampleCount);
RenderOne(renderer, device, Inputs(1200, 800), depth);
Assert.True(first.IsDisposed);
Assert.Equal(600, device.CreatedRenderTargets[^1].Description.Width);
Assert.Equal(400, device.CreatedRenderTargets[^1].Description.Height);
Assert.Equal(1, renderer.Performance.CpuSampleCount);
}
[Fact]
public void TargetFailureRollsBackAndRetryPublishesOneOwnedTexture()
{
var device = new RecordingGpuDevice();
using var renderer = Renderer(device, "medium");
GpuTextureSlot depth = TextureSlot(device, "depth");
int baselineSlots = device.LiveTextureSlotCount;
device.RenderTargetFailure = _ => new InvalidOperationException("volumetric allocation failed");
Assert.Throws<InvalidOperationException>(() => RenderOne(
renderer,
device,
Inputs(800, 600),
depth));
Assert.Equal(baselineSlots, device.LiveTextureSlotCount);
Assert.Empty(device.CreatedRenderTargets);
device.RenderTargetFailure = null;
RenderOne(renderer, device, Inputs(800, 600), depth);
Assert.Equal(baselineSlots + 1, device.LiveTextureSlotCount);
Assert.Single(device.CreatedRenderTargets);
}
[Fact]
public void DisposeReleasesOutputSlotTargetAndPipeline()
{
var device = new RecordingGpuDevice();
GpuTextureSlot depth = TextureSlot(device, "depth");
int baselineSlots = device.LiveTextureSlotCount;
var renderer = Renderer(device, "medium");
RenderOne(renderer, device, Inputs(800, 600), depth);
RecordingGpuRenderTarget target = Assert.Single(device.CreatedRenderTargets);
RecordingGpuPipeline pipeline = Assert.Single(device.CreatedPipelines);
renderer.Dispose();
Assert.Equal(baselineSlots, device.LiveTextureSlotCount);
Assert.True(target.IsDisposed);
Assert.True(pipeline.IsDisposed);
}
[Fact]
public void AuthoredWeatherAndSunElevationContinuouslyScaleTheSameFramePolicy()
{
var device = new RecordingGpuDevice();
using var renderer = Renderer(device, "medium");
GpuTextureSlot depth = TextureSlot(device, "depth");
RenderOne(renderer, device, Inputs(800, 600), depth);
float clearLowSun = renderer.LastDiagnostics.Strength;
AtmosphericFrameInputs overcast = Inputs(800, 600) with
{
Weather = WeatherKind.Overcast,
WeatherIntensity = 1f,
};
RenderOne(renderer, device, overcast, depth);
float overcastLowSun = renderer.LastDiagnostics.Strength;
AtmosphericFrameInputs noon = Inputs(800, 600) with
{
SunElevationDegrees = 70f,
};
RenderOne(renderer, device, noon, depth);
Assert.True(clearLowSun > overcastLowSun);
Assert.True(clearLowSun > renderer.LastDiagnostics.Strength);
Assert.True(overcastLowSun > 0f);
}
[Fact]
public void DeclaredActiveDayGroupMultiplierScalesAuthoredPolicy()
{
var device = new RecordingGpuDevice();
using var renderer = Renderer(device, "medium");
GpuTextureSlot depth = TextureSlot(device, "depth");
RenderOne(renderer, device, Inputs(800, 600) with { ActiveDayGroup = 0 }, depth);
float groupZero = renderer.LastDiagnostics.Strength;
RenderOne(renderer, device, Inputs(800, 600) with { ActiveDayGroup = 1 }, depth);
float groupOne = renderer.LastDiagnostics.Strength;
Assert.Equal(groupZero * 0.35f, groupOne, 5);
}
[Fact]
public void DeclaredVolumetricElevationCurveControlsShaftStrength()
{
var device = new RecordingGpuDevice();
RenderPackDescriptor source = BuiltInAtmosphericRenderPack.Descriptor;
RenderPackDescriptor changed = source with
{
AtmospherePolicy = source.AtmospherePolicy! with
{
VolumetricShaftSunElevationResponse =
[
new SunElevationResponsePoint(-90, 0.25),
new SunElevationResponsePoint(90, 0.25),
],
},
};
RenderQualityPreset medium = Assert.Single(changed.QualityPresets, value =>
value.Semantic == RenderQualitySemantic.Medium);
using var renderer = new VolumetricShaftRenderer(
device,
changed,
Assets(),
medium);
GpuTextureSlot depth = TextureSlot(device, "depth");
RenderOne(renderer, device, Inputs(800, 600), depth);
Assert.Equal(0.25f * 0.35f, renderer.LastDiagnostics.Strength, 5);
}
[Fact]
public void ShaderUsesVulkanYFlipWorldMetreBiasAndShadowStrengthMix()
{
string source = File.ReadAllText(Path.Combine(
RepositoryRoot(),
"src", "AcDream.App", "Rendering", "Shaders", "atmospheric_volumetric.frag"));
Assert.Contains("0.5 - ndc.y * 0.5", source, StringComparison.Ordinal);
Assert.Contains("surfaceToSun * max(uShadowBiasMeters.x, 0.0)", source,
StringComparison.Ordinal);
Assert.Contains("mix(1.0, visible, clamp(uShadowControl.x, 0.0, 1.0))", source,
StringComparison.Ordinal);
Assert.DoesNotContain("ndc.z -", source, StringComparison.Ordinal);
}
private static void RenderOne(
VolumetricShaftRenderer renderer,
RecordingGpuDevice device,
AtmosphericFrameInputs inputs,
GpuTextureSlot depth)
{
using IGpuFrame frame = device.BeginFrame();
DirectionalShadowFrameBinding shadow = Shadow(frame, device.DefaultTextureSlot);
renderer.Render(frame, in inputs, in shadow, depth);
frame.End();
}
private static DirectionalShadowFrameBinding Shadow(
IGpuFrame frame,
GpuTextureSlot shadowTexture)
{
GpuRingAllocation allocation = frame.AllocateRing(
DirectionalShadowUniforms.SizeInBytes,
GpuRingUsage.Uniform);
allocation.Data.Clear();
return new DirectionalShadowFrameBinding(
frame.Serial,
true,
allocation.Buffer,
allocation.OffsetBytes,
DirectionalShadowUniforms.SizeInBytes,
shadowTexture,
3);
}
private static GpuTextureSlot TextureSlot(RecordingGpuDevice device, string name)
{
IGpuTexture texture = device.CreateTexture(new GpuTextureDescription(
name,
GpuTextureKind.Texture2D,
GpuTextureFormat.Rgba8Unorm,
1,
1,
1,
1));
return device.RegisterTexture(texture, device.CreateSampler(GpuSamplerDescription.WorldClamp));
}
private static VolumetricShaftRenderer Renderer(
RecordingGpuDevice device,
string presetId)
{
RenderPackDescriptor descriptor = BuiltInAtmosphericRenderPack.Descriptor;
RenderQualityPreset preset = Assert.Single(
descriptor.QualityPresets,
value => string.Equals(value.Id, presetId, StringComparison.Ordinal));
return new VolumetricShaftRenderer(device, descriptor, Assets(), preset);
}
private static AtmosphericFrameInputs Inputs(int width, int height) => new(
new Vector2(0.5f, 0.4f),
true,
12f,
new Vector3(1f, 0.85f, 0.7f),
Vector3.Normalize(new Vector3(0.2f, 0.5f, 0.8f)),
1f,
Matrix4x4.Identity,
0,
WeatherKind.Clear,
0f,
1d / 60d,
width,
height,
true);
private static IRenderPackAssets Assets() =>
BuiltInAtmosphericRenderPack.CreateAssets(Path.Combine(
RepositoryRoot(),
"src", "AcDream.App", "Rendering", "Shaders", "spv"));
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.");
}
}