feat(render): implement Campaign AR and terrain fidelity

This commit is contained in:
Erik 2026-08-22 13:13:29 +02:00
parent 99cf26e00c
commit 7a5f96ede5
368 changed files with 50611 additions and 950 deletions

View file

@ -0,0 +1,51 @@
using System.Text;
namespace AcDream.Tools.ShaderCompiler;
/// <summary>
/// Tiny deterministic include expander for checked-in shader ABI snippets.
/// Only quoted, shader-directory-relative includes are accepted; traversal and
/// cycles fail the compile rather than reaching shaderc with host-dependent
/// search paths.
/// </summary>
internal static class GlslIncludeExpander
{
internal static string Expand(string source, string sourceDirectory) =>
Expand(source, Path.GetFullPath(sourceDirectory), []);
private static string Expand(
string source,
string sourceDirectory,
HashSet<string> active)
{
var output = new StringBuilder();
foreach (string line in source.Replace("\r\n", "\n").Split('\n'))
{
string trimmed = line.Trim();
if (!trimmed.StartsWith("#include \"", StringComparison.Ordinal)
|| !trimmed.EndsWith('"'))
{
output.AppendLine(line);
continue;
}
string key = trimmed[10..^1];
if (key.Length == 0
|| Path.IsPathRooted(key)
|| key.Contains("..", StringComparison.Ordinal)
|| key.Contains('\\'))
throw new InvalidDataException($"Unsafe GLSL include '{key}'.");
string path = Path.GetFullPath(Path.Combine(sourceDirectory, key));
if (!path.StartsWith(sourceDirectory + Path.DirectorySeparatorChar, StringComparison.Ordinal)
|| !File.Exists(path))
throw new FileNotFoundException($"GLSL include '{key}' was not found.", path);
if (!active.Add(path))
throw new InvalidDataException($"Cyclic GLSL include '{key}'.");
output.AppendLine($"// ---- begin include: {key} ----");
output.Append(Expand(File.ReadAllText(path), sourceDirectory, active));
output.AppendLine($"// ---- end include: {key} ----");
active.Remove(path);
}
return output.ToString();
}
}

View file

@ -28,15 +28,21 @@ internal static class Program
{
private static unsafe int Main(string[] args)
{
if (args.Length < 2)
if (args.Length < 2
|| args.Length > 3
|| (args.Length == 3 && !string.Equals(args[2], "--force", StringComparison.Ordinal)))
{
Console.Error.WriteLine("usage: ShaderCompiler <shaders-dir> <output-dir>");
Console.Error.WriteLine("usage: ShaderCompiler <shaders-dir> <output-dir> [--force]");
return 2;
}
string sourceDirectory = Path.GetFullPath(args[0]);
string outputDirectory = Path.GetFullPath(args[1]);
bool force = args.Length == 3;
Directory.CreateDirectory(outputDirectory);
string manifestPath = Path.Combine(outputDirectory, "shaders.manifest.json");
IReadOnlyDictionary<(string Name, string Stage), ShaderStageResult> previousStages =
force ? new Dictionary<(string, string), ShaderStageResult>() : LoadPreviousStages(manifestPath);
string[] names = Directory
.EnumerateFiles(sourceDirectory, "*.vert")
@ -70,7 +76,23 @@ internal static class Program
{
string path = Path.Combine(sourceDirectory, $"{name}.{stage}");
string source = File.ReadAllText(path);
if (source.Contains("#include \"", StringComparison.Ordinal))
source = GlslIncludeExpander.Expand(source, sourceDirectory);
string hash = Sha256(source);
string target = Path.Combine(outputDirectory, $"{name}.{stage}.spv");
// An unchanged source already has the exact committed artifact
// described by the prior manifest. Keeping it avoids compiler-
// version-only SPIR-V decoration reordering on the retail path;
// a real source edit changes the hash and recompiles normally.
if (previousStages.TryGetValue((name, stage), out ShaderStageResult? previous)
&& previous.Compiled
&& string.Equals(previous.SourceSha256, hash, StringComparison.Ordinal)
&& File.Exists(target))
{
stages.Add(new ShaderStageResult(stage, hash, true, null));
continue;
}
string transformed;
try
@ -88,13 +110,11 @@ internal static class Program
if (TryCompile(shaderc, compiler, options, transformed, $"{name}.{stage}", stage, out byte[] spirv, out string message))
{
string target = Path.Combine(outputDirectory, $"{name}.{stage}.spv");
File.WriteAllBytes(target, spirv);
stages.Add(new ShaderStageResult(stage, hash, true, null));
}
else
{
string target = Path.Combine(outputDirectory, $"{name}.{stage}.spv");
if (File.Exists(target))
File.Delete(target);
stages.Add(new ShaderStageResult(stage, hash, false, Summarise(message)));
@ -140,7 +160,6 @@ internal static class Program
var manifest = new ShaderManifest(
"Campaign V slice V6c. Regenerate with tools/compile-shaders.ps1.",
entries.OrderBy(entry => entry.Name, StringComparer.Ordinal).ToList());
string manifestPath = Path.Combine(outputDirectory, "shaders.manifest.json");
File.WriteAllText(
manifestPath,
JsonSerializer.Serialize(manifest, ShaderManifestJson.Options) + Environment.NewLine);
@ -218,6 +237,30 @@ internal static class Program
byte[] bytes = Encoding.UTF8.GetBytes(text.Replace("\r\n", "\n"));
return Convert.ToHexStringLower(SHA256.HashData(bytes));
}
private static IReadOnlyDictionary<(string Name, string Stage), ShaderStageResult> LoadPreviousStages(
string manifestPath)
{
if (!File.Exists(manifestPath))
return new Dictionary<(string, string), ShaderStageResult>();
try
{
ShaderManifest? manifest = JsonSerializer.Deserialize<ShaderManifest>(
File.ReadAllText(manifestPath),
ShaderManifestJson.Options);
return manifest?.Shaders
.SelectMany(shader => shader.Stages.Select(stage => (shader.Name, Stage: stage)))
.ToDictionary(item => (item.Name, item.Stage.Stage), item => item.Stage)
?? new Dictionary<(string, string), ShaderStageResult>();
}
catch (JsonException)
{
// A malformed or obsolete manifest is not an authority. Recompile
// everything and replace it with the current deterministic schema.
return new Dictionary<(string, string), ShaderStageResult>();
}
}
}
internal sealed record ShaderStageResult(

View file

@ -18,7 +18,8 @@ namespace AcDream.Tools.ShaderCompiler;
/// <c>BatchBuffer</c> (SSBO binding 1) and <c>SceneLighting</c> (UBO binding 1)
/// can share a number. Vulkan has one namespace per set, so moving uniform
/// buffers to set 1 preserves both numbers. <c>common.glsl</c> has carried this
/// macro since V2 for exactly this moment.</item>
/// macro since V2 for exactly this moment. <c>ACDREAM_PACK_UBO_SET</c> separately
/// names opt-in set 3, which retail pipeline layouts do not contain.</item>
/// <item>The texture table becomes a real descriptor array at set 2 binding 0,
/// and <c>ACDREAM_TEXTURE_HANDLE</c> becomes an index rather than a packed
/// bindless handle. <c>nonuniformEXT</c> is required, not optional: within one
@ -59,6 +60,10 @@ internal static class VulkanGlslPreamble
"uTextureIndexB",
"uParamA",
"uParamB",
// Render-pack logical inputs C/D are packed into the existing spare
// scalar words; no bytes are appended to retail's push block.
"uTextureIndexC",
"uTextureIndexD",
];
/// <summary>
@ -66,7 +71,13 @@ internal static class VulkanGlslPreamble
/// <paramref name="stage"/> only affects which stage-specific rewrites are
/// emitted.
/// </summary>
internal static string Build(string stage)
internal static string Build(string stage) =>
Build(stage, includePackUniformSet: false, includePackTextureIndices: false);
private static string Build(
string stage,
bool includePackUniformSet,
bool includePackTextureIndices)
{
var text = new StringBuilder();
text.AppendLine("// ---- injected by tools/compile-shaders.ps1 (Campaign V slice V6c) ----");
@ -86,6 +97,11 @@ internal static class VulkanGlslPreamble
text.AppendLine("// §3.4 set 1: every uniform buffer. Under GL this macro expands to nothing.");
text.AppendLine("#undef ACDREAM_UBO_SET");
text.AppendLine("#define ACDREAM_UBO_SET set = 1,");
if (includePackUniformSet)
{
text.AppendLine("#undef ACDREAM_PACK_UBO_SET");
text.AppendLine("#define ACDREAM_PACK_UBO_SET set = 3,");
}
text.AppendLine();
text.AppendLine("// §4.4 set 2: the global sampled-texture table that replaces");
text.AppendLine("// GL_ARB_bindless_texture. Variable count, partially bound,");
@ -136,6 +152,11 @@ internal static class VulkanGlslPreamble
text.AppendLine("#define uTextureIndexB acdreamPush.textureIndexB");
text.AppendLine("#define uParamA acdreamPush.paramA");
text.AppendLine("#define uParamB acdreamPush.paramB");
if (includePackTextureIndices)
{
text.AppendLine("#define uTextureIndexC floatBitsToUint(acdreamPush.paramA)");
text.AppendLine("#define uTextureIndexD floatBitsToUint(acdreamPush.paramB)");
}
text.AppendLine();
text.AppendLine("// §4.6: gl_DrawIDARB stays as written — glslang exposes it for Vulkan");
text.AppendLine("// under the same ARB extension name. gl_InstanceIndex already includes");
@ -158,6 +179,10 @@ internal static class VulkanGlslPreamble
internal static string Apply(string source, string stage)
{
ArgumentNullException.ThrowIfNull(source);
bool includePackUniformSet = source.Contains("ACDREAM_PACK_UBO_SET", StringComparison.Ordinal);
bool includePackTextureIndices =
source.Contains("uTextureIndexC", StringComparison.Ordinal)
|| source.Contains("uTextureIndexD", StringComparison.Ordinal);
string[] lines = source.Replace("\r\n", "\n").Split('\n');
var output = new StringBuilder();
bool injected = false;
@ -171,7 +196,7 @@ internal static class VulkanGlslPreamble
// more keeps it, because a shader that opted into 460 did so for
// a feature and quietly downgrading it would be a silent change.
output.AppendLine(HighestVersion(trimmed) >= 460 ? "#version 460 core" : "#version 450 core");
output.Append(Build(stage));
output.Append(Build(stage, includePackUniformSet, includePackTextureIndices));
injected = true;
continue;
}

View file

@ -0,0 +1,48 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"Silk.NET.Shaderc": {
"type": "Direct",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "+pXfOhmSCeeMECOo9HMi3C63LVbQ7FBxPFgxPKOT6mXD8Gg/90Wt4fLX4LqUuVbGid5LW6BXAUu1g17XQoawdA==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Shaderc.Native": "2.23.0"
}
},
"Microsoft.DotNet.PlatformAbstractions": {
"type": "Transitive",
"resolved": "3.1.6",
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
"resolved": "9.0.9",
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
},
"Silk.NET.Core": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
"dependencies": {
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
"Microsoft.Extensions.DependencyModel": "9.0.9"
}
},
"Silk.NET.Shaderc.Native": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "H6OMLIWdh2HITvkmj+ALs8LTIdQvQ2/JTtkDXinVbJ3xxrQIBXhVmc9jnuTQ67YDybGlENSMrgthzhLwK0rjnQ=="
}
},
"net10.0/win-x64": {
"Silk.NET.Shaderc.Native": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "H6OMLIWdh2HITvkmj+ALs8LTIdQvQ2/JTtkDXinVbJ3xxrQIBXhVmc9jnuTQ67YDybGlENSMrgthzhLwK0rjnQ=="
}
}
}
}