feat(render): implement Campaign AR and terrain fidelity
This commit is contained in:
parent
99cf26e00c
commit
7a5f96ede5
368 changed files with 50611 additions and 950 deletions
|
|
@ -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>
|
||||
141
tools/RenderPackValidator/PackManifest.cs
Normal file
141
tools/RenderPackValidator/PackManifest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
7
tools/RenderPackValidator/Program.cs
Normal file
7
tools/RenderPackValidator/Program.cs
Normal 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);
|
||||
}
|
||||
1500
tools/RenderPackValidator/RenderPackSdkValidator.cs
Normal file
1500
tools/RenderPackValidator/RenderPackSdkValidator.cs
Normal file
File diff suppressed because it is too large
Load diff
235
tools/RenderPackValidator/RenderPackValidatorCommand.cs
Normal file
235
tools/RenderPackValidator/RenderPackValidatorCommand.cs
Normal 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);
|
||||
10
tools/RenderPackValidator/packages.neutral.lock.json
Normal file
10
tools/RenderPackValidator/packages.neutral.lock.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"acdream.plugin.abstractions": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
tools/ShaderCompiler/GlslIncludeExpander.cs
Normal file
51
tools/ShaderCompiler/GlslIncludeExpander.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
48
tools/ShaderCompiler/packages.win-x64.lock.json
Normal file
48
tools/ShaderCompiler/packages.win-x64.lock.json
Normal 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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
325
tools/atmospheric-performance-matrix-common.ps1
Normal file
325
tools/atmospheric-performance-matrix-common.ps1
Normal 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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
51
tools/connected-atmospheric-exposure-comparison.route.txt
Normal file
51
tools/connected-atmospheric-exposure-comparison.route.txt
Normal 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
|
||||
341
tools/connected-render-pack-gate-common.ps1
Normal file
341
tools/connected-render-pack-gate-common.ps1
Normal 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
|
||||
}
|
||||
}
|
||||
59
tools/connected-render-pack-transitions.route.txt
Normal file
59
tools/connected-render-pack-transitions.route.txt
Normal 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
|
||||
196
tools/launch-atmospheric-preview.ps1
Normal file
196
tools/launch-atmospheric-preview.ps1
Normal 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'
|
||||
853
tools/run-atmospheric-performance-matrix.ps1
Normal file
853
tools/run-atmospheric-performance-matrix.ps1
Normal 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
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue