feat(render): implement Campaign AR and terrain fidelity

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

View file

@ -1,6 +1,9 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using System.Text.Json;
using AcDream.App.Rendering.Packs;
namespace AcDream.App.Diagnostics;
/// <summary>
@ -22,6 +25,7 @@ internal sealed class FrameScreenshotController
private readonly Func<int, int, byte[]> _readRgba;
private readonly string _directory;
private readonly Action<string> _log;
private readonly Func<RenderPackDiagnosticsSnapshot>? _renderPackMetadata;
private readonly Queue<string> _pending = new();
private readonly Dictionary<string, CaptureStatus> _status =
new(StringComparer.OrdinalIgnoreCase);
@ -29,13 +33,15 @@ internal sealed class FrameScreenshotController
internal FrameScreenshotController(
Func<int, int, byte[]> readRgba,
string directory,
Action<string>? log = null)
Action<string>? log = null,
Func<RenderPackDiagnosticsSnapshot>? renderPackMetadata = null)
{
_readRgba = readRgba ?? throw new ArgumentNullException(nameof(readRgba));
_directory = string.IsNullOrWhiteSpace(directory)
? throw new ArgumentException("A screenshot directory is required.", nameof(directory))
: Path.GetFullPath(directory);
_log = log ?? (_ => { });
_renderPackMetadata = renderPackMetadata;
}
public bool TryRequest(string name, out string error)
@ -85,6 +91,27 @@ internal sealed class FrameScreenshotController
string temporaryPath = path + ".tmp";
using (Image<Rgba32> image = Image.LoadPixelData<Rgba32>(flipped, width, height))
image.SaveAsPng(temporaryPath);
string? metadataPath = null;
string? temporaryMetadataPath = null;
if (_renderPackMetadata is not null)
{
metadataPath = Path.Combine(_directory, name + ".metadata.json");
temporaryMetadataPath = metadataPath + ".tmp";
var metadata = new FrameScreenshotMetadata(
SchemaVersion: 1,
Width: width,
Height: height,
RenderPack: _renderPackMetadata());
File.WriteAllBytes(
temporaryMetadataPath,
JsonSerializer.SerializeToUtf8Bytes(
metadata,
new JsonSerializerOptions { WriteIndented = true }));
}
if (metadataPath is not null && temporaryMetadataPath is not null)
File.Move(temporaryMetadataPath, metadataPath, overwrite: true);
File.Move(temporaryPath, path, overwrite: true);
_status[name] = new CaptureStatus(CaptureState.Complete);
@ -93,6 +120,9 @@ internal sealed class FrameScreenshotController
}
catch (Exception exception)
{
TryDelete(Path.Combine(_directory, name + ".png.tmp"));
TryDelete(Path.Combine(_directory, name + ".metadata.json.tmp"));
TryDelete(Path.Combine(_directory, name + ".metadata.json"));
string message = $"screenshot '{name}' failed: {exception.Message}";
_status[name] = new CaptureStatus(CaptureState.Failed, message);
_log($"[world-gate] screenshot-failed name={name} error={exception.Message}");
@ -100,6 +130,22 @@ internal sealed class FrameScreenshotController
}
}
private static void TryDelete(string path)
{
try
{
File.Delete(path);
}
catch (Exception error) when (error is IOException
or UnauthorizedAccessException
or ArgumentException
or NotSupportedException)
{
// Preserve the primary capture error. The next artifact directory
// teardown reports any file that could not be cleaned.
}
}
internal static byte[] FlipRows(byte[] pixels, int width, int height)
{
int stride = checked(width * 4);
@ -249,3 +295,9 @@ internal sealed class FrameScreenshotController
}
}
internal sealed record FrameScreenshotMetadata(
int SchemaVersion,
int Width,
int Height,
RenderPackDiagnosticsSnapshot RenderPack);

View file

