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,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<RootNamespace>AcDream.Tools.RenderPackValidator</RootNamespace>
<AssemblyName>AcDream.Tools.RenderPackValidator</AssemblyName>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="AcDream.RenderPackValidator.Tests" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,141 @@
using System.Text.Json;
using AcDream.Plugin.Abstractions;
namespace AcDream.Tools.RenderPackValidator;
internal sealed record PackManifest(
string Id,
string DisplayName,
string Version,
string EntryDll,
int ApiVersion,
IReadOnlyList<string> Dependencies,
IReadOnlyList<string> Kinds)
{
internal static ValidationOutcome Parse(string json)
{
ManifestDto? value;
try
{
value = JsonSerializer.Deserialize<ManifestDto>(json, JsonOptions);
}
catch (JsonException error)
{
return ValidationOutcome.Invalid($"plugin.json is invalid JSON: {error.Message}");
}
if (value is null)
return ValidationOutcome.Invalid("plugin.json is empty.");
if (!StableId.IsValid(value.Id))
return ValidationOutcome.Invalid("plugin.json id must be a stable lowercase logical id.");
if (string.IsNullOrWhiteSpace(value.DisplayName))
return ValidationOutcome.Invalid("plugin.json is missing displayName.");
if (string.IsNullOrWhiteSpace(value.Version))
return ValidationOutcome.Invalid("plugin.json is missing version.");
if (!System.Version.TryParse(value.Version, out _))
return ValidationOutcome.Invalid("plugin.json version must be a dotted numeric version.");
if (!SafeRelativePath.IsValid(value.EntryDll, requireDll: true))
return ValidationOutcome.Invalid("plugin.json entryDll must be a safe relative .dll path.");
if (!PluginApi.IsSupported(value.ApiVersion))
{
return ValidationOutcome.Invalid(
$"plugin.json apiVersion {value.ApiVersion} is unsupported; "
+ $"this SDK supports {PluginApi.MinimumSupported}..{PluginApi.Current}.");
}
string[] dependencies = value.Dependencies ?? [];
if (dependencies.Any(static dependency => !StableId.IsValid(dependency)))
return ValidationOutcome.Invalid("plugin.json contains an invalid dependency id.");
string[] kinds = value.Kinds ?? ["gameplay"];
if (kinds.Length == 0)
return ValidationOutcome.Invalid("plugin.json kinds must contain at least one entry.");
foreach (string? kind in kinds)
{
if (!string.Equals(kind, "gameplay", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(kind, "renderPack", StringComparison.OrdinalIgnoreCase))
{
return ValidationOutcome.Invalid(
$"plugin.json contains unknown kind '{kind ?? "<null>"}'.");
}
}
if (!kinds.Contains("renderPack", StringComparer.OrdinalIgnoreCase))
return ValidationOutcome.Invalid("plugin.json does not declare the renderPack kind.");
return ValidationOutcome.Valid(new PackManifest(
value.Id!,
value.DisplayName!,
value.Version!,
value.EntryDll!,
value.ApiVersion,
dependencies,
kinds.Distinct(StringComparer.OrdinalIgnoreCase).ToArray()));
}
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
};
private sealed class ManifestDto
{
public string? Id { get; set; }
public string? DisplayName { get; set; }
public string? Version { get; set; }
public string? EntryDll { get; set; }
public int ApiVersion { get; set; }
public string[]? Dependencies { get; set; }
public string[]? Kinds { get; set; }
}
}
internal readonly record struct ValidationOutcome(
bool Success,
string? Reason,
PackManifest? Manifest = null)
{
internal static ValidationOutcome Valid(PackManifest manifest) =>
new(true, null, manifest);
internal static ValidationOutcome Invalid(string reason) =>
new(false, reason);
}
internal static class StableId
{
internal static bool IsValid(string? value)
{
if (string.IsNullOrWhiteSpace(value) || value.Length > 128)
return false;
if (value[0] is < 'a' or > 'z')
return false;
return value.All(static character =>
character is >= 'a' and <= 'z'
|| character is >= '0' and <= '9'
|| character is '.' or '-' or '_');
}
}
internal static class SafeRelativePath
{
internal static bool IsValid(string? value, bool requireDll = false)
{
if (string.IsNullOrWhiteSpace(value)
|| value.Length > 512
|| Path.IsPathRooted(value)
|| value.Contains('\\'))
{
return false;
}
string[] segments = value.Split('/');
if (segments.Any(static segment =>
segment.Length == 0 || segment is "." or ".."))
{
return false;
}
return !requireDll
|| string.Equals(Path.GetExtension(value), ".dll", StringComparison.OrdinalIgnoreCase);
}
}

View file

@ -0,0 +1,7 @@
namespace AcDream.Tools.RenderPackValidator;
internal static class Program
{
private static int Main(string[] args) =>
RenderPackValidatorCommand.Run(args, Console.Out, Console.Error);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,235 @@
using System.Reflection;
using System.Runtime.Loader;
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.Tools.RenderPackValidator;
internal static class RenderPackValidatorCommand
{
internal static int Run(
IReadOnlyList<string> args,
TextWriter output,
TextWriter error)
{
ArgumentNullException.ThrowIfNull(args);
ArgumentNullException.ThrowIfNull(output);
ArgumentNullException.ThrowIfNull(error);
if (args.Count != 1 || args[0] is "-h" or "--help")
{
TextWriter target = args.Count == 1 ? output : error;
target.WriteLine("usage: RenderPackValidator <built-pack-directory>");
target.WriteLine("The directory must contain plugin.json and its built entry DLL.");
return args.Count == 1 ? 0 : 2;
}
string packDirectory;
try
{
packDirectory = Path.GetFullPath(args[0]);
}
catch (Exception exception) when (exception is ArgumentException
or NotSupportedException
or PathTooLongException)
{
error.WriteLine($"FAIL: invalid pack directory: {exception.Message}");
return 2;
}
string manifestPath = Path.Combine(packDirectory, "plugin.json");
if (!File.Exists(manifestPath))
{
error.WriteLine($"FAIL: plugin.json was not found in '{packDirectory}'.");
return 1;
}
string manifestJson;
try
{
manifestJson = File.ReadAllText(manifestPath);
}
catch (Exception exception) when (exception is IOException
or UnauthorizedAccessException)
{
error.WriteLine($"FAIL: plugin.json could not be read: {exception.Message}");
return 1;
}
ValidationOutcome parsed = PackManifest.Parse(manifestJson);
if (!parsed.Success)
{
error.WriteLine($"FAIL: {parsed.Reason}");
return 1;
}
PackManifest manifest = parsed.Manifest!;
string entryPath = ResolveInside(packDirectory, manifest.EntryDll);
if (!File.Exists(entryPath))
{
error.WriteLine($"FAIL: entry DLL '{manifest.EntryDll}' does not exist.");
return 1;
}
PackLoadContext? loadContext = null;
try
{
loadContext = new PackLoadContext(entryPath);
Assembly assembly = loadContext.LoadFromAssemblyPath(entryPath);
Type[] entryPoints = GetLoadableTypes(assembly)
.Where(static type =>
!type.IsAbstract
&& !type.IsInterface
&& typeof(IRenderPackPlugin).IsAssignableFrom(type))
.ToArray();
if (entryPoints.Length != 1)
{
error.WriteLine(
$"FAIL: entry DLL must contain exactly one public constructible "
+ $"IRenderPackPlugin; found {entryPoints.Length}.");
return 1;
}
if (!entryPoints[0].IsVisible
|| entryPoints[0].GetConstructor(Type.EmptyTypes) is null)
{
error.WriteLine(
$"FAIL: render-pack entry point '{entryPoints[0].FullName}' "
+ "has no public parameterless constructor.");
return 1;
}
var plugin = (IRenderPackPlugin)Activator.CreateInstance(entryPoints[0])!;
using var registrations = new CapturingRenderPackRegistry();
plugin.Register(registrations);
if (registrations.Entries.Count == 0)
{
error.WriteLine("FAIL: render-pack entry point registered no packs.");
return 1;
}
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (CapturedRenderPack entry in registrations.Entries)
{
RenderPackSdkValidationResult descriptor =
RenderPackSdkValidator.ValidateDescriptor(entry.Descriptor);
if (!descriptor.Success)
{
error.WriteLine($"FAIL: {descriptor.Reason}");
return 1;
}
if (!seen.Add(entry.Descriptor.Id))
{
error.WriteLine(
$"FAIL: render-pack id '{entry.Descriptor.Id}' was registered more than once.");
return 1;
}
RenderPackSdkValidationResult assets =
RenderPackSdkValidator.ValidateAssets(entry.Descriptor, entry.Assets);
if (!assets.Success)
{
error.WriteLine($"FAIL: {assets.Reason}");
return 1;
}
output.WriteLine(
$"OK: {entry.Descriptor.Id} {entry.Descriptor.PackVersion} "
+ $"(API {entry.Descriptor.PackApiVersion}, "
+ $"{entry.Descriptor.QualityPresets.Count} preset(s)).");
}
output.WriteLine(
$"Validated {registrations.Entries.Count} render pack(s) from '{manifest.Id}'.");
return 0;
}
catch (Exception exception)
{
error.WriteLine(
$"FAIL: pack entry point could not be inspected: "
+ exception.GetBaseException().Message);
return 1;
}
finally
{
loadContext?.Unload();
}
}
private static string ResolveInside(string root, string relativePath)
{
string resolved = Path.GetFullPath(Path.Combine(root, relativePath));
string prefix = Path.TrimEndingDirectorySeparator(root)
+ Path.DirectorySeparatorChar;
StringComparison comparison = OperatingSystem.IsWindows()
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal;
if (!resolved.StartsWith(prefix, comparison))
throw new UnauthorizedAccessException("entryDll escapes the pack directory.");
return resolved;
}
private static IEnumerable<Type> GetLoadableTypes(Assembly assembly)
{
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException exception)
{
return exception.Types.OfType<Type>();
}
}
private sealed class PackLoadContext(string entryPath)
: AssemblyLoadContext("render-pack-sdk-validator", isCollectible: true)
{
private const string AbstractionsAssemblyName = "AcDream.Plugin.Abstractions";
private readonly AssemblyDependencyResolver _resolver = new(entryPath);
protected override Assembly? Load(AssemblyName assemblyName)
{
if (assemblyName.Name == AbstractionsAssemblyName)
return null;
string? path = _resolver.ResolveAssemblyToPath(assemblyName);
return path is null ? null : LoadFromAssemblyPath(path);
}
}
}
internal sealed class CapturingRenderPackRegistry : IRenderPackRegistry, IDisposable
{
private readonly List<CapturedRenderPack> _entries = [];
private bool _disposed;
internal IReadOnlyList<CapturedRenderPack> Entries => _entries;
public IDisposable Register(RenderPackDescriptor descriptor, IRenderPackAssets assets)
{
ObjectDisposedException.ThrowIf(_disposed, this);
ArgumentNullException.ThrowIfNull(descriptor);
ArgumentNullException.ThrowIfNull(assets);
var entry = new CapturedRenderPack(descriptor, assets);
_entries.Add(entry);
return new Registration(_entries, entry);
}
public void Dispose()
{
_disposed = true;
_entries.Clear();
}
private sealed class Registration(
List<CapturedRenderPack> entries,
CapturedRenderPack entry) : IDisposable
{
private List<CapturedRenderPack>? _entries = entries;
public void Dispose() =>
Interlocked.Exchange(ref _entries, null)?.Remove(entry);
}
}
internal sealed record CapturedRenderPack(
RenderPackDescriptor Descriptor,
IRenderPackAssets Assets);

View file

@ -0,0 +1,10 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"acdream.plugin.abstractions": {
"type": "Project"
}
}
}
}

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=="
}
}
}
}

View file

