using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace AcDream.App.Tests.Rendering.Gpu.Vk;
///
/// Campaign V slice V6c, plan §4.6: the committed SPIR-V must match the GLSL it
/// was compiled from.
///
/// Plan §4.6 rules out runtime shader compilation — CI has no Vulkan SDK,
/// and a native shaderc dependency plus a startup cost would be paid for shaders
/// that never change at runtime — so the .spv artifacts are committed.
/// The obvious hazard follows immediately: someone edits a shader, the GL
/// backend picks it up because it compiles GLSL at startup, and the Vulkan
/// backend silently keeps rendering the old one. This test is what turns that
/// into a red build.
///
/// It also pins which production shaders are Vulkan-expressible TODAY. As
/// of Campaign V slice V11 (which deleted the one pair that never was —
/// mesh.vert/mesh.frag, the pre-modern-pipeline shader the N.5 ship
/// amendment's mandatory modern path made unreachable) every remaining pair
/// compiles. A future non-ready pair's failure is a specific source-level fact
/// belonging to a renderer-port slice that has not landed — not a toolchain gap.
/// Recording them here means the next slice inherits an inventory rather than a
/// rediscovery.
///
public sealed class VulkanShaderManifestTests
{
private sealed record StageEntry(string Stage, string SourceSha256, bool Compiled, string? Message);
private sealed record ShaderEntry(string Name, bool VulkanReady, IReadOnlyList Stages);
private sealed record Manifest(string Note, IReadOnlyList Shaders);
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
};
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 the repository root from the test binary.");
}
private static string ShadersDirectory() =>
Path.Combine(RepositoryRoot(), "src", "AcDream.App", "Rendering", "Shaders");
private static string SpirvDirectory() => Path.Combine(ShadersDirectory(), "spv");
private static Manifest ReadManifest()
{
string path = Path.Combine(SpirvDirectory(), "shaders.manifest.json");
Assert.True(File.Exists(path), $"The shader manifest is missing at {path}. Run tools/compile-shaders.ps1.");
return JsonSerializer.Deserialize(File.ReadAllText(path), JsonOptions)
?? throw new InvalidOperationException("The shader manifest could not be parsed.");
}
private static string Sha256OfSource(string path)
{
// Line endings are normalised before hashing so a checkout with a
// different core.autocrlf setting does not report every shader stale.
string text = File.ReadAllText(path).Replace("\r\n", "\n");
return Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(text)));
}
[Fact]
public void EveryGlslPairIsRecordedInTheManifest()
{
Manifest manifest = ReadManifest();
string[] pairs = Directory
.EnumerateFiles(ShadersDirectory(), "*.vert")
.Select(Path.GetFileNameWithoutExtension)
.Where(name => name is not null && File.Exists(Path.Combine(ShadersDirectory(), $"{name}.frag")))
.Select(name => name!)
.OrderBy(name => name, StringComparer.Ordinal)
.ToArray();
Assert.Equal(pairs, manifest.Shaders.Select(shader => shader.Name).OrderBy(n => n, StringComparer.Ordinal));
}
[Fact]
public void CommittedSpirvIsNotStaleAgainstItsGlslSource()
{
Manifest manifest = ReadManifest();
var stale = new List();
foreach (ShaderEntry shader in manifest.Shaders)
{
foreach (StageEntry stage in shader.Stages)
{
string source = Path.Combine(ShadersDirectory(), $"{shader.Name}.{stage.Stage}");
if (!File.Exists(source))
{
stale.Add($"{shader.Name}.{stage.Stage}: the GLSL source no longer exists");
continue;
}
string actual = Sha256OfSource(source);
if (!string.Equals(actual, stage.SourceSha256, StringComparison.Ordinal))
stale.Add($"{shader.Name}.{stage.Stage}: source changed since the .spv was built");
}
}
Assert.True(
stale.Count == 0,
"Committed SPIR-V is out of date. Run tools/compile-shaders.ps1 and commit the result.\n "
+ string.Join("\n ", stale));
}
[Fact]
public void EveryShaderTheManifestCallsReadyHasBothSpirvArtifacts()
{
Manifest manifest = ReadManifest();
foreach (ShaderEntry shader in manifest.Shaders.Where(entry => entry.VulkanReady))
{
foreach (string stage in (string[])["vert", "frag"])
{
string path = Path.Combine(SpirvDirectory(), $"{shader.Name}.{stage}.spv");
Assert.True(File.Exists(path), $"{shader.Name} is marked Vulkan-ready but {path} is missing.");
long length = new FileInfo(path).Length;
Assert.True(length > 0 && length % 4 == 0, $"{path} is not a whole number of SPIR-V words.");
}
}
}
[Fact]
public void ShadersTheManifestCallsUnreadyHaveNoStaleSpirvLeftBehind()
{
Manifest manifest = ReadManifest();
foreach (ShaderEntry shader in manifest.Shaders.Where(entry => !entry.VulkanReady))
{
foreach (string stage in (string[])["vert", "frag"])
{
string path = Path.Combine(SpirvDirectory(), $"{shader.Name}.{stage}.spv");
// A leftover .spv from an earlier attempt would be loaded
// happily by the device and would be a shader nobody can account
// for.
Assert.False(File.Exists(path), $"{shader.Name} is not Vulkan-ready but {path} exists.");
}
}
}
[Fact]
public void EveryUnreadyShaderRecordsWhyItCannotBeCompiledYet()
{
Manifest manifest = ReadManifest();
foreach (ShaderEntry shader in manifest.Shaders.Where(entry => !entry.VulkanReady))
{
Assert.Contains(shader.Stages, stage => !stage.Compiled && !string.IsNullOrWhiteSpace(stage.Message));
}
}
[Fact]
public void TheRhiVerificationShaderIsCompiled()
{
Manifest manifest = ReadManifest();
ShaderEntry probe = Assert.Single(
manifest.Shaders,
shader => string.Equals(shader.Name, "vk_probe", StringComparison.Ordinal));
// Plan §4.11 wants the capability probe to build a real pipeline from
// committed .spv; slice V5 deferred that to V6c because no toolchain
// existed. If this pair ever stops compiling, the Vulkan backend has no
// pipeline it can build at all.
Assert.True(probe.VulkanReady, "vk_probe must compile — the whole Vulkan backend draws with it.");
}
}