123 lines
5.3 KiB
C#
123 lines
5.3 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
/// <summary>
|
|
/// Campaign V slice V6e: the sky's dozen loose uniforms became one std140
|
|
/// uniform block, and this is the test that keeps the CPU struct and the GLSL
|
|
/// block describing the same 256 bytes.
|
|
///
|
|
/// <para>It exists because the failure mode is silent. std140 gives a
|
|
/// <c>vec3</c> 16-byte alignment while only using 12, so the block deliberately
|
|
/// parks a <c>float</c> in each of those pad words — which is why the lighting
|
|
/// colours and the per-surface scalars interleave rather than being grouped by
|
|
/// meaning. Get one member out of order and the shader reads the sun direction
|
|
/// where a colour should be: no compile error, no link error, no GL error, just
|
|
/// a wrong sky that only a human looking at the screen would catch. The offline
|
|
/// pixel gate masks the sky band for determinism, so nothing automated is
|
|
/// watching. This is the substitute.</para>
|
|
/// </summary>
|
|
public sealed class SkyParamsLayoutTests
|
|
{
|
|
private static Type SkyParamsType =>
|
|
typeof(AcDream.App.Rendering.Sky.SkyRenderer)
|
|
.GetNestedType("SkyParams", BindingFlags.NonPublic)
|
|
?? throw new InvalidOperationException("SkyRenderer.SkyParams is missing.");
|
|
|
|
[Theory]
|
|
// Three transforms first: mat4 is 4 vec4s in std140, so these need no thought.
|
|
[InlineData("Model", 0)]
|
|
[InlineData("SkyView", 64)]
|
|
[InlineData("SkyProjection", 128)]
|
|
// Then three (vec3, float) couples. Each float rides in the pad word the
|
|
// vec3's 16-byte alignment would otherwise waste.
|
|
[InlineData("AmbientColor", 192)]
|
|
[InlineData("Emissive", 204)]
|
|
[InlineData("SunColor", 208)]
|
|
[InlineData("DiffuseFactor", 220)]
|
|
[InlineData("SunDir", 224)]
|
|
[InlineData("Transparency", 236)]
|
|
// Finally a vec2 (8-byte aligned) and the last two scalars, filling the
|
|
// sixteenth vec4 exactly.
|
|
[InlineData("UvScroll", 240)]
|
|
[InlineData("ApplyFog", 248)]
|
|
[InlineData("SurfOpacity", 252)]
|
|
public void EveryMemberSitsWhereStd140PutsIt(string member, int expectedOffset)
|
|
{
|
|
Assert.Equal(
|
|
new IntPtr(expectedOffset),
|
|
Marshal.OffsetOf(SkyParamsType, member));
|
|
}
|
|
|
|
[Fact]
|
|
public void TheBlockIsAWholeNumberOfVec4s()
|
|
{
|
|
int declared = (int)(SkyParamsType
|
|
.GetField("SizeInBytes", BindingFlags.Public | BindingFlags.Static)
|
|
?.GetRawConstantValue()
|
|
?? throw new InvalidOperationException("SkyParams.SizeInBytes is missing."));
|
|
|
|
Assert.Equal(256, declared);
|
|
Assert.Equal(declared, Marshal.SizeOf(SkyParamsType));
|
|
// std140 rounds a block up to its largest member's alignment (16).
|
|
Assert.Equal(0, declared % 16);
|
|
}
|
|
|
|
[Fact]
|
|
public void BothStagesDeclareTheBlockIdentically()
|
|
{
|
|
// A uniform block named in two stages of one program must be declared
|
|
// the same way in both, or the link fails — but only if someone reads
|
|
// it, and sky.frag reads three of the twelve members. Comparing the
|
|
// declarations directly means a divergence is caught by a test rather
|
|
// than by a driver's link log at startup.
|
|
string shaders = Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders");
|
|
string vertexBlock = ExtractSkyParamsBlock(File.ReadAllText(Path.Combine(shaders, "sky.vert")));
|
|
string fragmentBlock = ExtractSkyParamsBlock(File.ReadAllText(Path.Combine(shaders, "sky.frag")));
|
|
|
|
Assert.Equal(vertexBlock, fragmentBlock);
|
|
|
|
// And the binding must be the one the CPU binds the buffer to.
|
|
Assert.Equal(4u, AcDream.App.Rendering.Gpu.GpuBindingModel.UniformSkyParams);
|
|
Assert.Contains("binding = 4) uniform SkyParams {", vertexBlock);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The block's declaration with comments and whitespace runs collapsed, so
|
|
/// the comparison is about members rather than formatting.
|
|
/// </summary>
|
|
private static string ExtractSkyParamsBlock(string source)
|
|
{
|
|
int start = source.IndexOf("layout(std140", StringComparison.Ordinal);
|
|
while (start >= 0)
|
|
{
|
|
int end = source.IndexOf("};", start, StringComparison.Ordinal);
|
|
Assert.True(end > start, "A layout(std140 …) block was never closed.");
|
|
string block = source[start..(end + 2)];
|
|
if (block.Contains("uniform SkyParams", StringComparison.Ordinal))
|
|
return Normalise(block);
|
|
start = source.IndexOf("layout(std140", end, StringComparison.Ordinal);
|
|
}
|
|
|
|
throw new InvalidOperationException("No SkyParams block found in the shader source.");
|
|
}
|
|
|
|
private static string Normalise(string block)
|
|
{
|
|
var text = new System.Text.StringBuilder();
|
|
foreach (string rawLine in block.Replace("\r\n", "\n").Split('\n'))
|
|
{
|
|
int comment = rawLine.IndexOf("//", StringComparison.Ordinal);
|
|
string line = comment >= 0 ? rawLine[..comment] : rawLine;
|
|
string collapsed = string.Join(' ', line.Split(
|
|
(char[]?)null, StringSplitOptions.RemoveEmptyEntries));
|
|
if (collapsed.Length > 0)
|
|
text.Append(collapsed).Append('\n');
|
|
}
|
|
|
|
return text.ToString();
|
|
}
|
|
}
|