@ -0,0 +1,325 @@
Set-StrictMode -Version Latest
function Get-AtmosphericPresetUnavailableReasonClassification {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$Reason,
[Parameter(Mandatory = $true)]
[ValidateSet('low', 'medium', 'high', 'auto')][string]$Preset)
if ($Preset -eq 'auto') {
$performancePattern = "^Automatic quality disabled render pack 'acdream\.atmospheric' because Low remained over its declared performance budget for 180 stable samples: GPU p99 (?<gpu>[0-9]+(?:\.[0-9]+)?) ms \(budget (?<gpuBudget>[0-9]+(?:\.[0-9]+)?) ms\), CPU p99 (?<cpu>[0-9]+(?:\.[0-9]+)?) ms \(budget (?<cpuBudget>[0-9]+(?:\.[0-9]+)?) ms\), resident GPU bytes (?<resident>[0-9]+) \(budget (?<residentBudget>[0-9]+)\)\.$"
$performance = [Regex]::Match(
$Reason,
$performancePattern,
[Text.RegularExpressions.RegexOptions]::CultureInvariant)
if ($performance.Success) {
$culture = [Globalization.CultureInfo]::InvariantCulture
$gpu = [double]::Parse($performance.Groups['gpu'].Value, $culture)
$gpuBudget = [double]::Parse($performance.Groups['gpuBudget'].Value, $culture)
$cpu = [double]::Parse($performance.Groups['cpu'].Value, $culture)
$cpuBudget = [double]::Parse($performance.Groups['cpuBudget'].Value, $culture)
$resident = [long]::Parse($performance.Groups['resident'].Value, $culture)
$residentBudget = [long]::Parse($performance.Groups['residentBudget'].Value, $culture)
if ($gpu -gt $gpuBudget -or $cpu -gt $cpuBudget -or
$resident -gt $residentBudget) {
return [pscustomobject][ordered]@{
Supported = $true
Classification = 'PerformanceUnavailable'
Reason = $Reason
}
}
}
foreach ($resolvedPreset in @('low', 'medium', 'high')) {
$classification = Get-AtmosphericPresetUnavailableReasonClassification `
-Reason $Reason -Preset $resolvedPreset
if ($classification.Supported) { return $classification }
}
return [pscustomobject][ordered]@{
Supported = $false
Classification = 'UnexpectedFailure'
Reason = $Reason
}
}
# Keep this allow-list deliberately narrow. A shader, validation, pipeline,
# draw, or ordinary runtime failure must not become a passing
# "unavailable" row merely because its prose contains words such as
# unsupported, resource, memory, or capability.
$escapedPreset = [Regex]::Escape($Preset)
$preparationPrefix = "(?:Render pack 'acdream\.atmospheric' could not be prepared: )?"
$resourcePatterns = @(
"^${preparationPrefix}Render pack preset '$escapedPreset' needs [1-9][0-9]* resident GPU bytes at [1-9][0-9]*x[1-9][0-9]*; its declared ceiling is [1-9][0-9]*\. Select a compatible preset or reduce the main-world resolution\.$",
"^${preparationPrefix}Render pack preset '$escapedPreset' needs [1-9][0-9]* resident GPU bytes at [1-9][0-9]*x[1-9][0-9]*; this host permits [1-9][0-9]* under its .+ policy\.$",
"^${preparationPrefix}Render pack preset '$escapedPreset' needs [1-9][0-9]* transient multisample GPU bytes at [1-9][0-9]*x[1-9][0-9]* x[1-9][0-9]*; this host permits [1-9][0-9]* under its .+ policy\.$",
"^Directional shadow rendering failed: Render pack preset '$escapedPreset' needs [1-9][0-9]* resident GPU bytes after materializing its scene-dependent shadow command buffers; the active pack budget is [1-9][0-9]* bytes\.$",
"^Preset '$escapedPreset' declares a [1-9][0-9]*-byte resident GPU ceiling, but this host permits [1-9][0-9]* bytes under its .+ policy\.$"
)
foreach ($pattern in $resourcePatterns) {
if ([Regex]::IsMatch($Reason, $pattern, [Text.RegularExpressions.RegexOptions]::CultureInvariant)) {
return [pscustomobject][ordered]@{
Supported = $true
Classification = 'ResourceUnavailable'
Reason = $Reason
}
}
}
$capabilityPatterns = @(
"^Pack 'acdream\.atmospheric' requires unsupported capability '[A-Za-z][A-Za-z0-9]*'\.$",
"^Preset '$escapedPreset' requires unsupported capability '[A-Za-z][A-Za-z0-9]*'\.$",
"^Preset '$escapedPreset' resource '[a-z0-9][a-z0-9.-]*' needs [1-9][0-9]* image-array layers; this device provides [0-9]+\.$",
"^Preset '$escapedPreset' resource '[a-z0-9][a-z0-9.-]*' needs [1-9][0-9]*(?:\.[0-9]+)?x[1-9][0-9]*(?:\.[0-9]+)?; this device's maximum 2-D image edge is [1-9][0-9]*\.$",
"^${preparationPrefix}Render pack preset '$escapedPreset' resolves an image to [1-9][0-9]*x[1-9][0-9]* at [1-9][0-9]*x[1-9][0-9]*; this device's maximum 2-D image edge is [1-9][0-9]*\.$",
"^${preparationPrefix}Render pack preset '$escapedPreset' needs [1-9][0-9]* image-array layers; this device provides [0-9]+\.$"
)
foreach ($pattern in $capabilityPatterns) {
if ([Regex]::IsMatch($Reason, $pattern, [Text.RegularExpressions.RegexOptions]::CultureInvariant)) {
return [pscustomobject][ordered]@{
Supported = $true
Classification = 'CapabilityUnavailable'
Reason = $Reason
}
}
}
return [pscustomobject][ordered]@{
Supported = $false
Classification = 'UnexpectedFailure'
Reason = $Reason
}
}
function Test-AtmosphericPerformanceMetadataEvidence {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$MetadataPath,
[Parameter(Mandatory = $true)]
[ValidateSet('retail', 'low', 'medium', 'high', 'auto')][string]$Preset,
[Parameter(Mandatory = $true)][int]$ExpectedWidth,
[Parameter(Mandatory = $true)][int]$ExpectedHeight,
[switch]$AllowSafeFallback)
$failures = [Collections.Generic.List[string]]::new()
function Require([object]$Value, [string]$Name, [string]$Context) {
$property = $Value.PSObject.Properties[$Name]
if ($null -eq $property) { throw "$Context is missing required property '$Name'." }
return $property.Value
}
function Fail([string]$Message) { $null = $failures.Add($Message) }
function Require-FiniteNonNegative([object]$Value, [string]$Name) {
$number = [double](Require $Value $Name 'RenderPack.Performance')
if (-not [double]::IsFinite($number) -or $number -lt 0) {
Fail "$Name must be finite and non-negative"
}
return $number
}
$metadata = Get-Content -Raw -LiteralPath $MetadataPath | ConvertFrom-Json
if ([int](Require $metadata 'SchemaVersion' 'metadata') -ne 1) {
Fail 'metadata SchemaVersion must be 1'
}
if ([int](Require $metadata 'Width' 'metadata') -ne $ExpectedWidth -or
[int](Require $metadata 'Height' 'metadata') -ne $ExpectedHeight) {
Fail "capture extent must be ${ExpectedWidth}x${ExpectedHeight}"
}
$pack = Require $metadata 'RenderPack' 'metadata'
$enhanced = $Preset -ne 'retail'
$state = [int](Require $pack 'State' 'RenderPack')
$fallback = $enhanced -and $state -eq 3
$expectedPackId = if ($enhanced -and -not $fallback) { 'acdream.atmospheric' } else { 'retail' }
$expectedVersion = if ($enhanced -and -not $fallback) { '1.0.0' } else { '' }
$expectedPreset = if ($enhanced -and -not $fallback) { $Preset } else { 'off' }
$expectedState = if ($enhanced -and -not $fallback) { 2 } elseif ($fallback) { 3 } else { 0 }
$actualEffectiveQuality = [string](
Require $pack 'EffectiveQuality' 'RenderPack')
$expectedQuality = if ($enhanced -and -not $fallback -and $Preset -ne 'auto') {
$Preset
} else { 'off' }
foreach ($comparison in @(
@('PackId', $expectedPackId), @('PackVersion', $expectedVersion),
@('PresetId', $expectedPreset))) {
$actual = [string](Require $pack ([string]$comparison[0]) 'RenderPack')
if ($actual -cne [string]$comparison[1]) {
Fail "$($comparison[0]) was '$actual', expected '$($comparison[1])'"
}
}
if ($enhanced -and -not $fallback -and $Preset -eq 'auto') {
if ($actualEffectiveQuality -cnotin @('low', 'medium', 'high')) {
Fail "Automatic EffectiveQuality was '$actualEffectiveQuality', expected low, medium, or high"
}
}
elseif ($actualEffectiveQuality -cne $expectedQuality) {
Fail "EffectiveQuality was '$actualEffectiveQuality', expected '$expectedQuality'"
}
if ($state -ne $expectedState) {
Fail "activation state must be $expectedState"
}
$generation = [int](Require $pack 'ActivationGeneration' 'RenderPack')
if (($fallback -and $generation -lt 1) -or
($enhanced -and -not $fallback -and $Preset -eq 'auto' -and
$generation -lt 1) -or
($enhanced -and -not $fallback -and $Preset -ne 'auto' -and
$generation -ne 1) -or
(-not $enhanced -and $generation -ne 0)) {
Fail "activation generation is invalid for state $state"
}
$failureReason = [string](Require $pack 'FailureReason' 'RenderPack')
$unavailableClassification = $null
if ($fallback) {
if (-not $AllowSafeFallback) {
Fail 'FailedToRetail fallback was not explicitly allowed for this capture'
}
if ([string]::IsNullOrWhiteSpace($failureReason)) {
Fail 'FailedToRetail fallback must preserve one exact failure reason'
}
else {
$reasonClassification = Get-AtmosphericPresetUnavailableReasonClassification `
-Reason $failureReason -Preset $Preset
$unavailableClassification = $reasonClassification.Classification
if (-not $reasonClassification.Supported) {
Fail 'FailedToRetail reason is not a strict resource/capability/Auto-performance unavailability'
}
}
}
elseif (-not [string]::IsNullOrWhiteSpace($failureReason)) {
Fail 'render-pack FailureReason must be empty'
}
$performance = Require $pack 'Performance' 'RenderPack'
$topResident = [long](Require $pack 'RetainedGpuBytes' 'RenderPack')
$topTransient = [long](Require $pack 'TransientGpuBytes' 'RenderPack')
$nestedResident = [long](Require $performance 'ResidentGpuBytes' 'RenderPack.Performance')
$nestedTransient = [long](Require $performance 'TransientGpuBytes' 'RenderPack.Performance')
if ($topResident -ne $nestedResident) { Fail 'top-level and performance resident GPU bytes disagree' }
if ($topTransient -ne $nestedTransient) { Fail 'top-level and performance transient GPU bytes disagree' }
$casterCount = [int](Require $pack 'ShadowCasterCount' 'RenderPack')
$cascadeCount = [int](Require $pack 'CascadeDrawCount' 'RenderPack')
$classificationCalls = [int](Require $pack 'CpuClassificationCalls' 'RenderPack')
$drawCalls = [int](Require $pack 'DrawCalls' 'RenderPack')
$dispatchCalls = [int](Require $pack 'DispatchCalls' 'RenderPack')
$imageCount = [int](Require $pack 'ImageCount' 'RenderPack')
$bufferCount = [int](Require $pack 'BufferCount' 'RenderPack')
$passes = @(Require $pack 'Passes' 'RenderPack')
if ($fallback) {
if ($casterCount -ne 0 -or $cascadeCount -ne 0 -or $classificationCalls -ne 0 -or
$drawCalls -ne 0 -or $dispatchCalls -ne 0 -or $passes.Count -ne 0 -or
$imageCount -ne 0 -or $bufferCount -ne 0 -or
$topResident -ne 0 -or $topTransient -ne 0) {
Fail 'FailedToRetail fallback must have zero pack work and resources'
}
foreach ($countName in @('CpuSampleCount', 'AbsoluteReceiverCpuSampleCount', 'GpuSampleCount')) {
if ([int](Require $performance $countName 'RenderPack.Performance') -ne 0) {
Fail "FailedToRetail fallback must have zero $countName"
}
}
foreach ($metric in @(
'IncrementalCpuMillisecondsP50', 'IncrementalCpuMillisecondsP95',
'IncrementalCpuMillisecondsP99', 'AbsoluteReceiverCpuMillisecondsP50',
'AbsoluteReceiverCpuMillisecondsP95', 'AbsoluteReceiverCpuMillisecondsP99',
'InclusiveGpuMillisecondsP50', 'InclusiveGpuMillisecondsP95',
'InclusiveGpuMillisecondsP99', 'ResidentGpuBytes', 'TransientGpuBytes')) {
if ([double](Require $performance $metric 'RenderPack.Performance') -ne 0) {
Fail "FailedToRetail fallback must report zero $metric"
}
}
}
elseif (-not $enhanced) {
if ($casterCount -ne 0 -or $cascadeCount -ne 0 -or $classificationCalls -ne 0 -or
$drawCalls -ne 0 -or $dispatchCalls -ne 0 -or $passes.Count -ne 0 -or
$imageCount -ne 0 -or $bufferCount -ne 0 -or
$topResident -ne 0 -or $topTransient -ne 0) {
Fail 'retail row must have zero pack work, resources, and classification'
}
}
else {
$shapePreset = if ($Preset -eq 'auto') {
$actualEffectiveQuality
} else { $Preset }
$expectedCascades = @{ low = 2; medium = 3; high = 4 }[$shapePreset]
if ($casterCount -le 0) { Fail 'enhanced row must contain at least one shadow caster' }
if ($cascadeCount -ne $expectedCascades) {
Fail "$Preset/$shapePreset must render exactly $expectedCascades cascades"
}
if ($classificationCalls -ne 0) { Fail 'warmed capture must perform zero CPU classifications' }
$expectedPassIds = [Collections.Generic.List[string]]::new()
$expectedPassIds.Add('atmospheric-world-receiver')
if ($shapePreset -eq 'low') {
$expectedPassIds.Add('directional-shadow-multiview')
}
else {
for ($cascade = 0; $cascade -lt $expectedCascades; $cascade++) {
$expectedPassIds.Add("directional-shadow-cascade-$cascade")
}
}
$postPassIds = @(
'atmospheric-sun-occlusion', 'atmospheric-sun-rays',
'atmospheric-volumetric-shafts', 'atmospheric-bloom-downsample',
'atmospheric-bloom-blur-horizontal', 'atmospheric-bloom-blur-vertical',
'atmospheric-filmic')
foreach ($id in $postPassIds) { $expectedPassIds.Add($id) }
$actualPassIds = @($passes | ForEach-Object { [string](Require $_ 'PassId' 'RenderPack.Passes[]') })
if (($actualPassIds -join '|') -cne (@($expectedPassIds) -join '|')) {
Fail "pass order/shape was '$($actualPassIds -join ',')'"
}
# The fixed offline scene resolves five prepared shadow submissions:
# terrain plus the stable opaque/alpha-cutout world runs. Low replays
# those once through multiview; Medium/High replay them per cascade.
$shadowDrawsPerPass = 5
# Low now uses the quarter-resolution separable graph as well. Its
# volumetric pass remains declared for one stable API shape but records
# zero draws because the Low preset disables volumetric strength.
$postDraws = 6
$expectedDraws = $postDraws + $(if ($shapePreset -eq 'low') {
$shadowDrawsPerPass
} else {
$shadowDrawsPerPass * $expectedCascades
})
$summedDraws = [int](($passes | Measure-Object -Property DrawCalls -Sum).Sum)
$summedDispatches = [int](($passes | Measure-Object -Property DispatchCalls -Sum).Sum)
if ($drawCalls -ne $expectedDraws -or $summedDraws -ne $expectedDraws) {
Fail "draw shape must total $expectedDraws calls"
}
if ($dispatchCalls -ne 0 -or $summedDispatches -ne 0) {
Fail 'atmospheric pack must issue zero dispatch calls'
}
foreach ($pass in $passes) {
$passId = [string](Require $pass 'PassId' 'RenderPack.Passes[]')
$expectedPassDraws = if ($passId -like 'directional-shadow-*') {
$shadowDrawsPerPass
}
elseif ($passId -in @('atmospheric-world-receiver', 'atmospheric-volumetric-shafts')) { 0 }
else { 1 }
if ([int](Require $pass 'DrawCalls' 'RenderPack.Passes[]') -ne $expectedPassDraws -or
[int](Require $pass 'DispatchCalls' 'RenderPack.Passes[]') -ne 0) {
Fail "pass '$passId' must record exactly $expectedPassDraws draws and zero dispatches"
}
}
foreach ($countName in @('CpuSampleCount', 'AbsoluteReceiverCpuSampleCount', 'GpuSampleCount')) {
if ([int](Require $performance $countName 'RenderPack.Performance') -ne 2048) {
Fail "$countName must contain the complete 2048-sample window"
}
}
foreach ($metric in @(
'IncrementalCpuMillisecondsP50', 'IncrementalCpuMillisecondsP95',
'IncrementalCpuMillisecondsP99', 'AbsoluteReceiverCpuMillisecondsP50',
'AbsoluteReceiverCpuMillisecondsP95', 'AbsoluteReceiverCpuMillisecondsP99',
'InclusiveGpuMillisecondsP50', 'InclusiveGpuMillisecondsP95',
'InclusiveGpuMillisecondsP99')) { $null = Require-FiniteNonNegative $performance $metric }
}
return [pscustomobject][ordered]@{
Passed = $failures.Count -eq 0
Failures = @($failures)
Outcome = if ($fallback) { 'Unavailable' } elseif ($enhanced) { 'Active' } else { 'Retail' }
UnavailableClassification = $unavailableClassification
FailureReason = if ($fallback) { $failureReason } else { $null }
EffectiveQuality = $actualEffectiveQuality
ShadowCasterCount = $casterCount
CascadeDrawCount = $cascadeCount
CpuClassificationCalls = $classificationCalls
PassIds = @($passes | ForEach-Object { [string]$_.PassId })
}
}

View file

