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

@ -35,6 +35,43 @@ public sealed class GameWindow :
System.Diagnostics.Stopwatch.GetTimestamp()
/ (double)System.Diagnostics.Stopwatch.Frequency;
internal static WindowOptions CreateStartupWindowOptions(
bool exactAutomationFramebuffer,
string persistedResolution,
bool useVSync)
{
WindowOptions defaults = WindowOptions.DefaultVulkan;
Vector2D<int> size = new(1280, 720);
WindowBorder border = defaults.WindowBorder;
if (exactAutomationFramebuffer)
{
if (!SilkRuntimeDisplayWindowTarget.TryParseResolution(
persistedResolution,
out int width,
out int height))
{
throw new InvalidOperationException(
"Exact automation framebuffer requires a valid persisted resolution.");
}
size = new Vector2D<int>(width, height);
border = WindowBorder.Hidden;
}
return defaults with
{
Size = size,
Title = "acdream — Vulkan",
VSync = useVSync,
WindowBorder = border,
// A desktop-sized borderless automation window must stay hidden,
// not iconified. Windows throttles/occludes an iconified GLFW
// surface, which prevents the performance gate from collecting a
// complete rolling sample window. Ordinary launches retain the
// Silk default visibility.
IsVisible = !exactAutomationFramebuffer,
};
}
private readonly AcDream.App.RuntimeOptions _options;
// Campaign LA slice LA1: no-op instance when --session-config didn't
// configure a statusFile (or the env-var launch path was used at all).
@ -57,6 +94,13 @@ public sealed class GameWindow :
// loop!" and would otherwise bury whatever exception actually wounded
// the loop). See docs/ISSUES.md #343.
private bool _renderLoopArmed;
// Silk may invoke Closing synchronously from IWindow.Close during Update,
// then still invoke Render once before its loop exits. Teardown cannot run
// from that Closing callback: it would dispose the scene while the cached
// render delegate is still eligible to execute. Latch the edge, skip that
// terminal render, and close the ownership graph after Run returns.
private bool _nativeCloseRequested;
private bool _nativeRunReturned;
private SilkWindowCallbackBinding? _windowCallbacks;
private GameWindowGraphics? _graphics;
// Campaign V slice V6h: borrowed, not owned — _graphics owns the context and
@ -414,6 +458,8 @@ public sealed class GameWindow :
private readonly AcDream.App.UI.RetailUiRuntimeLease _retailUiLease = new();
private InteractionUiLateBindings? _interactionUiLateBindings;
private readonly DeferredRenderFrameDiagnosticsSource _uiFrameDiagnostics = new();
private readonly AcDream.App.Rendering.Packs.DeferredRenderPackDiagnosticsSource
_renderPackDiagnostics = new();
private readonly AcDream.App.Combat.CombatAttackOperationsSlot
_combatAttackOperations = new();
private readonly AcDream.App.Combat.RuntimeCombatTargetOperationsSlot
@ -449,6 +495,7 @@ public sealed class GameWindow :
private AcDream.App.Rendering.ChargenPreviewController? _summaryPreviewController;
// Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad.
private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry;
private readonly AcDream.App.Plugins.BufferedRenderPackRegistry? _renderPackRegistry;
private AcDream.App.Plugins.GraphicalPluginSession? _pluginSession;
// Campaign V slice V11 deleted the ImGui developer-tools frontend along
// with the OpenGL backend it required, so no host ever composes a
@ -636,7 +683,8 @@ public sealed class GameWindow :
WorldEvents worldEvents,
AcDream.App.Plugins.BufferedUiRegistry? uiRegistry,
GraphicalHostPlatformServices platformServices,
AcDream.App.Plugins.AppAutomationSurface? automation = null)
AcDream.App.Plugins.AppAutomationSurface? automation = null,
AcDream.App.Plugins.BufferedRenderPackRegistry? renderPackRegistry = null)
{
_options = options ?? throw new System.ArgumentNullException(nameof(options));
_automation = automation;
@ -724,6 +772,7 @@ public sealed class GameWindow :
characterOptionValue: _runtime.CharacterOwner.Options.GetOptionBit);
_animationDiagnostics = AnimationPresentationDiagnostics.FromEnvironment();
_uiRegistry = uiRegistry;
_renderPackRegistry = renderPackRegistry;
_animatedEntities = new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(
_liveEntityRuntimeSlot);
// #184 Slice 2a: the extracted per-remote DR tick. Its stateful
@ -799,12 +848,10 @@ public sealed class GameWindow :
// attribute there — both are attachment properties the RHI device
// configures instead. The raw-GL window options this used to fork to
// were deleted at Campaign V slice V11.
var options = WindowOptions.DefaultVulkan with
{
Size = new Vector2D<int>(1280, 720),
Title = "acdream — Vulkan",
VSync = startupPacing.UseVSync,
};
WindowOptions options = CreateStartupWindowOptions(
_options.ExactAutomationFramebuffer,
startup.Display.Resolution,
startupPacing.UseVSync);
_startupPacing = startupPacing;
_startupQuality = startup.Quality;
@ -833,6 +880,8 @@ public sealed class GameWindow :
try
{
_window.Run();
_nativeRunReturned = true;
CompleteShutdown(releaseNativeWindow: false);
}
catch (Exception failure)
{
@ -1359,7 +1408,10 @@ public sealed class GameWindow :
_localPlayerMode,
_chaseCameraInput,
_pointerPosition,
_renderDiagnosticLog),
_renderDiagnosticLog,
_options.InitialOrbitDistanceMeters,
_options.InitialOrbitYawDegrees,
_options.InitialOrbitPitchDegrees),
this).Compose(platformResult),
(platformResult, hostInputCamera) =>
new ContentEffectsAudioCompositionPhase(
@ -1387,7 +1439,11 @@ public sealed class GameWindow :
new SilkRuntimeDisplayWindowTarget(_window!),
_displayFramePacing,
hostInputCamera.CameraController,
contentEffectsAudio.Audio?.Engine)))
contentEffectsAudio.Audio?.Engine))
{
RenderPacks = _renderPackRegistry,
GpuDevice = hostInputCamera.GpuDevice,
})
.Compose(platformResult, hostInputCamera, contentEffectsAudio),
(platformResult, contentEffectsAudio, settingsDevTools) =>
{
@ -1461,7 +1517,9 @@ public sealed class GameWindow :
Console.WriteLine,
hostInputCamera.GpuDevice,
hostInputCamera.GpuFrameLifetime,
() => WorldTime.CurrentCalendar),
() => WorldTime.CurrentCalendar,
settingsDevTools.RenderPacks,
_renderPackDiagnostics.CaptureDiagnostics),
_retailUiLease,
this).Compose(
platformResult,
@ -1519,7 +1577,8 @@ public sealed class GameWindow :
DevFrameDiagnostics: null,
_uiFrameDiagnostics,
Console.WriteLine,
compositionToast),
compositionToast,
_renderPackDiagnostics),
this).Compose(
platformResult,
hostInputCamera,
@ -1631,7 +1690,8 @@ public sealed class GameWindow :
_animatedEntities,
_updateFrameClock,
_frameGraphs,
Console.WriteLine),
Console.WriteLine,
_renderPackDiagnostics),
this).Compose(
platformResult,
hostInputCamera,
@ -1672,6 +1732,11 @@ public sealed class GameWindow :
// #343: see OnUpdate above — armed on entry, cleared on every normal
// exit path below, left stuck true if anything here throws.
_renderLoopArmed = true;
if (_nativeCloseRequested)
{
_renderLoopArmed = false;
return;
}
Vector2D<int> size = _window!.Size;
// Campaign V slice V6h: swapchain currency is the one piece of
// presentation the RHI contract deliberately leaves to the host (plan
@ -1735,12 +1800,23 @@ public sealed class GameWindow :
private void CompleteShutdown(bool releaseNativeWindow)
{
// IWindow.Close can raise Closing synchronously from Update and Silk
// can still issue one cached Render callback before Run returns. Keep
// Closing as the one narrow shutdown edge, but do not release frame
// owners until the native loop has actually returned. OnRender sees
// this latch and makes that terminal callback inert.
if (!releaseNativeWindow && !_nativeRunReturned)
{
_nativeCloseRequested = true;
return;
}
if (!_lifetime.HasShutdownRoots)
{
// Campaign LA slice LA1: capture BEFORE the shutdown roots run —
// by the time teardown completes, IsInWorld is always false
// regardless of whether a real session was ever connected.
// OnClosing() and Dispose() both funnel through this method;
// post-Run shutdown and Dispose() both funnel through this method;
// HasShutdownRoots's own guard means this fires exactly once,
// from whichever of the two reaches it first.
if (_runtime.Session.IsInWorld)
@ -1754,7 +1830,7 @@ public sealed class GameWindow :
if (report.Status == GameWindowLifetimeStatus.Complete)
{
// "exited" = terminal — only the true Dispose() call (not the
// OnClosing() native-window-close-request pass) represents the
// post-Run native-window-close-request pass) represents the
// process actually being done.
if (releaseNativeWindow)
ReportExited(report);