@ -240,6 +240,22 @@ internal sealed class WorldLifecycleAutomationController :
private readonly Func<RuntimeWorldTransitOwnershipSnapshot>
_getTransitOwnership;
private readonly Func<int> _getPortalMaterializationCount;
private readonly Func<int> _getRenderPackPerformanceSampleCount;
private readonly Func<bool> _getRenderPackFailedToRetail;
private readonly Func<RetailUiAutomationRenderPackStatus>
_getRenderPackStatus;
private readonly Func<string, (bool Succeeded, string Error)>
_selectRenderPack;
private readonly Func<(bool Succeeded, string Error)>?
_disableRenderPack;
private readonly Func<(bool Succeeded, string Error)>?
_reenableRenderPack;
private readonly Func<(int Width, int Height)> _getFramebufferSize;
private readonly Func<int, int, (bool Succeeded, string Error)>
_resizeFramebuffer;
private readonly Func<(bool Succeeded, string Error)>
_resetRenderPackPerformance;
private readonly Action? _requestClientClose;
private readonly Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot>
_captureResources;
private readonly FrameScreenshotController _screenshots;
@ -248,6 +264,7 @@ internal sealed class WorldLifecycleAutomationController :
private readonly object _requestOwner = new();
private readonly object _sync = new();
private readonly Queue<WorldLifecycleCheckpointRequest> _requests = [];
private string? _lastEnabledRenderPackPreset;
private int _sequence;
private bool _disposed;
@ -260,7 +277,17 @@ internal sealed class WorldLifecycleAutomationController :
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> captureResources,
FrameScreenshotController screenshots,
string artifactDirectory,
Action<string>? log = null)
Action<string>? log = null,
Func<int>? getRenderPackPerformanceSampleCount = null,
Func<(bool Succeeded, string Error)>? resetRenderPackPerformance = null,
Func<bool>? getRenderPackFailedToRetail = null,
Func<RetailUiAutomationRenderPackStatus>? getRenderPackStatus = null,
Func<string, (bool Succeeded, string Error)>? selectRenderPack = null,
Func<(bool Succeeded, string Error)>? disableRenderPack = null,
Func<(bool Succeeded, string Error)>? reenableRenderPack = null,
Func<(int Width, int Height)>? getFramebufferSize = null,
Func<int, int, (bool Succeeded, string Error)>? resizeFramebuffer = null,
Action? requestClientClose = null)
{
_getReveal = getReveal ?? throw new ArgumentNullException(nameof(getReveal));
_getEnvironmentOwnership = getEnvironmentOwnership
@ -270,6 +297,21 @@ internal sealed class WorldLifecycleAutomationController :
?? throw new ArgumentNullException(nameof(getTransitOwnership));
_getPortalMaterializationCount = getPortalMaterializationCount
?? throw new ArgumentNullException(nameof(getPortalMaterializationCount));
_getRenderPackPerformanceSampleCount =
getRenderPackPerformanceSampleCount ?? (() => 0);
_getRenderPackFailedToRetail = getRenderPackFailedToRetail ?? (() => false);
_getRenderPackStatus = getRenderPackStatus
?? (() => RetailUiAutomationRenderPackStatus.Retail);
_selectRenderPack = selectRenderPack
?? (_ => (false, "render-pack selection automation is unavailable"));
_disableRenderPack = disableRenderPack;
_reenableRenderPack = reenableRenderPack;
_getFramebufferSize = getFramebufferSize ?? (() => (0, 0));
_resizeFramebuffer = resizeFramebuffer
?? ((_, _) => (false, "framebuffer resize automation is unavailable"));
_resetRenderPackPerformance = resetRenderPackPerformance
?? (() => (false, "render-pack performance automation is unavailable"));
_requestClientClose = requestClientClose;
_captureResources = captureResources ?? throw new ArgumentNullException(nameof(captureResources));
_screenshots = screenshots ?? throw new ArgumentNullException(nameof(screenshots));
_artifactDirectory = string.IsNullOrWhiteSpace(artifactDirectory)
@ -281,6 +323,113 @@ internal sealed class WorldLifecycleAutomationController :
public bool IsWorldReady => _getReveal().IsReady;
public bool IsWorldViewportVisible => _getReveal().WorldViewportObserved;
public int PortalMaterializationCount => _getPortalMaterializationCount();
public int RenderPackPerformanceSampleCount =>
_getRenderPackPerformanceSampleCount();
public bool RenderPackFailedToRetail => _getRenderPackFailedToRetail();
public RetailUiAutomationRenderPackStatus RenderPackStatus =>
_getRenderPackStatus();
public int FramebufferWidth => _getFramebufferSize().Width;
public int FramebufferHeight => _getFramebufferSize().Height;
public bool TrySelectRenderPack(string presetId, out string error)
{
ArgumentException.ThrowIfNullOrWhiteSpace(presetId);
string normalized = presetId.ToLowerInvariant();
if (normalized == "off")
normalized = "retail";
if (normalized is not ("retail" or "low" or "medium" or "high" or "auto"))
{
error = $"unknown render-pack preset '{presetId}'";
return false;
}
(bool succeeded, string selectionError) = _selectRenderPack(normalized);
if (succeeded && normalized != "retail")
_lastEnabledRenderPackPreset = normalized;
error = selectionError;
return succeeded;
}
public bool TryDisableRenderPack(out string error)
{
if (_disableRenderPack is not null)
{
(bool succeeded, string disableError) = _disableRenderPack();
error = disableError;
return succeeded;
}
RetailUiAutomationRenderPackStatus current = RenderPackStatus;
if (current.State == RetailUiAutomationRenderPackState.Active
&& !string.Equals(current.PackId, "retail", StringComparison.OrdinalIgnoreCase))
{
_lastEnabledRenderPackPreset = current.PresetId;
}
return TrySelectRenderPack("retail", out error);
}
public bool TryReenableRenderPack(out string error)
{
if (_reenableRenderPack is not null)
{
(bool succeeded, string reenableError) = _reenableRenderPack();
error = reenableError;
return succeeded;
}
if (string.IsNullOrWhiteSpace(_lastEnabledRenderPackPreset))
{
error = "render-pack re-enable requires a prior active enhanced selection";
return false;
}
return TrySelectRenderPack(_lastEnabledRenderPackPreset, out error);
}
public bool TryResizeFramebuffer(int width, int height, out string error)
{
if (width < 320 || height < 240 || width > 8192 || height > 8192)
{
error = "automation framebuffer size must be within 320x240 and 8192x8192";
return false;
}
(bool succeeded, string resizeError) = _resizeFramebuffer(width, height);
error = resizeError;
return succeeded;
}
public bool TryResetRenderPackPerformance(out string error)
{
// A terminal fallback owns no enhanced evidence. Treat reset as an
// idempotent no-op so a reset/wait/screenshot automation sequence can
// report the unavailable preset instead of stopping before capture.
if (RenderPackFailedToRetail)
{
error = string.Empty;
return true;
}
(bool succeeded, string resetError) = _resetRenderPackPerformance();
error = resetError;
return succeeded;
}
public bool TryRequestClientClose(out string error)
{
if (_requestClientClose is null)
{
error = "client-close automation is unavailable";
return false;
}
try
{
_requestClientClose();
error = string.Empty;
return true;
}
catch (Exception exception)
{
error = $"client-close automation failed: {exception.Message}";
return false;
}
}
public bool TryRequestCheckpoint(
string name,