@ -36,6 +36,12 @@
Use glslc when available. On by default; pass -PreferSdk:$false to force the
managed path, which is what a comparison between the two wants.
.PARAMETER ForceRecompile
Recompile even when the existing manifest proves that a source and its
committed SPIR-V artifact are unchanged. Use this only when validating a
compiler/toolchain change; ordinary regeneration preserves exact retail
binaries while compiling every changed shader.
.EXAMPLE
tools/compile-shaders.ps1
#>
@ -43,7 +49,8 @@
param(
[string]$ShadersDirectory,
[string]$OutputDirectory,
[bool]$PreferSdk = $true
[bool]$PreferSdk = $true,
[switch]$ForceRecompile
)
$ErrorActionPreference = 'Stop'
@ -149,7 +156,9 @@ if (-not (Test-Path $native)) {
Write-Step "shaderc native: $native"
Write-Step "compiling $ShadersDirectory -> $OutputDirectory"
& dotnet $binary $ShadersDirectory $OutputDirectory
$compilerArguments = @($binary, $ShadersDirectory, $OutputDirectory)
if ($ForceRecompile) { $compilerArguments += '--force' }
& dotnet @compilerArguments
if ($LASTEXITCODE -ne 0) { throw "Shader compilation failed with exit code $LASTEXITCODE." }
Write-Step 'done'

View file

@ -0,0 +1,51 @@
# Connected outdoor/interior exposure comparison for Atmospheric Rendering.
# Captures the retail-faithful path and the High preset at the same authored
# locations, then exercises the UI-default Low preset last so a fail-safe
# fallback records its exact controller reason without losing the comparisons.
wait world-ready 90000
wait world-visible 30000
# Dense outdoor receiver/caster scene.
command /teleloc 0x09040008 11.4 188.6 87.705
wait materialized 1 90000
wait world-visible 30000
sleep 5000
renderpack select retail
wait render-pack retail 90000
sleep 3000
screenshot exposure_outdoor_retail 15000
renderpack select high
wait render-pack high 90000
sleep 3000
screenshot exposure_outdoor_atmospheric_high 15000
renderpack disable
wait render-pack retail 90000
# Facility Hub interior: EnvCell lighting, no outdoor directional-shadow path.
command /teleloc 0x8A020164 70.35 -40.66 -5.9
wait materialized 2 90000
wait world-visible 30000
sleep 5000
screenshot exposure_interior_retail 15000
renderpack select high
wait render-pack high 90000
sleep 3000
screenshot exposure_interior_atmospheric_high 15000
renderpack disable
wait render-pack retail 90000
# Return outdoors and exercise the first/default selectable preset last.
command /teleloc 0x09040008 11.4 188.6 87.705
wait materialized 3 90000
wait world-visible 30000
sleep 3000
renderpack select low
wait render-pack low 90000
sleep 3000
screenshot exposure_outdoor_atmospheric_low 15000
checkpoint atmospheric_exposure_comparison

View file

@ -0,0 +1,341 @@
# Shared render-pack selection seam for connected graphical gates.
$script:ConnectedGateEnvironmentNames = @(
'ACDREAM_AUTOMATION_ARTIFACT_DIR', 'ACDREAM_CACHE_DIR',
'ACDREAM_COLLISION_SHADOW_DIR', 'ACDREAM_COLLISION_SHADOW_EVERY',
'ACDREAM_CONFIG_DIR', 'ACDREAM_DATA_DIR', 'ACDREAM_DAT_DIR',
'ACDREAM_DEVTOOLS', 'ACDREAM_DUMP_MOVE_TRUTH', 'ACDREAM_FRAME_HISTORY',
'ACDREAM_FRAME_PROF', 'ACDREAM_LIVE', 'ACDREAM_NET_DROP_DIR',
'ACDREAM_NET_DROP_PCT', 'ACDREAM_NET_DROP_SEED', 'ACDREAM_NO_AUDIO',
'ACDREAM_PAK_PATH', 'ACDREAM_RENDER_BACKEND', 'ACDREAM_RETAIL_UI',
'ACDREAM_AUTOMATION_EXACT_FRAMEBUFFER', 'ACDREAM_DAY_GROUP',
'ACDREAM_WORLD_TIME', 'ACDREAM_SKY_PHASE_SECONDS',
'ACDREAM_ORBIT_DISTANCE_METERS', 'ACDREAM_ORBIT_YAW_DEGREES',
'ACDREAM_ORBIT_PITCH_DEGREES', 'ACDREAM_VULKAN_DEVICE',
'ACDREAM_VULKAN_FORCE_UNSUPPORTED', 'ACDREAM_VULKAN_PROBE',
'ACDREAM_VULKAN_PROBE_FRAMES', 'ACDREAM_TEST_HOST',
'ACDREAM_TEST_PASS', 'ACDREAM_TEST_PORT', 'ACDREAM_TEST_USER',
'ACDREAM_UI_PROBE_DUMP', 'ACDREAM_UI_PROBE_SCRIPT',
'ACDREAM_UNCAPPED_RENDER', 'ACDREAM_WB_DIAG'
)
function Assert-ConnectedGateSafeLeafName {
param([Parameter(Mandatory = $true)][string]$Name)
if ($Name -notmatch '^[A-Za-z0-9][A-Za-z0-9_-]{0,79}$') {
throw "Unsafe screenshot leaf name '$Name'."
}
}
function Assert-ConnectedGateContainedPath {
param([Parameter(Mandatory = $true)][string]$Root,
[Parameter(Mandatory = $true)][string]$Path)
$rootFull = [IO.Path]::GetFullPath($Root).TrimEnd(
[IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar)
$pathFull = [IO.Path]::GetFullPath($Path)
$prefix = $rootFull + [IO.Path]::DirectorySeparatorChar
if (-not $pathFull.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) {
throw "Path '$pathFull' is not contained by gate root '$rootFull'."
}
return $pathFull
}
function Assert-ConnectedGateNoReparsePoint {
param([Parameter(Mandatory = $true)][string]$Path)
if ((Get-Item -LiteralPath $Path).Attributes -band [IO.FileAttributes]::ReparsePoint) {
throw "Gate path must not be a reparse point: '$Path'."
}
}
function Get-ConnectedRenderPackExpectation {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('retail', 'low', 'medium', 'high', 'auto')]
[string]$Preset
)
return [pscustomobject][ordered]@{
RequestedPreset = $Preset
PackId = if ($Preset -eq 'retail') { 'retail' } else { 'acdream.atmospheric' }
PackVersion = if ($Preset -eq 'retail') { $null } else { '1.0.0' }
PresetId = if ($Preset -eq 'retail') { 'off' } else { $Preset }
ExpectedState = if ($Preset -eq 'retail') { 0 } else { 2 }
ExpectedSchemaVersion = 1
}
}
function New-ConnectedRenderPackGateState {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$Root,
[Parameter(Mandatory = $true)]
[ValidateSet('retail', 'low', 'medium', 'high', 'auto')]
[string]$Preset,
[hashtable]$SettingOverrides = @{}
)
if ($Preset -eq 'retail' -and $SettingOverrides.Count -ne 0) {
throw 'Render-pack setting overrides require an enhanced render-pack preset.'
}
$rootFull = [IO.Path]::GetFullPath($Root)
if (-not (Test-Path -LiteralPath $rootFull -PathType Container)) {
throw "Connected gate root does not exist: '$rootFull'."
}
Assert-ConnectedGateNoReparsePoint $rootFull
$stateDirectory = Assert-ConnectedGateContainedPath $rootFull (Join-Path $rootFull 'isolated-state')
if (Test-Path -LiteralPath $stateDirectory) {
throw "Isolated gate state already exists: '$stateDirectory'."
}
# A connected closeout row must not inherit a diagnostic, content-path,
# budget, camera, weather, or device override from a prior shell run. Keep
# the explicitly-owned launch names for absent-variable restoration and
# also capture every currently-defined ACDREAM_* name so newly-added knobs
# fail isolated without requiring this seam to know their semantics first.
$transactionNames = @(
$script:ConnectedGateEnvironmentNames
Get-ChildItem Env: |
Where-Object { $_.Name -like 'ACDREAM_*' } |
Select-Object -ExpandProperty Name
) | Sort-Object -Unique
$previous = [ordered]@{}
foreach ($name in $transactionNames) {
$previous[$name] = [Environment]::GetEnvironmentVariable(
$name, [EnvironmentVariableTarget]::Process)
Remove-Item -LiteralPath "Env:$name" -ErrorAction SilentlyContinue
}
$configDirectory = Join-Path $stateDirectory 'config'
$dataDirectory = Join-Path $stateDirectory 'data'
$cacheDirectory = Join-Path $stateDirectory 'cache'
$null = New-Item -ItemType Directory -Path $configDirectory, $dataDirectory, $cacheDirectory
foreach ($path in @($stateDirectory, $configDirectory, $dataDirectory, $cacheDirectory)) {
Assert-ConnectedGateNoReparsePoint $path
}
$packId = if ($Preset -eq 'retail') { 'retail' } else { 'acdream.atmospheric' }
$packVersion = if ($Preset -eq 'retail') { $null } else { '1.0.0' }
$presetId = if ($Preset -eq 'retail') { 'off' } else { $Preset }
$expectedState = if ($Preset -eq 'retail') { 0 } else { 2 }
$orderedOverrides = [ordered]@{}
foreach ($key in @($SettingOverrides.Keys | Sort-Object)) {
if ([string]::IsNullOrWhiteSpace([string]$key)) {
throw 'Render-pack setting override IDs cannot be empty.'
}
$orderedOverrides[[string]$key] = [string]$SettingOverrides[$key]
}
[ordered]@{
display = [ordered]@{ renderPack = [ordered]@{
packId = $packId; packVersion = $packVersion; presetId = $presetId
settingOverrides = $orderedOverrides
} }
version = 3
} | ConvertTo-Json -Depth 8 |
Set-Content -Encoding utf8 -LiteralPath (Join-Path $configDirectory 'settings.json')
[Environment]::SetEnvironmentVariable('ACDREAM_CONFIG_DIR', $configDirectory, 'Process')
[Environment]::SetEnvironmentVariable('ACDREAM_DATA_DIR', $dataDirectory, 'Process')
[Environment]::SetEnvironmentVariable('ACDREAM_CACHE_DIR', $cacheDirectory, 'Process')
return [pscustomobject][ordered]@{
RequestedPreset = $Preset; PackId = $packId; PackVersion = $packVersion
PresetId = $presetId; ExpectedState = $expectedState; ExpectedSchemaVersion = 1
SettingOverrides = $orderedOverrides; StateDirectory = $stateDirectory
ConfigDirectory = $configDirectory; DataDirectory = $dataDirectory
CacheDirectory = $cacheDirectory; PreviousEnvironment = $previous
}
}
function Restore-ConnectedRenderPackGateEnvironment {
[CmdletBinding()]
param([Parameter(Mandatory = $true)][object]$State)
foreach ($entry in $State.PreviousEnvironment.GetEnumerator()) {
if ($null -eq $entry.Value) {
Remove-Item -LiteralPath "Env:$($entry.Key)" -ErrorAction SilentlyContinue
}
else {
[Environment]::SetEnvironmentVariable(
[string]$entry.Key, $entry.Value, [EnvironmentVariableTarget]::Process)
}
}
}
function Get-ConnectedGateBinaryIdentity {
[CmdletBinding()]
param([Parameter(Mandatory = $true)][string]$Repository,
[Parameter(Mandatory = $true)][string]$Executable,
[switch]$SkipBuild)
$sourceCommit = (& git -C $Repository rev-parse HEAD).Trim().ToLowerInvariant()
$trackedStatus = @(& git -C $Repository status --short --untracked-files=all)
$productVersion = [Diagnostics.FileVersionInfo]::GetVersionInfo($Executable).ProductVersion
$match = [regex]::Match([string]$productVersion, '(?i)(?<![0-9a-f])([0-9a-f]{40})(?![0-9a-f])')
$binaryCommit = if ($match.Success) { $match.Groups[1].Value.ToLowerInvariant() } else { $null }
if ($null -eq $binaryCommit) {
throw "Measured binary ProductVersion does not contain a full source commit: '$productVersion'."
}
if ($binaryCommit -cne $sourceCommit) {
throw "Measured binary commit $binaryCommit differs from checked-out source commit $sourceCommit."
}
if ($trackedStatus.Count -ne 0) {
throw 'Connected closeout evidence cannot prove binary/source identity while source changes are present.'
}
return [pscustomobject][ordered]@{
SourceCommit = $sourceCommit; SourceTrackedStatus = $trackedStatus
BinaryProductVersion = $productVersion; BinaryCommit = $binaryCommit
BinaryMatchesSource = $true; SkipBuild = [bool]$SkipBuild
}
}
function Add-ConnectedRenderPackMetadataFailures {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$ArtifactDirectory,
[Parameter(Mandatory = $true)][string[]]$ScreenshotNames,
[Parameter(Mandatory = $true)][object]$State,
[Parameter(Mandatory = $true)][System.Collections.IList]$Failures,
[Parameter(Mandatory = $true)][string]$Label
)
try {
Assert-ConnectedGateNoReparsePoint $ArtifactDirectory
$screenshotsDirectory = Assert-ConnectedGateContainedPath $ArtifactDirectory (
Join-Path $ArtifactDirectory 'screenshots')
if (Test-Path -LiteralPath $screenshotsDirectory -PathType Container) {
Assert-ConnectedGateNoReparsePoint $screenshotsDirectory
}
}
catch {
$null = $Failures.Add("${Label}: unsafe artifact directory: $($_.Exception.Message)")
return
}
foreach ($name in $ScreenshotNames) {
try { Assert-ConnectedGateSafeLeafName $name }
catch { $null = $Failures.Add("${Label}: $($_.Exception.Message)"); continue }
$metadataPath = Assert-ConnectedGateContainedPath $ArtifactDirectory (
Join-Path $ArtifactDirectory "screenshots\$name.metadata.json")
if (-not (Test-Path -LiteralPath $metadataPath)) {
$null = $Failures.Add("${Label}: render-pack metadata is missing for screenshot '$name'")
continue
}
try {
$metadata = Get-Content -Raw -LiteralPath $metadataPath | ConvertFrom-Json
if ([int]$metadata.SchemaVersion -ne [int]$State.ExpectedSchemaVersion) {
$null = $Failures.Add("${Label}: screenshot '$name' schema was $($metadata.SchemaVersion), expected $($State.ExpectedSchemaVersion)")
}
$actual = $metadata.RenderPack
if ($null -eq $actual) {
$null = $Failures.Add("${Label}: screenshot '$name' has no RenderPack metadata")
continue
}
foreach ($comparison in @(
@('PackId', [string]$State.PackId),
@('PackVersion', [string]$State.PackVersion),
@('PresetId', [string]$State.PresetId))) {
$property = [string]$comparison[0]; $expected = [string]$comparison[1]
if ([string]$actual.$property -cne $expected) {
$null = $Failures.Add("${Label}: screenshot '$name' $property '$($actual.$property)', expected '$expected'")
}
}
if ([int]$actual.State -ne [int]$State.ExpectedState) {
$null = $Failures.Add("${Label}: screenshot '$name' state was $($actual.State), expected $($State.ExpectedState)")
}
$expectedGeneration = if ($State.RequestedPreset -eq 'retail') { 0 } else { 1 }
if ([int]$actual.ActivationGeneration -lt $expectedGeneration) {
$null = $Failures.Add("${Label}: screenshot '$name' activation generation was $($actual.ActivationGeneration), expected at least $expectedGeneration")
}
$allowedQuality = if ($State.RequestedPreset -eq 'auto') { @('low', 'medium', 'high') }
elseif ($State.RequestedPreset -eq 'retail') { @('off') }
else { @([string]$State.RequestedPreset) }
if ([string]$actual.EffectiveQuality -cnotin $allowedQuality) {
$null = $Failures.Add("${Label}: screenshot '$name' effective quality '$($actual.EffectiveQuality)' is invalid for '$($State.RequestedPreset)'")
}
if (-not [string]::IsNullOrWhiteSpace([string]$actual.FailureReason)) {
$null = $Failures.Add("${Label}: screenshot '$name' render-pack failure: $($actual.FailureReason)")
}
if ([long]$actual.RetainedGpuBytes -ne [long]$actual.Performance.ResidentGpuBytes) {
$null = $Failures.Add("${Label}: screenshot '$name' retained GPU byte ledgers disagree")
}
if ([long]$actual.TransientGpuBytes -ne [long]$actual.Performance.TransientGpuBytes) {
$null = $Failures.Add("${Label}: screenshot '$name' transient GPU byte ledgers disagree")
}
$worldTransformUsage =
$actual.PSObject.Properties['SharedWorldTransformUsedInstances']
if ($null -eq $worldTransformUsage) {
$null = $Failures.Add(
"${Label}: screenshot '$name' has no combined shared-world-transform usage")
}
if ($State.RequestedPreset -eq 'retail') {
foreach ($property in @(
'RetainedGpuBytes',
'TransientGpuBytes',
'ImageCount',
'BufferCount',
'DrawCalls',
'DispatchCalls',
'ShadowCasterCount',
'CascadeDrawCount',
'CpuClassificationCalls',
'SharedWorldTransformUsedInstances')) {
if ([long]$actual.$property -ne 0) {
$null = $Failures.Add(
"${Label}: retail screenshot '$name' $property was $($actual.$property), expected zero pack work")
}
}
if (@($actual.Passes).Count -ne 0) {
$null = $Failures.Add(
"${Label}: retail screenshot '$name' recorded pack passes, expected none")
}
}
else {
if ([long]$actual.RetainedGpuBytes -le 0 -or
[int]$actual.ImageCount -le 0 -or
([int]$actual.DrawCalls + [int]$actual.DispatchCalls) -le 0 -or
@($actual.Passes).Count -le 0) {
$null = $Failures.Add(
"${Label}: enhanced screenshot '$name' was active but recorded no complete pack graph work")
}
# Every built-in enhanced preset includes Tier 2. When authored
# outdoor sun/weather policy produces non-zero shadow strength,
# the same captured frame must prove caster and cascade work.
if ([bool]$actual.Outdoor -and
[double]$actual.DirectionalShadowStrength -gt 0) {
$expectedCascades = @{
low = 2
medium = 3
high = 4
}[[string]$actual.EffectiveQuality]
if ([int]$actual.ShadowCasterCount -le 0) {
$null = $Failures.Add(
"${Label}: enhanced outdoor screenshot '$name' had positive shadow strength but no shadow casters")
}
if ($null -ne $worldTransformUsage -and
[long]$worldTransformUsage.Value -le 0) {
$null = $Failures.Add(
"${Label}: enhanced outdoor screenshot '$name' had positive shadow strength but no combined shared-world-transform usage")
}
if ($null -eq $expectedCascades -or
[int]$actual.CascadeDrawCount -ne [int]$expectedCascades) {
$null = $Failures.Add(
"${Label}: enhanced outdoor screenshot '$name' rendered $($actual.CascadeDrawCount) cascades, expected $expectedCascades for '$($actual.EffectiveQuality)'")
}
}
}
}
catch {
$null = $Failures.Add("${Label}: render-pack metadata for screenshot '$name' is invalid: $($_.Exception.Message)")
}
}
}
function Get-ConnectedRenderPackGateReport {
[CmdletBinding()]
param([Parameter(Mandatory = $true)][object]$State)
return [pscustomobject][ordered]@{
RequestedPreset = $State.RequestedPreset; PackId = $State.PackId
PackVersion = $State.PackVersion; PresetId = $State.PresetId
ExpectedActivationState = $State.ExpectedState
ExpectedMetadataSchemaVersion = $State.ExpectedSchemaVersion
SettingOverrides = $State.SettingOverrides
IsolatedStateDirectory = $State.StateDirectory
}
}

View file

@ -0,0 +1,59 @@
# Executable in-process Atmospheric Rendering transition route. Pack and
# framebuffer requests use the production Display-settings owners; waits
# observe the published render-pack generation and real framebuffer extent.
wait world-ready 90000
wait world-visible 30000
# Dense outdoor scene with buildings, foliage, the local equipped player,
# and any authoritative remote/monster publications supplied by ACE.
command /teleloc 0x09040008 11.4 188.6 87.705
wait materialized 1 90000
wait world-visible 30000
# Pin authored noon so a positive directional-shadow policy row is expected.
input press AcdreamCycleTimeOfDay
input press AcdreamCycleTimeOfDay
input press AcdreamCycleTimeOfDay
sleep 5000
# Explicit preset change, exact disable to retail, and exact re-enable of the
# prior enhanced selection. Each barrier observes frame-boundary publication.
renderpack select high
wait render-pack high 90000
input down MovementTurnRight
sleep 3000
screenshot transition_selected_high 15000
input up MovementTurnRight
renderpack disable
wait render-pack retail 90000
sleep 3000
screenshot transition_disabled_retail 15000
renderpack reenable
wait render-pack high 90000
sleep 3000
screenshot transition_reenabled_high 15000
# Exercise live window/swapchain recreation and wait for the real framebuffer,
# not just the persisted outer-window request.
resize 1024 768
wait framebuffer 1024 768 30000
sleep 3000
screenshot transition_resized_high 15000
# Authored selected-celestial and weather inputs use the canonical diagnostic actions.
input press AcdreamCycleTimeOfDay
sleep 3000
screenshot transition_dusk_high 15000
input press AcdreamCycleWeather
sleep 3000
screenshot transition_overcast_high 15000
input press AcdreamCycleWeather
sleep 3000
screenshot transition_rain_high 15000
checkpoint atmospheric_transitions

View file

@ -0,0 +1,196 @@
<#
.SYNOPSIS
Launches a visible, offline acdream Atmospheric Rendering preview.
.DESCRIPTION
Builds acdream unless -SkipBuild is supplied, creates a timestamped
disposable settings/profile directory under artifacts, selects the built-in
Atmospheric shader pack, and starts the real AcDream.App executable.
The preview deliberately removes inherited live-session credentials for the
child process. It never reads or rewrites the user's normal acdream settings.
.PARAMETER Preset
Atmospheric quality preset. Defaults to High.
.PARAMETER Resolution
Window resolution written to the disposable settings profile.
.PARAMETER EnableAudio
Explicitly enable OpenAL for this preview. Audio is disabled by default so
graphics inspection does not depend on the system audio device/driver.
.PARAMETER NoAudio
Legacy explicit spelling for the safe default. Retained so existing
invocations remain valid; do not combine it with -EnableAudio.
.PARAMETER SkipBuild
Launch the existing Release binary without rebuilding it.
.EXAMPLE
.\tools\launch-atmospheric-preview.ps1 -Preset High
.EXAMPLE
.\tools\launch-atmospheric-preview.ps1 -Preset High -EnableAudio
#>
[CmdletBinding()]
param(
[ValidateSet('Low', 'Medium', 'High', 'Auto')]
[string]$Preset = 'High',
[ValidatePattern('^[1-9][0-9]*x[1-9][0-9]*$')]
[string]$Resolution = '1920x1080',
[switch]$EnableAudio,
[switch]$NoAudio,
[switch]$SkipBuild
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$repo = Split-Path -Parent $PSScriptRoot
$exe = Join-Path $repo 'src\AcDream.App\bin\Release\net10.0\AcDream.App.exe'
$datDirectory = Join-Path $env:USERPROFILE "Documents\Asheron's Call"
$audioEnabled = [bool]$EnableAudio
if ($EnableAudio -and $NoAudio) {
throw '-EnableAudio and -NoAudio are mutually exclusive.'
}
if (-not $SkipBuild) {
Write-Host '[atmospheric-preview] building acdream Release'
& dotnet build (Join-Path $repo 'AcDream.slnx') -c Release --nologo -v q
if ($LASTEXITCODE -ne 0) {
throw "acdream Release build failed with exit code $LASTEXITCODE."
}
}
if (-not (Test-Path -LiteralPath $exe)) {
throw "acdream executable not found at '$exe'. Build it first or omit -SkipBuild."
}
if (-not (Test-Path -LiteralPath $datDirectory)) {
throw "AC DAT directory not found at '$datDirectory'."
}
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss-fff'
$root = Join-Path $repo "artifacts\atmospheric-rendering\visible-$($Preset.ToLowerInvariant())-$stamp"
$config = Join-Path $root 'state\config'
$data = Join-Path $root 'state\data'
$cache = Join-Path $root 'state\cache'
if (Test-Path -LiteralPath $root) {
throw "Preview artifact directory already exists: '$root'."
}
New-Item -ItemType Directory -Path $config, $data, $cache | Out-Null
$settings = [ordered]@{
display = [ordered]@{
resolution = $Resolution
fullscreen = $false
vsync = $true
renderPack = [ordered]@{
packId = 'acdream.atmospheric'
packVersion = '1.0.0'
presetId = $Preset.ToLowerInvariant()
settingOverrides = [ordered]@{}
}
}
version = 3
}
$settingsPath = Join-Path $config 'settings.json'
$settings | ConvertTo-Json -Depth 8 |
Set-Content -Encoding utf8 -LiteralPath $settingsPath
$requiredEnvironmentNames = @(
'ACDREAM_CONFIG_DIR',
'ACDREAM_DATA_DIR',
'ACDREAM_CACHE_DIR',
'ACDREAM_DAT_DIR',
'ACDREAM_RETAIL_UI',
'ACDREAM_NO_AUDIO'
)
$environmentNames = @(
@([Environment]::GetEnvironmentVariables('Process').Keys) |
ForEach-Object { [string]$_ } |
Where-Object { $_.StartsWith('ACDREAM_', [StringComparison]::OrdinalIgnoreCase) }
$requiredEnvironmentNames
) | Sort-Object -Unique
$prior = @{}
foreach ($name in $environmentNames) {
$prior[$name] = [Environment]::GetEnvironmentVariable($name, 'Process')
}
$stdoutLog = Join-Path $root 'client.stdout.log'
$stderrLog = Join-Path $root 'client.stderr.log'
$launchPath = Join-Path $root 'launch.json'
$binary = Get-Item -LiteralPath $exe
$binaryVersion = [Diagnostics.FileVersionInfo]::GetVersionInfo($exe).ProductVersion
$launch = [ordered]@{
schemaVersion = 2
status = 'prepared'
processId = $null
executable = $exe
executableSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $exe).Hash.ToLowerInvariant()
executableProductVersion = $binaryVersion
executableLastWriteUtc = $binary.LastWriteTimeUtc.ToString('O')
preset = $Preset.ToLowerInvariant()
resolution = $Resolution
audio = [ordered]@{
enabled = $audioEnabled
mode = if ($audioEnabled) { 'enabled-explicit' } else { 'disabled-default' }
}
stateRoot = Join-Path $root 'state'
settings = $settingsPath
stdoutLog = $stdoutLog
stderrLog = $stderrLog
preparedUtc = [DateTimeOffset]::UtcNow.ToString('O')
launchedUtc = $null
startupError = $null
}
$launch | ConvertTo-Json -Depth 6 |
Set-Content -Encoding utf8 -LiteralPath $launchPath
$process = $null
try {
# The preview is a clean product launch, not an extension of whichever
# connected/automation/diagnostic gate happened to run in this shell.
# Clear every inherited ACDREAM_* value transactionally, then publish only
# the explicit preview inputs below. Prior values are restored without ever
# being serialized into the artifact.
foreach ($name in $environmentNames) {
[Environment]::SetEnvironmentVariable($name, $null, 'Process')
}
$env:ACDREAM_CONFIG_DIR = $config
$env:ACDREAM_DATA_DIR = $data
$env:ACDREAM_CACHE_DIR = $cache
$env:ACDREAM_DAT_DIR = $datDirectory
$env:ACDREAM_RETAIL_UI = '1'
$env:ACDREAM_NO_AUDIO = if ($audioEnabled) { $null } else { '1' }
try {
$process = Start-Process -FilePath $exe -PassThru `
-RedirectStandardOutput $stdoutLog `
-RedirectStandardError $stderrLog
}
catch {
$launch.status = 'start-failed'
$launch.startupError = $_.Exception.Message
$launch | ConvertTo-Json -Depth 6 |
Set-Content -Encoding utf8 -LiteralPath $launchPath
throw
}
}
finally {
foreach ($name in $environmentNames) {
[Environment]::SetEnvironmentVariable($name, $prior[$name], 'Process')
}
}
$launch.status = 'started'
$launch.processId = $process.Id
$launch.launchedUtc = [DateTimeOffset]::UtcNow.ToString('O')
$launch | ConvertTo-Json -Depth 6 |
Set-Content -Encoding utf8 -LiteralPath $launchPath
Write-Host "[atmospheric-preview] launched acdream process $($process.Id)"
Write-Host "[atmospheric-preview] preset: $Preset; resolution: $Resolution"
Write-Host "[atmospheric-preview] audio: $($launch.audio.mode)"
Write-Host "[atmospheric-preview] disposable state: $root"
Write-Host "[atmospheric-preview] logs: $stdoutLog and $stderrLog"
Write-Host '[atmospheric-preview] close the acdream window normally when finished'

View file

@ -0,0 +1,853 @@
<#
.SYNOPSIS
Capture the reproducible Atmospheric render-pack performance matrix.
.DESCRIPTION
Runs the existing isolated offline pixel gate for retail, Low, Medium,
High, and Auto at 1920x1080, 2560x1440, and 3840x2160. Every row receives the same
authored-time, camera, MSAA, and warmup pins. Capped and uncapped modes are
selected explicitly with -FramePacing.
The declared CPU and resident-memory ceilings are enforced for every
enhanced row. The declared GPU ceilings are specifically 1080p ceilings,
so 1440p and 4K GPU measurements are reported without comparing them to a
threshold that was never declared for those resolutions. The exact 1080p
Low/Medium/High CPU p50/p99, GPU p50/p99, and memory ceilings are immutable
in this script and are emitted in both summaries.
No credentials or live-session environment values are read or recorded.
This wrapper never compares against or writes the user's normal settings:
run-offline-pixel-gate.ps1 owns a separate isolated state directory for
every row.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$Out,
[ValidateSet('capped', 'uncapped', 'both')]
[string]$FramePacing = 'both',
[ValidateSet('retail', 'low', 'medium', 'high', 'auto')]
[string[]]$PresetSet = @('retail', 'low', 'medium', 'high', 'auto'),
[ValidateSet('1920x1080', '2560x1440', '3840x2160')]
[string[]]$ResolutionSet = @('1920x1080', '2560x1440', '3840x2160'),
[ValidateRange(45000, 600000)]
[int]$WarmupMs = 45000,
[int]$DayGroup = 0,
[ValidateRange(0.0, 1.0)]
[double]$WorldDayFraction = 0.5,
[double]$SkyPhaseSeconds = 0,
[ValidateRange(0.0, 5000.0)]
[double]$OrbitDistanceMeters = 0,
[Nullable[double]]$OrbitYawDegrees,
[ValidateRange(-89.0, 89.0)]
[Nullable[double]]$OrbitPitchDegrees,
[switch]$SkipBuild
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $false
. (Join-Path $PSScriptRoot 'atmospheric-performance-matrix-common.ps1')
. (Join-Path $PSScriptRoot 'connected-render-pack-gate-common.ps1')
$repo = Split-Path -Parent $PSScriptRoot
$pixelGate = Join-Path $PSScriptRoot 'run-offline-pixel-gate.ps1'
$solution = Join-Path $repo 'AcDream.slnx'
$cli = Join-Path $repo 'src\AcDream.Cli\bin\Release\net10.0\AcDream.Cli.dll'
$outputRoot = [IO.Path]::GetFullPath($Out)
$jsonPath = Join-Path $outputRoot 'atmospheric-performance-matrix.json'
$markdownPath = Join-Path $outputRoot 'atmospheric-performance-matrix.md'
$presets = @($PresetSet | ForEach-Object { $_.ToLowerInvariant() } | Select-Object -Unique)
$screenshotLeaf = 'world-offline'
$resolutions = @($ResolutionSet | Select-Object -Unique)
$pacingModes = switch ($FramePacing) {
'capped' { @('capped') }
'uncapped' { @('uncapped') }
default { @('capped', 'uncapped') }
}
# These are the exact declarations in BuiltInAtmosphericRenderPack and the
# campaign plan's Performance budget table. Do not relax them in this tool.
$budgets = @{
low = [pscustomobject][ordered]@{
IncrementalCpuMillisecondsP50 = 0.15
IncrementalCpuMillisecondsP99 = 0.50
InclusiveGpuMillisecondsP50At1080p = 2.00
InclusiveGpuMillisecondsP99At1080p = 3.00
ResidentGpuBytes = 64L * 1024L * 1024L
}
medium = [pscustomobject][ordered]@{
IncrementalCpuMillisecondsP50 = 0.25
IncrementalCpuMillisecondsP99 = 0.75
InclusiveGpuMillisecondsP50At1080p = 3.25
InclusiveGpuMillisecondsP99At1080p = 4.50
ResidentGpuBytes = 128L * 1024L * 1024L
}
high = [pscustomobject][ordered]@{
IncrementalCpuMillisecondsP50 = 0.35
IncrementalCpuMillisecondsP99 = 1.00
InclusiveGpuMillisecondsP50At1080p = 4.50
InclusiveGpuMillisecondsP99At1080p = 6.00
ResidentGpuBytes = 256L * 1024L * 1024L
}
}
function Get-RequiredProperty {
param(
[Parameter(Mandatory = $true)][object]$Value,
[Parameter(Mandatory = $true)][string]$Name,
[Parameter(Mandatory = $true)][string]$Context
)
$property = $Value.PSObject.Properties[$Name]
if ($null -eq $property) {
throw "$Context is missing required property '$Name'."
}
return $property.Value
}
function Format-Invariant([double]$Value, [string]$Format = '0.###') {
return $Value.ToString($Format, [Globalization.CultureInfo]::InvariantCulture)
}
function Add-Failure(
[Collections.Generic.List[string]]$RowFailures,
[string]$Message)
{
$null = $RowFailures.Add($Message)
}
function Assert-MatrixContainedPath([string]$Root, [string]$Path) {
return Assert-ConnectedGateContainedPath $Root $Path
}
function Compare-MatrixFallbackFramebuffer {
param(
[Parameter(Mandatory = $true)][string]$Expected,
[Parameter(Mandatory = $true)][string]$Actual,
[Parameter(Mandatory = $true)][string]$ReportPath,
[Parameter(Mandatory = $true)][string]$MaskPath)
Add-Type -AssemblyName System.Drawing
$image = [System.Drawing.Bitmap]::FromFile($Actual)
try {
$mask = [System.Drawing.Bitmap]::new(
$image.Width,
$image.Height,
[System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
try {
$graphics = [System.Drawing.Graphics]::FromImage($mask)
try {
$graphics.Clear([System.Drawing.Color]::FromArgb(0, 0, 0, 0))
$opaque = [System.Drawing.SolidBrush]::new(
[System.Drawing.Color]::FromArgb(255, 255, 0, 255))
try {
$graphics.FillRectangle(
$opaque,
0,
0,
$image.Width,
[Math]::Min(280, $image.Height))
}
finally { $opaque.Dispose() }
}
finally { $graphics.Dispose() }
$mask.Save($MaskPath, [System.Drawing.Imaging.ImageFormat]::Png)
}
finally { $mask.Dispose() }
}
finally { $image.Dispose() }
& dotnet $cli compare-screenshots `
$Expected $Actual $ReportPath 2 0.001 $MaskPath | Out-Null
$compareExitCode = $LASTEXITCODE
if (-not (Test-Path -LiteralPath $ReportPath -PathType Leaf)) {
return [pscustomobject][ordered]@{
Passed = $false
DifferentPixelFraction = $null
ExitCode = $compareExitCode
ReportPath = $ReportPath
Failure = 'fallback/default screenshot comparison produced no report'
}
}
$verdict = Get-Content -Raw -LiteralPath $ReportPath | ConvertFrom-Json
$passedProperty = $verdict.PSObject.Properties['passed']
if ($null -eq $passedProperty) { $passedProperty = $verdict.PSObject.Properties['Passed'] }
$fractionProperty = $verdict.PSObject.Properties['differentPixelFraction']
if ($null -eq $fractionProperty) {
$fractionProperty = $verdict.PSObject.Properties['DifferentPixelFraction']
}
$passed = $null -ne $passedProperty -and [bool]$passedProperty.Value
return [pscustomobject][ordered]@{
Passed = $passed
DifferentPixelFraction = if ($null -eq $fractionProperty) {
$null
} else { [double]$fractionProperty.Value }
ExitCode = $compareExitCode
ReportPath = $ReportPath
Failure = if ($passed) {
$null
} else { 'safe fallback framebuffer differs from its paired default beyond tolerance 2 / 0.001' }
}
}
if (-not (Test-Path -LiteralPath $pixelGate)) {
throw "Offline pixel gate not found at '$pixelGate'."
}
if (Test-Path -LiteralPath $outputRoot) {
throw "Output directory already exists: '$outputRoot'. Choose a new directory."
}
$outputParent = Split-Path -Parent $outputRoot
if (-not (Test-Path -LiteralPath $outputParent -PathType Container)) {
throw "Output parent directory does not exist: '$outputParent'."
}
Assert-ConnectedGateNoReparsePoint $outputParent
Assert-ConnectedGateSafeLeafName ([IO.Path]::GetFileName($outputRoot))
if (@(Get-Process -Name AcDream.App -ErrorAction SilentlyContinue).Count -gt 0) {
throw 'An AcDream.App client is already running; close it gracefully before the matrix.'
}
if (-not $SkipBuild) {
& dotnet build $solution -c Release --no-restore
if ($LASTEXITCODE -ne 0) {
throw "Release build failed with exit code $LASTEXITCODE."
}
}
$exe = Join-Path $repo 'src\AcDream.App\bin\Release\net10.0\AcDream.App.exe'
if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) {
throw "Client executable not found: '$exe'."
}
$binaryIdentity = Get-ConnectedGateBinaryIdentity `
-Repository $repo -Executable $exe -SkipBuild:$SkipBuild
$null = New-Item -ItemType Directory -Path $outputRoot
Assert-ConnectedGateNoReparsePoint $outputRoot
$sourceCommit = $binaryIdentity.SourceCommit
$sourceStatus = @($binaryIdentity.SourceTrackedStatus)
$powershellExecutable = (Get-Process -Id $PID).Path
$rows = [Collections.Generic.List[object]]::new()
$matrixFailures = [Collections.Generic.List[string]]::new()
$startedUtc = [DateTime]::UtcNow
$expectedCasterCount = $null
$expectedAdapterIdentity = $null
foreach ($pacing in $pacingModes) {
foreach ($resolution in $resolutions) {
$dimensions = $resolution.Split('x')
$expectedWidth = [int]$dimensions[0]
$expectedHeight = [int]$dimensions[1]
foreach ($preset in $presets) {
$rowId = "$pacing-$preset-$($resolution.Replace('x', 'x'))"
Assert-ConnectedGateSafeLeafName $rowId
$rowDirectory = Assert-MatrixContainedPath $outputRoot (Join-Path $outputRoot $rowId)
if (Test-Path -LiteralPath $rowDirectory) {
throw "Matrix row directory is not fresh: '$rowDirectory'."
}
$metadataPath = Assert-MatrixContainedPath $rowDirectory (
Join-Path $rowDirectory "screenshots\$screenshotLeaf.metadata.json")
$rowFailures = [Collections.Generic.List[string]]::new()
$captureExitCode = -1
$metadata = $null
$pack = $null
$performance = $null
$cpuSampleCount = $null
$absoluteReceiverCpuSampleCount = $null
$gpuSampleCount = $null
$cpuP50 = $null
$cpuP95 = $null
$cpuP99 = $null
$absoluteReceiverCpuP50 = $null
$absoluteReceiverCpuP95 = $null
$absoluteReceiverCpuP99 = $null
$gpuP50 = $null
$gpuP95 = $null
$gpuP99 = $null
$residentGpuBytes = $null
$transientGpuBytes = $null
$casterCount = $null
$cascadeCount = $null
$drawCalls = $null
$dispatchCalls = $null
$imageCount = $null
$bufferCount = $null
$effectiveQuality = $null
$activationState = $null
$failureReason = $null
$availability = if ($preset -eq 'retail') { 'Retail' } else { 'Unknown' }
$unavailableClassification = $null
$metadataSchemaVersion = $null
$packVersion = $null
$activationGeneration = $null
$topResidentGpuBytes = $null
$topTransientGpuBytes = $null
$classificationCalls = $null
$passIds = @()
$framebufferSha256 = $null
$processEvidence = $null
$adapterEvidence = $null
$budget = if ($preset -in @('retail', 'auto')) {
$null
} else { $budgets[$preset] }
$gpuBudgetApplies = $false
try {
$arguments = @(
'-NoProfile',
'-File', $pixelGate,
'-Out', $rowDirectory,
'-WarmupMs', "$WarmupMs",
'-DayGroup', "$DayGroup",
'-WorldDayFraction', (Format-Invariant $WorldDayFraction '0.################'),
'-SkyPhaseSeconds', (Format-Invariant $SkyPhaseSeconds '0.################'),
'-MsaaSamples', '0',
'-RenderPackPreset', $preset,
'-Resolution', $resolution,
'-OrbitDistanceMeters', (Format-Invariant $OrbitDistanceMeters '0.################'),
'-SkipBuild')
if ($preset -ne 'retail') {
$arguments += @(
'-RequiredRenderPackSamples', '2048',
'-RenderPackSampleTimeoutMs', '300000',
'-AllowSafeRenderPackFallback')
}
if ($null -ne $OrbitYawDegrees) {
$arguments += @(
'-OrbitYawDegrees',
(Format-Invariant ([double]$OrbitYawDegrees) '0.################'))
}
if ($null -ne $OrbitPitchDegrees) {
$arguments += @(
'-OrbitPitchDegrees',
(Format-Invariant ([double]$OrbitPitchDegrees) '0.################'))
}
if ($pacing -eq 'uncapped') {
$arguments += '-Uncapped'
}
& $powershellExecutable @arguments
$captureExitCode = $LASTEXITCODE
if ($captureExitCode -ne 0) {
Add-Failure $rowFailures (
"offline capture exited with code $captureExitCode")
}
if (-not (Test-Path -LiteralPath $metadataPath)) {
throw "screenshot metadata is missing at '$metadataPath'."
}
foreach ($capturedPath in @(
$rowDirectory,
(Join-Path $rowDirectory 'screenshots'),
(Join-Path $rowDirectory 'isolated-state'),
(Join-Path $rowDirectory 'isolated-state\cache'))) {
Assert-ConnectedGateNoReparsePoint $capturedPath
}
$metadata = Get-Content -Raw -LiteralPath $metadataPath | ConvertFrom-Json
$metadataSchemaVersion = [int](Get-RequiredProperty $metadata 'SchemaVersion' 'metadata')
$strictEvidence = Test-AtmosphericPerformanceMetadataEvidence `
-MetadataPath $metadataPath -Preset $preset `
-ExpectedWidth $expectedWidth -ExpectedHeight $expectedHeight `
-AllowSafeFallback:($preset -ne 'retail')
foreach ($strictFailure in @($strictEvidence.Failures)) {
Add-Failure $rowFailures $strictFailure
}
$availability = [string]$strictEvidence.Outcome
$unavailableClassification = $strictEvidence.UnavailableClassification
$classificationCalls = $strictEvidence.CpuClassificationCalls
$passIds = @($strictEvidence.PassIds)
$actualWidth = [int](Get-RequiredProperty $metadata 'Width' 'metadata')
$actualHeight = [int](Get-RequiredProperty $metadata 'Height' 'metadata')
if ($actualWidth -ne $expectedWidth -or $actualHeight -ne $expectedHeight) {
Add-Failure $rowFailures (
"capture extent was ${actualWidth}x${actualHeight}, expected $resolution")
}
$pack = Get-RequiredProperty $metadata 'RenderPack' 'metadata'
$activationState = [int](Get-RequiredProperty $pack 'State' 'RenderPack')
$actualPackId = [string](Get-RequiredProperty $pack 'PackId' 'RenderPack')
$actualPresetId = [string](Get-RequiredProperty $pack 'PresetId' 'RenderPack')
$packVersion = [string](Get-RequiredProperty $pack 'PackVersion' 'RenderPack')
$activationGeneration = [int](Get-RequiredProperty $pack 'ActivationGeneration' 'RenderPack')
$topResidentGpuBytes = [long](Get-RequiredProperty $pack 'RetainedGpuBytes' 'RenderPack')
$topTransientGpuBytes = [long](Get-RequiredProperty $pack 'TransientGpuBytes' 'RenderPack')
$effectiveQuality = [string](
Get-RequiredProperty $pack 'EffectiveQuality' 'RenderPack')
if ($availability -eq 'Active' -and $preset -eq 'auto') {
$budget = $budgets[$effectiveQuality]
if ($null -eq $budget) {
Add-Failure $rowFailures (
"Automatic resolved unknown effective quality '$effectiveQuality'")
}
}
$gpuBudgetApplies = $availability -eq 'Active' -and
$resolution -eq '1920x1080'
$failureReason = Get-RequiredProperty $pack 'FailureReason' 'RenderPack'
$expectedPackId = if ($preset -eq 'retail' -or $availability -eq 'Unavailable') {
'retail'
}
else {
'acdream.atmospheric'
}
$expectedPresetId = if ($preset -eq 'retail' -or $availability -eq 'Unavailable') {
'off'
} else { $preset }
$expectedState = if ($preset -eq 'retail') {
0
} elseif ($availability -eq 'Unavailable') {
3
} else { 2 }
if ($actualPackId -cne $expectedPackId) {
Add-Failure $rowFailures (
"pack was '$actualPackId', expected '$expectedPackId'")
}
if ($actualPresetId -cne $expectedPresetId) {
Add-Failure $rowFailures (
"preset was '$actualPresetId', expected '$expectedPresetId'")
}
if ($activationState -ne $expectedState) {
Add-Failure $rowFailures (
"activation state was $activationState, expected $expectedState")
}
if ($availability -ne 'Unavailable' -and
-not [string]::IsNullOrWhiteSpace([string]$failureReason)) {
Add-Failure $rowFailures "render-pack failure: $failureReason"
}
$casterCount = [int](Get-RequiredProperty $pack 'ShadowCasterCount' 'RenderPack')
$cascadeCount = [int](Get-RequiredProperty $pack 'CascadeDrawCount' 'RenderPack')
$drawCalls = [int](Get-RequiredProperty $pack 'DrawCalls' 'RenderPack')
$dispatchCalls = [int](Get-RequiredProperty $pack 'DispatchCalls' 'RenderPack')
$imageCount = [int](Get-RequiredProperty $pack 'ImageCount' 'RenderPack')
$bufferCount = [int](Get-RequiredProperty $pack 'BufferCount' 'RenderPack')
$performance = Get-RequiredProperty $pack 'Performance' 'RenderPack'
# Consume only the post-audit metric names. The old absolute
# CpuMilliseconds*/GpuMilliseconds* fields must never be used
# to decide an incremental budget row.
$cpuSampleCount = [int](Get-RequiredProperty `
$performance 'CpuSampleCount' 'RenderPack.Performance')
$absoluteReceiverCpuSampleCount = [int](Get-RequiredProperty `
$performance 'AbsoluteReceiverCpuSampleCount' 'RenderPack.Performance')
$gpuSampleCount = [int](Get-RequiredProperty `
$performance 'GpuSampleCount' 'RenderPack.Performance')
$cpuP50 = [double](Get-RequiredProperty `
$performance 'IncrementalCpuMillisecondsP50' 'RenderPack.Performance')
$cpuP95 = [double](Get-RequiredProperty `
$performance 'IncrementalCpuMillisecondsP95' 'RenderPack.Performance')
$cpuP99 = [double](Get-RequiredProperty `
$performance 'IncrementalCpuMillisecondsP99' 'RenderPack.Performance')
$absoluteReceiverCpuP50 = [double](Get-RequiredProperty `
$performance 'AbsoluteReceiverCpuMillisecondsP50' 'RenderPack.Performance')
$absoluteReceiverCpuP95 = [double](Get-RequiredProperty `
$performance 'AbsoluteReceiverCpuMillisecondsP95' 'RenderPack.Performance')
$absoluteReceiverCpuP99 = [double](Get-RequiredProperty `
$performance 'AbsoluteReceiverCpuMillisecondsP99' 'RenderPack.Performance')
$gpuP50 = [double](Get-RequiredProperty `
$performance 'InclusiveGpuMillisecondsP50' 'RenderPack.Performance')
$gpuP95 = [double](Get-RequiredProperty `
$performance 'InclusiveGpuMillisecondsP95' 'RenderPack.Performance')
$gpuP99 = [double](Get-RequiredProperty `
$performance 'InclusiveGpuMillisecondsP99' 'RenderPack.Performance')
$residentGpuBytes = [long](Get-RequiredProperty `
$performance 'ResidentGpuBytes' 'RenderPack.Performance')
$transientGpuBytes = [long](Get-RequiredProperty `
$performance 'TransientGpuBytes' 'RenderPack.Performance')
if ($availability -eq 'Active') {
if ($null -eq $expectedCasterCount) { $expectedCasterCount = $casterCount }
elseif ($casterCount -ne $expectedCasterCount) {
Add-Failure $rowFailures (
"shadow caster membership $casterCount differs from matrix oracle $expectedCasterCount")
}
}
$pngPath = Assert-MatrixContainedPath $rowDirectory (
Join-Path $rowDirectory "screenshots\$screenshotLeaf.png")
$framebufferSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $pngPath).Hash.ToLowerInvariant()
$processPath = Assert-MatrixContainedPath $rowDirectory (
Join-Path $rowDirectory 'capture-process.json')
$processEvidence = Get-Content -Raw -LiteralPath $processPath | ConvertFrom-Json
if ([int]$processEvidence.SchemaVersion -ne 1 -or
[long]$processEvidence.WorkingSetBytes -le 0 -or
[long]$processEvidence.PrivateMemoryBytes -le 0) {
Add-Failure $rowFailures 'process-memory evidence is missing or invalid'
}
$capabilityPath = Assert-MatrixContainedPath $rowDirectory (
Join-Path $rowDirectory 'isolated-state\cache\diagnostics\graphical-capabilities-vulkan.json')
$capability = Get-Content -Raw -LiteralPath $capabilityPath | ConvertFrom-Json
$adapterEvidence = [pscustomobject][ordered]@{
DeviceName = [string]$capability.DeviceName
DriverInfo = [string]$capability.DriverInfo
DeviceType = [string]$capability.DeviceType
SelectedDeviceIndex = [int]$capability.SelectedDeviceIndex
DeviceApiVersion = [string]$capability.DeviceApiVersion
}
$adapterIdentity = "$($adapterEvidence.DeviceName)|$($adapterEvidence.DriverInfo)|$($adapterEvidence.SelectedDeviceIndex)"
if ([string]::IsNullOrWhiteSpace($adapterEvidence.DeviceName)) {
Add-Failure $rowFailures 'Vulkan adapter identity is missing'
}
elseif ($null -eq $expectedAdapterIdentity) { $expectedAdapterIdentity = $adapterIdentity }
elseif ($adapterIdentity -cne $expectedAdapterIdentity) {
Add-Failure $rowFailures 'Vulkan adapter identity changed between matrix rows'
}
if ($preset -eq 'retail') {
if ($imageCount -ne 0 -or $bufferCount -ne 0 -or
$drawCalls -ne 0 -or $dispatchCalls -ne 0 -or
$casterCount -ne 0 -or $cascadeCount -ne 0 -or
$residentGpuBytes -ne 0 -or $transientGpuBytes -ne 0) {
Add-Failure $rowFailures (
'retail row activated pack resources, submissions, casters, or memory')
}
}
elseif ($availability -eq 'Unavailable') {
# The strict metadata oracle already proved exact retail
# identity, zero samples/work/resources, and an allow-listed
# resource/capability reason. No enhanced budget applies to
# a preset which never became active.
}
else {
if ($cpuSampleCount -le 0 -or $gpuSampleCount -le 0) {
Add-Failure $rowFailures (
"enhanced row has insufficient samples: cpu=$cpuSampleCount gpu=$gpuSampleCount")
}
if (-not [double]::IsFinite($cpuP50) -or $cpuP50 -lt 0 -or
-not [double]::IsFinite($cpuP99) -or $cpuP99 -lt 0 -or
-not [double]::IsFinite($gpuP50) -or $gpuP50 -lt 0 -or
-not [double]::IsFinite($gpuP99) -or $gpuP99 -lt 0 -or
$residentGpuBytes -lt 0) {
Add-Failure $rowFailures 'performance measurements are non-finite or negative'
}
if ($cpuP50 -gt $budget.IncrementalCpuMillisecondsP50) {
Add-Failure $rowFailures (
"incremental CPU p50 $(Format-Invariant $cpuP50) ms exceeds " +
"$(Format-Invariant $budget.IncrementalCpuMillisecondsP50) ms")
}
if ($cpuP99 -gt $budget.IncrementalCpuMillisecondsP99) {
Add-Failure $rowFailures (
"incremental CPU p99 $(Format-Invariant $cpuP99) ms exceeds " +
"$(Format-Invariant $budget.IncrementalCpuMillisecondsP99) ms")
}
if ($residentGpuBytes -gt $budget.ResidentGpuBytes) {
Add-Failure $rowFailures (
"resident GPU bytes $residentGpuBytes exceed $($budget.ResidentGpuBytes)")
}
if ($gpuBudgetApplies -and
$gpuP50 -gt $budget.InclusiveGpuMillisecondsP50At1080p) {
Add-Failure $rowFailures (
"inclusive GPU p50 $(Format-Invariant $gpuP50) ms exceeds the 1080p " +
"ceiling $(Format-Invariant $budget.InclusiveGpuMillisecondsP50At1080p) ms")
}
if ($gpuBudgetApplies -and
$gpuP99 -gt $budget.InclusiveGpuMillisecondsP99At1080p) {
Add-Failure $rowFailures (
"inclusive GPU p99 $(Format-Invariant $gpuP99) ms exceeds the 1080p " +
"ceiling $(Format-Invariant $budget.InclusiveGpuMillisecondsP99At1080p) ms")
}
}
}
catch {
Add-Failure $rowFailures $_.Exception.Message
}
$passed = $rowFailures.Count -eq 0
$row = [pscustomobject][ordered]@{
Id = $rowId
FramePacing = $pacing
Preset = $preset
Resolution = $resolution
CaptureDirectory = $rowDirectory
MetadataPath = $metadataPath
CaptureExitCode = $captureExitCode
Passed = $passed
Failures = @($rowFailures)
Activation = [pscustomobject][ordered]@{
MetadataSchemaVersion = $metadataSchemaVersion
PackVersion = $packVersion
State = $activationState
Generation = $activationGeneration
EffectiveQuality = $effectiveQuality
FailureReason = $failureReason
Availability = $availability
UnavailableClassification = $unavailableClassification
}
FramebufferSha256 = $framebufferSha256
PairedDefaultFramebufferSha256 = $null
PairedDefaultComparison = $null
Process = $processEvidence
Adapter = $adapterEvidence
Samples = [pscustomobject][ordered]@{
IncrementalCpu = $cpuSampleCount
AbsoluteReceiverCpu = $absoluteReceiverCpuSampleCount
InclusiveGpu = $gpuSampleCount
}
PerformanceMilliseconds = [pscustomobject][ordered]@{
IncrementalCpuP50 = $cpuP50
IncrementalCpuP95 = $cpuP95
IncrementalCpuP99 = $cpuP99
AbsoluteReceiverCpuP50 = $absoluteReceiverCpuP50
AbsoluteReceiverCpuP95 = $absoluteReceiverCpuP95
AbsoluteReceiverCpuP99 = $absoluteReceiverCpuP99
InclusiveGpuP50 = $gpuP50
InclusiveGpuP95 = $gpuP95
InclusiveGpuP99 = $gpuP99
}
Resources = [pscustomobject][ordered]@{
TopLevelResidentGpuBytes = $topResidentGpuBytes
TopLevelTransientGpuBytes = $topTransientGpuBytes
ResidentGpuBytes = $residentGpuBytes
TransientGpuBytes = $transientGpuBytes
Images = $imageCount
Buffers = $bufferCount
}
Work = [pscustomobject][ordered]@{
ShadowCasters = $casterCount
CascadeDraws = $cascadeCount
DrawCalls = $drawCalls
DispatchCalls = $dispatchCalls
CpuClassificationCalls = $classificationCalls
PassIds = @($passIds)
}
Budget = if ($null -eq $budget) {
$null
}
else {
[pscustomobject][ordered]@{
IncrementalCpuMillisecondsP50 =
$budget.IncrementalCpuMillisecondsP50
IncrementalCpuMillisecondsP99 =
$budget.IncrementalCpuMillisecondsP99
InclusiveGpuMillisecondsP50At1080p =
$budget.InclusiveGpuMillisecondsP50At1080p
InclusiveGpuMillisecondsP99At1080p =
$budget.InclusiveGpuMillisecondsP99At1080p
ResidentGpuBytes = $budget.ResidentGpuBytes
GpuBudgetApplies = $gpuBudgetApplies
}
}
}
$null = $rows.Add($row)
foreach ($failure in $rowFailures) {
$null = $matrixFailures.Add("${rowId}: $failure")
}
}
}
}
foreach ($row in @($rows | Where-Object { $_.Preset -ne 'retail' })) {
$defaultRow = @($rows | Where-Object {
$_.Preset -eq 'retail' -and
$_.FramePacing -eq $row.FramePacing -and
$_.Resolution -eq $row.Resolution
})
if ($defaultRow.Count -ne 1 -or
[string]::IsNullOrWhiteSpace([string]$defaultRow[0].FramebufferSha256)) {
$message = "$($row.Id): paired default framebuffer digest is unavailable"
$null = $matrixFailures.Add($message)
$row.Passed = $false
$row.Failures = @($row.Failures) + $message
}
else {
$row.PairedDefaultFramebufferSha256 = $defaultRow[0].FramebufferSha256
if ($row.Activation.Availability -eq 'Unavailable') {
$expectedPng = Assert-MatrixContainedPath $defaultRow[0].CaptureDirectory (
Join-Path $defaultRow[0].CaptureDirectory "screenshots\$screenshotLeaf.png")
$actualPng = Assert-MatrixContainedPath $row.CaptureDirectory (
Join-Path $row.CaptureDirectory "screenshots\$screenshotLeaf.png")
$comparisonReport = Assert-MatrixContainedPath $row.CaptureDirectory (
Join-Path $row.CaptureDirectory 'compare-paired-default.json')
$comparisonMask = Assert-MatrixContainedPath $row.CaptureDirectory (
Join-Path $row.CaptureDirectory 'compare-paired-default-mask.png')
try {
$row.PairedDefaultComparison = Compare-MatrixFallbackFramebuffer `
-Expected $expectedPng `
-Actual $actualPng `
-ReportPath $comparisonReport `
-MaskPath $comparisonMask
if (-not $row.PairedDefaultComparison.Passed) {
$message = "$($row.Id): $($row.PairedDefaultComparison.Failure)"
$null = $matrixFailures.Add($message)
$row.Passed = $false
$row.Failures = @($row.Failures) + $message
}
}
catch {
$message = "$($row.Id): fallback/default screenshot comparison failed: $($_.Exception.Message)"
$null = $matrixFailures.Add($message)
$row.Passed = $false
$row.Failures = @($row.Failures) + $message
}
}
}
}
$finishedUtc = [DateTime]::UtcNow
$report = [pscustomobject][ordered]@{
SchemaVersion = 1
Scope = 'offline fixed-scene pack diagnostics; final receiver CPU authority requires connected identical pack-off/on A/B'
Passed = $matrixFailures.Count -eq 0
StartedUtc = $startedUtc.ToString('O')
FinishedUtc = $finishedUtc.ToString('O')
SourceCommit = $sourceCommit
TrackedSourceStatus = @($sourceStatus)
BinaryProductVersion = $binaryIdentity.BinaryProductVersion
BinaryCommit = $binaryIdentity.BinaryCommit
BinaryMatchesSource = $binaryIdentity.BinaryMatchesSource
SkipBuild = $binaryIdentity.SkipBuild
FramePacing = $FramePacing
Pins = [pscustomobject][ordered]@{
WarmupMs = $WarmupMs
ExplicitPresetPerformanceWindowResetAfterWarmup = $true
AutomaticPerformanceWindowPolicy =
'settled rolling window; diagnostic reset prohibited'
RequiredEnhancedSamplesPerMetric = 2048
RenderPackSampleTimeoutMs = 300000
DayGroup = $DayGroup
WorldDayFraction = $WorldDayFraction
SkyPhaseSeconds = $SkyPhaseSeconds
MsaaSamples = 0
OrbitDistanceMeters = $OrbitDistanceMeters
OrbitYawDegrees = $OrbitYawDegrees
OrbitPitchDegrees = $OrbitPitchDegrees
Presets = $presets
Resolutions = $resolutions
}
Summary = [pscustomobject][ordered]@{
TotalRows = $rows.Count
PassedRows = @($rows | Where-Object Passed).Count
ActiveEnhancedRows = @($rows | Where-Object {
$_.Activation.Availability -eq 'Active' -and $_.Passed
}).Count
ResourceUnavailableRows = @($rows | Where-Object {
$_.Activation.UnavailableClassification -eq 'ResourceUnavailable' -and $_.Passed
}).Count
CapabilityUnavailableRows = @($rows | Where-Object {
$_.Activation.UnavailableClassification -eq 'CapabilityUnavailable' -and $_.Passed
}).Count
FailedRows = @($rows | Where-Object { -not $_.Passed }).Count
}
EvidenceOracle = [pscustomobject][ordered]@{
RequiredSamplesPerMetric = 2048
EqualEnhancedShadowCasterCount = $expectedCasterCount
CascadeDraws = [pscustomobject][ordered]@{ Low = 2; Medium = 3; High = 4 }
WarmedCpuClassificationCalls = 0
VulkanAdapterIdentity = $expectedAdapterIdentity
}
DeclaredBudgets = [pscustomobject][ordered]@{
Low = $budgets.low
Medium = $budgets.medium
High = $budgets.high
}
Rows = @($rows)
Failures = @($matrixFailures)
}
$report | ConvertTo-Json -Depth 12 |
Set-Content -Encoding utf8 -LiteralPath $jsonPath
$markdown = [Text.StringBuilder]::new()
$null = $markdown.AppendLine('# Atmospheric Rendering Performance Matrix')
$null = $markdown.AppendLine()
$null = $markdown.AppendLine('Scope: offline fixed-scene pack diagnostics. Final receiver CPU authority requires connected identical pack-off/on A/B.')
$null = $markdown.AppendLine()
$null = $markdown.AppendLine("- Result: **$(if ($report.Passed) { 'PASS' } else { 'FAIL' })**")
$null = $markdown.AppendLine("- Source commit: ``$sourceCommit``")
$null = $markdown.AppendLine("- Binary commit: ``$($binaryIdentity.BinaryCommit)`` (source match: $($binaryIdentity.BinaryMatchesSource))")
$null = $markdown.AppendLine("- Vulkan adapter: ``$expectedAdapterIdentity``")
$null = $markdown.AppendLine("- Frame pacing: ``$FramePacing``")
$null = $markdown.AppendLine(
"- Pins: warmup ${WarmupMs} ms; day group $DayGroup; world day fraction " +
"$(Format-Invariant $WorldDayFraction '0.################'); sky phase " +
"$(Format-Invariant $SkyPhaseSeconds '0.################') s; MSAA 0; orbit " +
"$(Format-Invariant $OrbitDistanceMeters) m")
$null = $markdown.AppendLine(
"- Evidence window: explicit presets reset diagnostics after warmup; Auto preserves " +
"its hysteresis-owned rolling window. Every active row waits for a complete 2048-sample " +
"CPU / receiver / GPU window (300000 ms timeout).")
$null = $markdown.AppendLine()
$null = $markdown.AppendLine('## Declared ceilings')
$null = $markdown.AppendLine()
$null = $markdown.AppendLine('| Preset | CPU p50 / p99 (ms) | GPU p50 / p99 at 1080p (ms) | Resident MiB |')
$null = $markdown.AppendLine('|---|---:|---:|---:|')
foreach ($preset in @('low', 'medium', 'high')) {
$budget = $budgets[$preset]
$null = $markdown.AppendLine(
"| $preset | $(Format-Invariant $budget.IncrementalCpuMillisecondsP50) / " +
"$(Format-Invariant $budget.IncrementalCpuMillisecondsP99) | " +
"$(Format-Invariant $budget.InclusiveGpuMillisecondsP50At1080p) / " +
"$(Format-Invariant $budget.InclusiveGpuMillisecondsP99At1080p) | " +
"$([Math]::Round($budget.ResidentGpuBytes / 1MB, 0)) |")
}
$null = $markdown.AppendLine()
$null = $markdown.AppendLine('## Rows')
$null = $markdown.AppendLine()
$null = $markdown.AppendLine('| Pacing | Preset | Resolution | Availability | CPU / receiver / GPU samples | CPU p50 / p95 / p99 ms | Receiver p50 / p95 / p99 ms | GPU p50 / p95 / p99 ms | Resident MiB | Process WS / private MiB | Casters | Cascades | Draw / dispatch | Framebuffer / paired-default SHA-256 | Result |')
$null = $markdown.AppendLine('|---|---|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---|---|')
foreach ($row in $rows) {
$cpu = if ($null -eq $row.PerformanceMilliseconds.IncrementalCpuP50) {
'n/a'
}
else {
"$(Format-Invariant $row.PerformanceMilliseconds.IncrementalCpuP50) / " +
"$(Format-Invariant $row.PerformanceMilliseconds.IncrementalCpuP95) / " +
"$(Format-Invariant $row.PerformanceMilliseconds.IncrementalCpuP99)"
}
$gpu = if ($null -eq $row.PerformanceMilliseconds.InclusiveGpuP50) {
'n/a'
}
else {
"$(Format-Invariant $row.PerformanceMilliseconds.InclusiveGpuP50) / " +
"$(Format-Invariant $row.PerformanceMilliseconds.InclusiveGpuP95) / " +
"$(Format-Invariant $row.PerformanceMilliseconds.InclusiveGpuP99)"
}
$receiver = if ($null -eq $row.PerformanceMilliseconds.AbsoluteReceiverCpuP50) {
'n/a'
}
else {
"$(Format-Invariant $row.PerformanceMilliseconds.AbsoluteReceiverCpuP50) / " +
"$(Format-Invariant $row.PerformanceMilliseconds.AbsoluteReceiverCpuP95) / " +
"$(Format-Invariant $row.PerformanceMilliseconds.AbsoluteReceiverCpuP99)"
}
$resident = if ($null -eq $row.Resources.ResidentGpuBytes) {
'n/a'
}
else {
Format-Invariant ($row.Resources.ResidentGpuBytes / 1MB)
}
$processMemory = if ($null -eq $row.Process) { 'n/a' } else {
"$(Format-Invariant ($row.Process.WorkingSetBytes / 1MB)) / " +
"$(Format-Invariant ($row.Process.PrivateMemoryBytes / 1MB))"
}
$digest = if ($null -eq $row.FramebufferSha256) { 'n/a' } else { $row.FramebufferSha256 }
$pairedDigest = if ($null -eq $row.PairedDefaultFramebufferSha256) { 'n/a' } else { $row.PairedDefaultFramebufferSha256 }
$result = if ($row.Passed -and $row.Activation.Availability -eq 'Unavailable') {
'UNAVAILABLE: ' + $row.Activation.UnavailableClassification + ' - ' +
(([string]$row.Activation.FailureReason) -replace '\|', '/')
}
elseif ($row.Passed) {
'PASS'
}
else {
'FAIL: ' + ((@($row.Failures) -join '; ') -replace '\|', '/')
}
$null = $markdown.AppendLine(
"| $($row.FramePacing) | $($row.Preset) | $($row.Resolution) | $($row.Activation.Availability) | " +
"$($row.Samples.IncrementalCpu) / $($row.Samples.AbsoluteReceiverCpu) / $($row.Samples.InclusiveGpu) | " +
"$cpu | $receiver | $gpu | $resident | $processMemory | $($row.Work.ShadowCasters) | " +
"$($row.Work.CascadeDraws) | $($row.Work.DrawCalls) / $($row.Work.DispatchCalls) | " +
"$digest / $pairedDigest | $result |")
}
if ($matrixFailures.Count -ne 0) {
$null = $markdown.AppendLine()
$null = $markdown.AppendLine('## Failures')
$null = $markdown.AppendLine()
foreach ($failure in $matrixFailures) {
$null = $markdown.AppendLine("- $failure")
}
}
$markdown.ToString() | Set-Content -Encoding utf8 -LiteralPath $markdownPath
Write-Output "JSON=$jsonPath"
Write-Output "MARKDOWN=$markdownPath"
Write-Output "RESULT=$(if ($report.Passed) { 'PASS' } else { 'FAIL' })"
if (-not $report.Passed) {
exit 1
}

View file

@ -9,11 +9,15 @@ param(
[switch]$CaptureContention,
[switch]$SkipRuntimeCounters,
[int]$LoginTimeoutSeconds = 90,
[int]$CollisionShadowEvery = 0
[int]$CollisionShadowEvery = 0,
[ValidateSet('retail', 'low', 'medium', 'high', 'auto')]
[string]$RenderPackPreset = 'retail',
[hashtable]$RenderPackSettingOverrides = @{}
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'connected-render-pack-gate-common.ps1')
if ([string]::IsNullOrWhiteSpace($Account)) { $Account = 'testaccount' }
if ([string]::IsNullOrWhiteSpace($Password)) { $Password = 'testpassword' }
@ -722,35 +726,19 @@ if (-not $SkipBuild) {
if (-not (Test-Path -LiteralPath $exe)) { throw "client executable not found: $exe" }
if (-not (Test-Path -LiteralPath $cliDll)) { throw "CLI assembly not found: $cliDll" }
$sourceCommit = (& git -C $Repository rev-parse HEAD).Trim()
# -SkipBuild is intentionally supported, so the checked-out source commit is
# not necessarily the binary being measured. Read the SDK-stamped
# AssemblyInformationalVersion from the executable and make the BINARY commit
# the baseline identity. Otherwise a docs-only commit after a build can
# silently mislabel every performance artifact.
$binaryProductVersion = [Diagnostics.FileVersionInfo]::GetVersionInfo($exe).ProductVersion
$binaryCommitMatch = [regex]::Match(
[string]$binaryProductVersion,
'\+([0-9a-fA-F]{40})(?:\.|$)')
$binaryCommit = if ($binaryCommitMatch.Success) {
$binaryCommitMatch.Groups[1].Value.ToLowerInvariant()
}
else {
$null
}
$commit = if ($null -ne $binaryCommit) { $binaryCommit } else { $sourceCommit }
$binaryMatchesSource = $null -ne $binaryCommit -and $binaryCommit -eq $sourceCommit
# Generated logs and artifacts may be untracked beside a clean source tree.
# The reproducibility contract is about tracked source modifications.
$sourceStatus = @(& git -C $Repository status --short --untracked-files=no)
if ($null -eq $binaryCommit) {
$failures.Add(
"client binary ProductVersion '$binaryProductVersion' does not identify a 40-character source commit")
}
elseif (-not $binaryMatchesSource) {
$warnings.Add(
"measured binary commit $binaryCommit differs from checked-out source commit $sourceCommit")
}
$renderPackGate = New-ConnectedRenderPackGateState `
-Root $artifactDir `
-Preset $RenderPackPreset `
-SettingOverrides $RenderPackSettingOverrides
try {
$binaryIdentity = Get-ConnectedGateBinaryIdentity `
-Repository $Repository -Executable $exe -SkipBuild:$SkipBuild
$sourceCommit = $binaryIdentity.SourceCommit
$binaryProductVersion = $binaryIdentity.BinaryProductVersion
$binaryCommit = $binaryIdentity.BinaryCommit
$binaryMatchesSource = $binaryIdentity.BinaryMatchesSource
$sourceStatus = @($binaryIdentity.SourceTrackedStatus)
$commit = $binaryCommit
$videoControllers = @(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue | ForEach-Object {
[pscustomobject]@{ Name = $_.Name; DriverVersion = $_.DriverVersion; AdapterRam = $_.AdapterRAM }
})
@ -789,7 +777,7 @@ $null = New-Item -ItemType Directory -Force -Path $artifactDir
$acdreamEnvVars = Get-ChildItem Env: | Where-Object { $_.Name -like 'ACDREAM_*' } |
Sort-Object Name |
ForEach-Object {
$sensitive = $_.Name -match '(?i)(PASS|PASSWORD|TOKEN|SECRET|KEY)'
$sensitive = $_.Name -match '(?i)(PASS|PASSWORD|TOKEN|SECRET|KEY|USER|ACCOUNT)'
[pscustomobject]@{
Name = $_.Name
Value = if ($sensitive) { '<redacted>' } else { $_.Value }
@ -809,6 +797,7 @@ $envDisclosure = [pscustomobject][ordered]@{
RuntimeCounters = -not [bool]$SkipRuntimeCounters
CollisionShadowEvery = $CollisionShadowEvery
Route = $routeFileName
RenderPackSelection = (Get-ConnectedRenderPackGateReport $renderPackGate)
EnvironmentVariables = @($acdreamEnvVars)
}
$envDisclosure | ConvertTo-Json -Depth 4 |
@ -924,6 +913,12 @@ try {
30
$canonicalCheckpoints = @(Read-CanonicalCheckpoints)
Add-CanonicalCheckpointFailures $process
Add-ConnectedRenderPackMetadataFailures `
-ArtifactDirectory $artifactDir `
-ScreenshotNames $expectedCheckpointNames `
-State $renderPackGate `
-Failures $failures `
-Label $runName
if ($CollisionShadowEvery -gt 0 -and $canonicalCheckpoints.Count -gt 0) {
$lastShadow =
$canonicalCheckpoints[-1].resources.PSObject.Properties['collisionShadow']
@ -1027,6 +1022,7 @@ finally {
BinaryMatchesSource = $binaryMatchesSource
SessionName = $env:SESSIONNAME
CollisionShadowEvery = $CollisionShadowEvery
RenderPackSelection = (Get-ConnectedRenderPackGateReport $renderPackGate)
ExitCode = $exitCode
GracefulExit = $gracefulExit
Failures = @($failures)
@ -1057,5 +1053,9 @@ finally {
foreach ($failure in $failures) { Write-Output "FAILURE=$failure" }
foreach ($warning in $warnings) { Write-Output "WARNING=$warning" }
}
}
finally {
Restore-ConnectedRenderPackGateEnvironment $renderPackGate
}
if ($failures.Count -gt 0) { exit 1 }

View file

@ -6,11 +6,15 @@ param(
[string]$AceLogPath = 'C:\ACE\Server\ACE_Log.txt',
[switch]$SkipBuild,
[int]$SessionTimeoutSeconds = 420,
[int]$CollisionShadowEvery = 0
[int]$CollisionShadowEvery = 0,
[ValidateSet('retail', 'low', 'medium', 'high', 'auto')]
[string]$RenderPackPreset = 'retail',
[hashtable]$RenderPackSettingOverrides = @{}
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'connected-render-pack-gate-common.ps1')
if ([string]::IsNullOrWhiteSpace($Account)) { $Account = 'testaccount' }
if ([string]::IsNullOrWhiteSpace($Password)) { $Password = 'testpassword' }
@ -231,7 +235,8 @@ function Invoke-Session(
[string]$RoutePath,
[bool]$Uncapped,
[string[]]$ExpectedCheckpoints,
[string[]]$ExpectedScreenshots)
[string[]]$ExpectedScreenshots,
[hashtable]$ScreenshotStateOverrides = @{})
{
$sessionDir = Join-Path $root $Label
$artifactDir = Join-Path $sessionDir 'artifacts'
@ -294,6 +299,8 @@ function Invoke-Session(
$client.Refresh()
$processSample = [pscustomobject][ordered]@{
ProcessId = $client.Id
StartTimeUtc = $client.StartTime.ToUniversalTime().ToString('O')
WorkingSetMiB = [Math]::Round($client.WorkingSet64 / 1MB, 1)
PrivateMiB = [Math]::Round($client.PrivateMemorySize64 / 1MB, 1)
HandleCount = $client.HandleCount
@ -335,6 +342,18 @@ function Invoke-Session(
$png = Join-Path $artifactDir "screenshots\$name.png"
if (-not (Test-Png $png)) { $failures.Add("${Label}: missing or invalid screenshot '$png'") }
}
foreach ($name in $ExpectedScreenshots) {
$expectedState = if ($ScreenshotStateOverrides.ContainsKey($name)) {
$ScreenshotStateOverrides[$name]
}
else { $renderPackGate }
Add-ConnectedRenderPackMetadataFailures `
-ArtifactDirectory $artifactDir `
-ScreenshotNames @($name) `
-State $expectedState `
-Failures $failures `
-Label $Label
}
$graceful = Close-ClientGracefully $client
$client.Refresh()
@ -388,6 +407,128 @@ function Invoke-Session(
}
}
function Add-AtmosphericTransitionSemanticGates([object]$Session) {
if ($null -eq $Session) { return }
$screenshots = Join-Path $Session.ArtifactDirectory 'screenshots'
$rows = @{}
foreach ($name in @(
'transition_selected_high',
'transition_disabled_retail',
'transition_reenabled_high',
'transition_resized_high',
'transition_dusk_high',
'transition_overcast_high',
'transition_rain_high')) {
$path = Join-Path $screenshots "$name.metadata.json"
if (Test-Path -LiteralPath $path) {
$rows[$name] = Get-Content -Raw -LiteralPath $path | ConvertFrom-Json
}
}
if ($rows.Count -ne 7) { return }
$selected = $rows.transition_selected_high.RenderPack
$disabled = $rows.transition_disabled_retail.RenderPack
$reenabled = $rows.transition_reenabled_high.RenderPack
if ([long]$disabled.ActivationGeneration -le [long]$selected.ActivationGeneration -or
[long]$reenabled.ActivationGeneration -le [long]$disabled.ActivationGeneration) {
$failures.Add('atmospheric-transitions: select/disable/re-enable activation generations were not strictly monotonic')
}
if ([int]$selected.ShadowCasterCount -le 0) {
$failures.Add('atmospheric-transitions: dense outdoor High row published no directional-shadow casters')
}
if ([int]$selected.ShadowTransformChurn.LiveDynamicRootChanges -le 0) {
$failures.Add('atmospheric-transitions: moving High row published no live-dynamic caster transform')
}
if ([int]$selected.ShadowTransformChurn.EquippedChildChanges -le 0) {
$failures.Add('atmospheric-transitions: moving High row published no equipped-child caster transform')
}
$casterClasses = $selected.ShadowTransformChurn.CasterClasses
if ($null -eq $casterClasses) {
$failures.Add('atmospheric-transitions: High row published no authoritative caster-class diagnostics')
}
else {
foreach ($property in @(
'TerrainCommands',
'OutdoorStatics',
'Buildings',
'AnimatedStatics',
'LocalPlayers',
'RemotePlayers',
'NonPlayerCreatures',
'OtherLiveDynamics',
'EquippedChildren')) {
if ($property -notin @($casterClasses.PSObject.Properties.Name)) {
$failures.Add(
"atmospheric-transitions: caster diagnostics omitted required '$property' metadata")
}
elseif ([int]$casterClasses.$property -lt 0) {
$failures.Add(
"atmospheric-transitions: caster diagnostics published negative '$property' metadata")
}
}
foreach ($required in @(
@('TerrainCommands', 'terrain command'),
@('OutdoorStatics', 'outdoor-static scenery'),
@('Buildings', 'building'),
@('LocalPlayers', 'local-player'),
@('EquippedChildren', 'equipped-child'))) {
$property = [string]$required[0]
if ([int]$casterClasses.$property -le 0) {
$failures.Add(
"atmospheric-transitions: dense outdoor High row published no $($required[1]) caster evidence")
}
}
}
$resized = $rows.transition_resized_high
if ([int]$resized.Width -ne 1024 -or [int]$resized.Height -ne 768) {
$failures.Add(
"atmospheric-transitions: resized screenshot was $($resized.Width)x$($resized.Height), expected exact 1024x768 framebuffer")
}
$dusk = $rows.transition_dusk_high.RenderPack
if ([Math]::Abs(
[double]$dusk.SunElevationDegrees -
[double]$reenabled.SunElevationDegrees) -lt 0.01) {
$failures.Add('atmospheric-transitions: authored time change did not alter published sun elevation')
}
if ([string]$rows.transition_overcast_high.RenderPack.Weather -cnotmatch '(?i)^overcast$') {
$failures.Add('atmospheric-transitions: first weather edge did not publish Overcast')
}
if ([string]$rows.transition_rain_high.RenderPack.Weather -cnotmatch '(?i)^rain$') {
$failures.Add('atmospheric-transitions: second weather edge did not publish Rain')
}
}
function Get-FreshContextRecreationGate(
[object]$FirstSession,
[object]$SecondSession)
{
$definition = 'graceful full graphical-process teardown followed by a fresh process; the fresh process constructs a new Vulkan device/context/swapchain ownership graph'
if ($null -eq $FirstSession -or $null -eq $SecondSession) {
$failures.Add('new-context recreation could not be proven because a required session is missing')
return [pscustomobject][ordered]@{
Definition = $definition
Passed = $false
FirstProcess = $null
SecondProcess = $null
}
}
$firstIdentity = "$($FirstSession.Process.ProcessId)@$($FirstSession.Process.StartTimeUtc)"
$secondIdentity = "$($SecondSession.Process.ProcessId)@$($SecondSession.Process.StartTimeUtc)"
$passed = $FirstSession.GracefulExit -and
$SecondSession.GracefulExit -and
$firstIdentity -cne $secondIdentity
if (-not $passed) {
$failures.Add('new-context recreation did not prove graceful teardown and a distinct fresh process')
}
return [pscustomobject][ordered]@{
Definition = $definition
Passed = $passed
FirstProcess = $firstIdentity
SecondProcess = $secondIdentity
}
}
function Add-SameLocationGates([object]$CappedSession) {
if ($null -eq $CappedSession) { return }
$first = @($CappedSession.Checkpoints | Where-Object { $_.name -eq 'aerlinthe_first' }) | Select-Object -First 1
@ -413,64 +554,136 @@ function Add-SameLocationGates([object]$CappedSession) {
}
}
if (@(Get-Process -Name AcDream.App -ErrorAction SilentlyContinue).Count -gt 0) {
throw 'an AcDream.App client is already running; close it gracefully before the gate'
$renderPackGate = New-ConnectedRenderPackGateState `
-Root $root `
-Preset $RenderPackPreset `
-SettingOverrides $RenderPackSettingOverrides
try {
if (@(Get-Process -Name AcDream.App -ErrorAction SilentlyContinue).Count -gt 0) {
throw 'an AcDream.App client is already running; close it gracefully before the gate'
}
if (@(Get-NetUDPEndpoint -LocalPort 9000 -ErrorAction SilentlyContinue).Count -eq 0) {
throw 'local ACE is not listening on UDP port 9000'
}
if (-not (Test-Path -LiteralPath $AceLogPath)) {
throw "ACE log was not found: $AceLogPath"
}
if (-not $SkipBuild) {
& dotnet build (Join-Path $Repository 'AcDream.slnx') -c Release --no-restore
if ($LASTEXITCODE -ne 0) { throw "Release build failed with exit code $LASTEXITCODE" }
}
if (-not (Test-Path -LiteralPath $exe)) { throw "client executable not found: $exe" }
$binaryIdentity = Get-ConnectedGateBinaryIdentity `
-Repository $Repository -Executable $exe -SkipBuild:$SkipBuild
$capped = Invoke-Session `
'capped' `
(Join-Path $Repository 'tools\connected-world-lifecycle.route.txt') `
$false `
@('capped_login', 'aerlinthe_first', 'rynthid', 'facility_hub', 'holtburg_after_dungeon', 'aerlinthe_revisit') `
@('capped_login', 'aerlinthe_first', 'facility_hub', 'holtburg_after_dungeon', 'aerlinthe_revisit')
Add-SameLocationGates $capped
# The second process starts as soon as ACE records accepting the first
# process's transport Disconnect. No elapsed-time settle delay hides a
# shutdown race.
$uncapped = Invoke-Session `
'uncapped-reconnect' `
(Join-Path $Repository 'tools\connected-world-reconnect.route.txt') `
$true `
@('uncapped_reconnect') `
@('uncapped_reconnect')
$contextRecreation = Get-FreshContextRecreationGate $capped $uncapped
# The medium matrix row is the one canonical transition row. It starts
# from a known enhanced selection, then proves an in-process High select,
# retail disable, exact High re-enable, live resize, and authored
# sun/weather changes without multiplying this long route across all five
# preset rows.
$transitionSession = $null
if ($RenderPackPreset -eq 'medium') {
$highExpectation = Get-ConnectedRenderPackExpectation -Preset high
$retailExpectation = Get-ConnectedRenderPackExpectation -Preset retail
$transitionSession = Invoke-Session `
'atmospheric-transitions' `
(Join-Path $Repository 'tools\connected-render-pack-transitions.route.txt') `
$false `
@('atmospheric_transitions') `
@(
'transition_selected_high',
'transition_disabled_retail',
'transition_reenabled_high',
'transition_resized_high',
'transition_dusk_high',
'transition_overcast_high',
'transition_rain_high') `
@{
transition_selected_high = $highExpectation
transition_disabled_retail = $retailExpectation
transition_reenabled_high = $highExpectation
transition_resized_high = $highExpectation
transition_dusk_high = $highExpectation
transition_overcast_high = $highExpectation
transition_rain_high = $highExpectation
}
Add-AtmosphericTransitionSemanticGates $transitionSession
}
$report = [pscustomobject][ordered]@{
Passed = $failures.Count -eq 0
StartedUtc = $startedUtc.ToString('O')
FinishedUtc = [DateTime]::UtcNow.ToString('O')
Commit = $binaryIdentity.BinaryCommit
SourceCommit = $binaryIdentity.SourceCommit
BinaryProductVersion = $binaryIdentity.BinaryProductVersion
BinaryCommit = $binaryIdentity.BinaryCommit
BinaryMatchesSource = $binaryIdentity.BinaryMatchesSource
SkipBuild = $binaryIdentity.SkipBuild
SourceStatus = @(& git -C $Repository status --short)
SessionName = $env:SESSIONNAME
CollisionShadowEvery = $CollisionShadowEvery
RenderPackSelection = (Get-ConnectedRenderPackGateReport $renderPackGate)
ContextRecreation = $contextRecreation
TransitionAutomation = [pscustomobject][ordered]@{
Executed = $null -ne $transitionSession
CanonicalRow = 'medium'
Session = $transitionSession
ProvenCasterRoutes = @(
'terrain shadow-command publication',
'outdoor-static scenery publication (including trees, without a tree discriminator)',
'building caster publication',
'local-player caster publication',
'moving live-dynamic root transform publication',
'equipped-child caster and moving-transform publication')
RemainingCasterClassEvidence = @(
'a second live client is still required to prove a nonzero remote-player caster count',
'a deterministic populated connected row is still required to prove nonzero active animated-static and non-player creature counts',
'create-object render metadata proves non-player creature, not hostile monster versus non-hostile NPC',
'outdoor DAT scenery has no authoritative tree discriminator, so trees remain grouped with other outdoor statics')
}
VideoControllers = @(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue |
ForEach-Object { [pscustomobject]@{
Name = $_.Name
DriverVersion = $_.DriverVersion
AdapterRam = $_.AdapterRAM
} })
Failures = @($failures)
Warnings = @($warnings)
Sessions = @($sessions)
}
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $reportPath -Encoding utf8
Write-Output "REPORT=$reportPath"
Write-Output "RESULT=$(if ($report.Passed) { 'PASS' } else { 'FAIL' })"
foreach ($failure in $failures) { Write-Output "FAILURE=$failure" }
foreach ($warning in $warnings) { Write-Output "WARNING=$warning" }
}
if (@(Get-NetUDPEndpoint -LocalPort 9000 -ErrorAction SilentlyContinue).Count -eq 0) {
throw 'local ACE is not listening on UDP port 9000'
finally {
Restore-ConnectedRenderPackGateEnvironment $renderPackGate
}
if (-not (Test-Path -LiteralPath $AceLogPath)) {
throw "ACE log was not found: $AceLogPath"
}
if (-not $SkipBuild) {
& dotnet build (Join-Path $Repository 'AcDream.slnx') -c Release --no-restore
if ($LASTEXITCODE -ne 0) { throw "Release build failed with exit code $LASTEXITCODE" }
}
if (-not (Test-Path -LiteralPath $exe)) { throw "client executable not found: $exe" }
$capped = Invoke-Session `
'capped' `
(Join-Path $Repository 'tools\connected-world-lifecycle.route.txt') `
$false `
@('capped_login', 'aerlinthe_first', 'rynthid', 'facility_hub', 'holtburg_after_dungeon', 'aerlinthe_revisit') `
@('capped_login', 'aerlinthe_first', 'facility_hub', 'holtburg_after_dungeon', 'aerlinthe_revisit')
Add-SameLocationGates $capped
# The second process starts as soon as ACE records accepting the first
# process's transport Disconnect. No elapsed-time settle delay hides a
# shutdown race.
$null = Invoke-Session `
'uncapped-reconnect' `
(Join-Path $Repository 'tools\connected-world-reconnect.route.txt') `
$true `
@('uncapped_reconnect') `
@('uncapped_reconnect')
$report = [pscustomobject][ordered]@{
Passed = $failures.Count -eq 0
StartedUtc = $startedUtc.ToString('O')
FinishedUtc = [DateTime]::UtcNow.ToString('O')
Commit = (& git -C $Repository rev-parse HEAD).Trim()
SourceStatus = @(& git -C $Repository status --short)
SessionName = $env:SESSIONNAME
CollisionShadowEvery = $CollisionShadowEvery
VideoControllers = @(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue |
ForEach-Object { [pscustomobject]@{
Name = $_.Name
DriverVersion = $_.DriverVersion
AdapterRam = $_.AdapterRAM
} })
Failures = @($failures)
Warnings = @($warnings)
Sessions = @($sessions)
}
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $reportPath -Encoding utf8
Write-Output "REPORT=$reportPath"
Write-Output "RESULT=$(if ($report.Passed) { 'PASS' } else { 'FAIL' })"
foreach ($failure in $failures) { Write-Output "FAILURE=$failure" }
foreach ($warning in $warnings) { Write-Output "WARNING=$warning" }
if ($failures.Count -gt 0) { exit 1 }

View file

@ -100,6 +100,23 @@
so a known, registered band (for instance AD-46's treeline) can be quantified
separately from the rest of the frame.
.PARAMETER OrbitDistanceMeters
Diagnostic initial orbit-camera distance in metres. Zero keeps acdream's
normal default.
.PARAMETER OrbitYawDegrees
Diagnostic initial orbit-camera heading in degrees. Omit to keep acdream's
normal default.
.PARAMETER OrbitPitchDegrees
Diagnostic initial orbit-camera elevation in degrees. Omit to keep
acdream's normal default. A shallow positive angle can put a low sun in
frame for ray and volumetric-shaft captures.
.PARAMETER Uncapped
Disable both VSync and the normal refresh-rate software limiter. Omit for
the capped product cadence. This is a diagnostic measurement mode only.
.PARAMETER SkipBuild
Skip the Release build (use when the caller already built).
@ -122,6 +139,22 @@ param(
[int]$Tolerance = 2,
[double]$MaxDifferentFraction = 0.001,
[int]$MaskTopPixels = 280,
[ValidateSet('retail', 'low', 'medium', 'high', 'auto')]
[string]$RenderPackPreset = 'retail',
[ValidatePattern('^[1-9][0-9]*x[1-9][0-9]*$')]
[string]$Resolution = '1280x720',
[ValidateRange(0.0, 5000.0)]
[double]$OrbitDistanceMeters = 0,
[Nullable[double]]$OrbitYawDegrees,
[ValidateRange(-89.0, 89.0)]
[Nullable[double]]$OrbitPitchDegrees,
[hashtable]$RenderPackSettingOverrides = @{},
[ValidateRange(0, 2048)]
[int]$RequiredRenderPackSamples = 0,
[ValidateRange(1000, 600000)]
[int]$RenderPackSampleTimeoutMs = 300000,
[switch]$AllowSafeRenderPackFallback,
[switch]$Uncapped,
[switch]$SkipBuild
)
@ -129,6 +162,11 @@ $ErrorActionPreference = 'Stop'
$repo = Split-Path -Parent $PSScriptRoot
$exe = Join-Path $repo 'src\AcDream.App\bin\Release\net10.0\AcDream.App.exe'
$cli = Join-Path $repo 'src\AcDream.Cli\bin\Release\net10.0\AcDream.Cli.dll'
. (Join-Path $PSScriptRoot 'atmospheric-performance-matrix-common.ps1')
if ($AllowSafeRenderPackFallback -and $RenderPackPreset -eq 'retail') {
throw '-AllowSafeRenderPackFallback is valid only for an explicitly selected enhanced preset.'
}
function Write-Step($message) { Write-Host "[pixel-gate] $message" }
@ -144,27 +182,114 @@ if (-not (Test-Path $exe)) { throw "Client not found at $exe. Build Release firs
if (Test-Path $Out) { Remove-Item -Recurse -Force $Out }
New-Item -ItemType Directory -Force -Path $Out | Out-Null
# Every capture owns a complete disposable path set. This prevents an offline
# gate from inheriting or rewriting the user's real pack selection, settings,
# plugins, screenshots, or pipeline cache.
$state = Join-Path $Out 'isolated-state'
$config = Join-Path $state 'config'
$data = Join-Path $state 'data'
$cache = Join-Path $state 'cache'
New-Item -ItemType Directory -Force -Path $config, $data, $cache | Out-Null
$packId = if ($RenderPackPreset -eq 'retail') { 'retail' } else { 'acdream.atmospheric' }
$packVersion = if ($RenderPackPreset -eq 'retail') { $null } else { '1.0.0' }
$presetId = if ($RenderPackPreset -eq 'retail') { 'off' } else { $RenderPackPreset }
$orderedOverrides = [ordered]@{}
foreach ($key in @($RenderPackSettingOverrides.Keys | Sort-Object)) {
$orderedOverrides[$key] = [string]$RenderPackSettingOverrides[$key]
}
$settings = [ordered]@{
display = [ordered]@{
resolution = $Resolution
fullscreen = $false
vsync = $false
renderPack = [ordered]@{
packId = $packId
packVersion = $packVersion
presetId = $presetId
settingOverrides = $orderedOverrides
}
}
version = 3
}
$settings | ConvertTo-Json -Depth 8 | Set-Content -Encoding utf8 `
-LiteralPath (Join-Path $config 'settings.json')
$probe = Join-Path $Out 'offline.probe.txt'
# The script runner reads one command per line. A single settled capture is the
# whole gate: a second stop would need camera movement, which offline has no
# deterministic way to drive.
Set-Content -Encoding utf8 -Path $probe -Value @"
sleep $WarmupMs
screenshot world-offline 30000
sleep 500
"@
$probeCommands = [Collections.Generic.List[string]]::new()
$probeCommands.Add("sleep $WarmupMs")
if ($RequiredRenderPackSamples -gt 0) {
# Explicit presets discard startup evidence after warmup. Auto deliberately
# owns a continuous rolling window for its hysteresis policy and rejects a
# diagnostic reset; after the same warmup we wait until its current stable
# resource generation contains one complete window.
if ($RenderPackPreset -ne 'auto') {
$probeCommands.Add('renderpack reset-performance')
}
$probeCommands.Add(
"wait render-pack-samples $RequiredRenderPackSamples $RenderPackSampleTimeoutMs")
if ($AllowSafeRenderPackFallback) {
# The screenshot controller reads the last completed swapchain image.
# A runtime Auto fallback can publish retail at the boundary that
# satisfies the wait while that completed image still belongs to the
# prior enhanced frame. Give the default path time to present fresh
# frames before pairing its pixels with the retail oracle.
$probeCommands.Add('sleep 2000')
}
}
$probeCommands.Add('screenshot world-offline 30000')
# Keep the process alive briefly after the PNG commit so this parent can record
# the matching process envelope, then ask the hidden client itself to execute
# the ordinary IWindow.Close shutdown path. Hidden windows intentionally have
# no MainWindowHandle, so WM_CLOSE cannot be the primary close mechanism.
$probeCommands.Add('sleep 4000')
$probeCommands.Add('close-client')
Set-Content -Encoding utf8 -Path $probe -Value $probeCommands
$log = Join-Path $Out 'client.log'
# --- 3. Launch offline --------------------------------------------------------
$previousLive = $env:ACDREAM_LIVE
$previousConfigDirectory = $env:ACDREAM_CONFIG_DIR
$previousDataDirectory = $env:ACDREAM_DATA_DIR
$previousCacheDirectory = $env:ACDREAM_CACHE_DIR
$previousOrbitDistance = $env:ACDREAM_ORBIT_DISTANCE_METERS
$previousOrbitYaw = $env:ACDREAM_ORBIT_YAW_DEGREES
$previousOrbitPitch = $env:ACDREAM_ORBIT_PITCH_DEGREES
$previousUncappedRender = $env:ACDREAM_UNCAPPED_RENDER
$previousExactFramebuffer = $env:ACDREAM_AUTOMATION_EXACT_FRAMEBUFFER
Remove-Item Env:\ACDREAM_LIVE -ErrorAction SilentlyContinue
$env:ACDREAM_DAT_DIR = Join-Path $env:USERPROFILE "Documents\Asheron's Call"
$env:ACDREAM_CONFIG_DIR = $config
$env:ACDREAM_DATA_DIR = $data
$env:ACDREAM_CACHE_DIR = $cache
$env:ACDREAM_NO_AUDIO = '1'
$env:ACDREAM_RETAIL_UI = '1'
$env:ACDREAM_DAY_GROUP = "$DayGroup"
$env:ACDREAM_UI_PROBE_SCRIPT = $probe
$env:ACDREAM_AUTOMATION_ARTIFACT_DIR = $Out
$env:ACDREAM_AUTOMATION_EXACT_FRAMEBUFFER = '1'
$env:ACDREAM_UNCAPPED_RENDER = if ($Uncapped) { '1' } else { $null }
if ($OrbitDistanceMeters -gt 0) {
$env:ACDREAM_ORBIT_DISTANCE_METERS = $OrbitDistanceMeters.ToString(
[System.Globalization.CultureInfo]::InvariantCulture)
} else {
Remove-Item Env:\ACDREAM_ORBIT_DISTANCE_METERS -ErrorAction SilentlyContinue
}
if ($null -ne $OrbitYawDegrees) {
$env:ACDREAM_ORBIT_YAW_DEGREES = ([double]$OrbitYawDegrees).ToString(
[System.Globalization.CultureInfo]::InvariantCulture)
} else {
Remove-Item Env:\ACDREAM_ORBIT_YAW_DEGREES -ErrorAction SilentlyContinue
}
if ($null -ne $OrbitPitchDegrees) {
$env:ACDREAM_ORBIT_PITCH_DEGREES = ([double]$OrbitPitchDegrees).ToString(
[System.Globalization.CultureInfo]::InvariantCulture)
} else {
Remove-Item Env:\ACDREAM_ORBIT_PITCH_DEGREES -ErrorAction SilentlyContinue
}
# The determinism pins, forced rather than inherited. See .DESCRIPTION.
$invariant = [System.Globalization.CultureInfo]::InvariantCulture
@ -173,13 +298,17 @@ $env:ACDREAM_SKY_PHASE_SECONDS = $SkyPhaseSeconds.ToString($invariant)
if ($MsaaSamples -ge 0) { $env:ACDREAM_MSAA_SAMPLES = "$MsaaSamples" }
else { Remove-Item Env:\ACDREAM_MSAA_SAMPLES -ErrorAction SilentlyContinue }
Write-Step "launching offline client (warmup ${WarmupMs}ms, day group $DayGroup, day fraction $WorldDayFraction, sky phase $SkyPhaseSeconds, MSAA $MsaaSamples)"
Write-Step "launching offline client (pack $packId/$presetId, $Resolution, $(if ($Uncapped) { 'uncapped' } else { 'capped' }), orbit ${OrbitDistanceMeters}m/$OrbitYawDegrees deg yaw/$OrbitPitchDegrees deg pitch, warmup ${WarmupMs}ms, day group $DayGroup, day fraction $WorldDayFraction, sky phase $SkyPhaseSeconds, MSAA $MsaaSamples)"
$proc = Start-Process -FilePath $exe -RedirectStandardOutput $log `
-RedirectStandardError "$log.err" -PassThru -WindowStyle Minimized
-RedirectStandardError "$log.err" -PassThru -WindowStyle Hidden
try {
$shots = Join-Path $Out 'screenshots'
$deadline = (Get-Date).AddMilliseconds($WarmupMs + 60000)
$sampleWaitMs = if ($RequiredRenderPackSamples -gt 0) {
$RenderPackSampleTimeoutMs
} else { 0 }
$deadline = (Get-Date).AddMilliseconds(
$WarmupMs + $sampleWaitMs + 60000)
$captured = $false
while ((Get-Date) -lt $deadline) {
if ((Test-Path $shots) -and (Get-ChildItem $shots -Filter *.png -ErrorAction SilentlyContinue)) {
@ -196,27 +325,111 @@ try {
}
# Let the probe script finish its trailing sleep so the PNG is fully flushed.
Start-Sleep -Milliseconds 1500
$proc.Refresh()
[pscustomobject][ordered]@{
SchemaVersion = 1
CapturedUtc = [DateTime]::UtcNow.ToString('O')
WorkingSetBytes = [long]$proc.WorkingSet64
PrivateMemoryBytes = [long]$proc.PrivateMemorySize64
HandleCount = [int]$proc.HandleCount
ThreadCount = [int]$proc.Threads.Count
} | ConvertTo-Json -Depth 3 | Set-Content -Encoding utf8 `
-LiteralPath (Join-Path $Out 'capture-process.json')
}
finally {
# Graceful close: WM_CLOSE runs the shutdown path, so the ownership ledger
# converges the way the lifecycle tests expect. No ACE session exists here,
# but keeping the habit means this script is safe to point at a live run too.
$app = Get-Process -Name AcDream.App -ErrorAction SilentlyContinue
if ($app) {
$app.CloseMainWindow() | Out-Null
if (-not $app.WaitForExit(10000)) {
Write-Step 'WM_CLOSE timed out; forcing'
$app | Stop-Process -Force
}
# The probe's close-client verb runs acdream's normal IWindow.Close path.
# Wait for that exact process; never target another AcDream.App instance.
# Force is cleanup-only and makes the gate fail rather than disguising a
# broken ownership/shutdown path as a successful capture.
$shutdownFailure = $null
$proc.Refresh()
if (-not $proc.HasExited -and -not $proc.WaitForExit(15000)) {
$shutdownFailure = 'in-process automation close timed out'
Write-Step "$shutdownFailure; forcing exact capture process"
Stop-Process -Id $proc.Id -Force
$proc.WaitForExit()
}
if ($null -eq $shutdownFailure -and $proc.ExitCode -ne 0) {
$shutdownFailure = "client exited with code $($proc.ExitCode)"
}
Remove-Item Env:\ACDREAM_MSAA_SAMPLES -ErrorAction SilentlyContinue
Remove-Item Env:\ACDREAM_WORLD_TIME -ErrorAction SilentlyContinue
Remove-Item Env:\ACDREAM_SKY_PHASE_SECONDS -ErrorAction SilentlyContinue
if ($null -eq $previousOrbitDistance) {
Remove-Item Env:\ACDREAM_ORBIT_DISTANCE_METERS -ErrorAction SilentlyContinue
} else { $env:ACDREAM_ORBIT_DISTANCE_METERS = $previousOrbitDistance }
if ($null -eq $previousOrbitYaw) {
Remove-Item Env:\ACDREAM_ORBIT_YAW_DEGREES -ErrorAction SilentlyContinue
} else { $env:ACDREAM_ORBIT_YAW_DEGREES = $previousOrbitYaw }
if ($null -eq $previousOrbitPitch) {
Remove-Item Env:\ACDREAM_ORBIT_PITCH_DEGREES -ErrorAction SilentlyContinue
} else { $env:ACDREAM_ORBIT_PITCH_DEGREES = $previousOrbitPitch }
if ($null -eq $previousUncappedRender) {
Remove-Item Env:\ACDREAM_UNCAPPED_RENDER -ErrorAction SilentlyContinue
} else { $env:ACDREAM_UNCAPPED_RENDER = $previousUncappedRender }
if ($null -eq $previousExactFramebuffer) {
Remove-Item Env:\ACDREAM_AUTOMATION_EXACT_FRAMEBUFFER -ErrorAction SilentlyContinue
} else {
$env:ACDREAM_AUTOMATION_EXACT_FRAMEBUFFER = $previousExactFramebuffer
}
if ($previousLive) { $env:ACDREAM_LIVE = $previousLive }
if ($null -eq $previousConfigDirectory) {
Remove-Item Env:\ACDREAM_CONFIG_DIR -ErrorAction SilentlyContinue
} else { $env:ACDREAM_CONFIG_DIR = $previousConfigDirectory }
if ($null -eq $previousDataDirectory) {
Remove-Item Env:\ACDREAM_DATA_DIR -ErrorAction SilentlyContinue
} else { $env:ACDREAM_DATA_DIR = $previousDataDirectory }
if ($null -eq $previousCacheDirectory) {
Remove-Item Env:\ACDREAM_CACHE_DIR -ErrorAction SilentlyContinue
} else { $env:ACDREAM_CACHE_DIR = $previousCacheDirectory }
if ($null -ne $shutdownFailure) {
throw $shutdownFailure
}
}
$captures = Get-ChildItem (Join-Path $Out 'screenshots') -Filter *.png
$captures = @(Get-ChildItem (Join-Path $Out 'screenshots') -Filter *.png)
Write-Step "captured $($captures.Count) screenshot(s) into $Out"
$metadataPath = Join-Path $Out 'screenshots\world-offline.metadata.json'
if (-not (Test-Path -LiteralPath $metadataPath)) {
throw "Render-pack screenshot metadata is missing at '$metadataPath'."
}
$metadata = Get-Content -Raw -LiteralPath $metadataPath | ConvertFrom-Json
if ($AllowSafeRenderPackFallback) {
$dimensions = $Resolution.Split('x')
$evidence = Test-AtmosphericPerformanceMetadataEvidence `
-MetadataPath $metadataPath `
-Preset $RenderPackPreset `
-ExpectedWidth ([int]$dimensions[0]) `
-ExpectedHeight ([int]$dimensions[1]) `
-AllowSafeFallback
if (-not $evidence.Passed) {
throw ('Capture is neither an active complete evidence window nor a safe ' +
'resource/capability fallback: ' + (@($evidence.Failures) -join '; '))
}
Write-Step ("render-pack capture outcome: {0}{1}" -f `
$evidence.Outcome,
$(if ($evidence.Outcome -eq 'Unavailable') {
" ($($evidence.UnavailableClassification): $($evidence.FailureReason))"
} else { '' }))
}
else {
if (($metadata.RenderPack.PackId -ne $packId) -or
($metadata.RenderPack.PresetId -ne $presetId)) {
throw ("Capture selected {0}/{1}, expected {2}/{3}. Failure: {4}" -f `
$metadata.RenderPack.PackId,
$metadata.RenderPack.PresetId,
$packId,
$presetId,
$metadata.RenderPack.FailureReason)
}
$expectedState = if ($RenderPackPreset -eq 'retail') { 0 } else { 2 }
if ([int]$metadata.RenderPack.State -ne $expectedState) {
throw ("Capture render-pack state was {0}, expected {1}. Failure: {2}" -f `
$metadata.RenderPack.State,
$expectedState,
$metadata.RenderPack.FailureReason)
}
}
# The offline window is minimised but still focusable, so a stray scroll or key
# press from whoever is at the keyboard can move the camera mid-capture. That