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

@ -51,7 +51,9 @@ internal sealed record FrameRootDependencies(
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> Animations,
UpdateFrameClock UpdateClock,
GameFrameGraphSlot FrameGraphs,
Action<string> Log)
Action<string> Log,
AcDream.App.Rendering.Packs.DeferredRenderPackDiagnosticsSource?
RenderPackDiagnostics = null)
{
public RuntimeLocalPlayerMovementState PlayerController =>
Runtime.MovementOwner;
@ -234,6 +236,7 @@ internal sealed class FrameRootCompositionPhase
ref bool bindingsOwnedByScope)
{
FrameRootDependencies d = _dependencies;
bindings = new FrameRootRuntimeBindings();
WorldRenderFoundation foundation = world.Foundation;
// Campaign V slice V6h: the frame root's raw-GL render graph fork was
// deleted at slice V11. The graph is the clear pass, the private-
@ -285,6 +288,35 @@ internal sealed class FrameRootCompositionPhase
renderFrameLivePreparation);
Fault(FrameRootCompositionPoint.RenderResourcesCreated);
AcDream.App.Rendering.Packs.RenderPackController? renderPackController = null;
AcDream.App.Rendering.Packs.RenderPackSelectionBinding? renderPackSelection = null;
AcDream.App.Rendering.Packs.AtmosphericFrameInputState? atmosphericInputs = null;
if (settings.RenderPacks is { } renderPackCatalog)
{
renderPackController = new AcDream.App.Rendering.Packs.RenderPackController(
renderPackCatalog.Snapshot,
new AcDream.App.Rendering.Packs.AtmosphericRenderPackRuntimeFactory(
host.GpuDevice),
new AcDream.App.Rendering.Packs.RenderPackReceiverPipelineCoordinator(
foundation.Terrain!,
live.DrawDispatcher!),
AcDream.App.Rendering.Packs.ThreadPoolRenderPackPreparationScheduler.Instance,
renderPackCatalog);
bindings.Adopt("render-pack controller", renderPackController);
renderPackSelection = new AcDream.App.Rendering.Packs.RenderPackSelectionBinding(
d.Settings,
renderPackController,
d.Log);
bindings.Adopt("render-pack selection", renderPackSelection);
if (d.RenderPackDiagnostics is { } renderPackDiagnostics)
{
bindings.Adopt(
"render-pack diagnostics",
renderPackDiagnostics.BindOwned(renderPackController));
}
atmosphericInputs = new AcDream.App.Rendering.Packs.AtmosphericFrameInputState();
}
var renderWeatherFrame = new RenderWeatherFrameController(
d.WorldTime,
d.Weather);
@ -463,7 +495,8 @@ internal sealed class FrameRootCompositionPhase
worldScenePasses,
d.RenderRange,
worldSceneDiagnostics,
live.WorldAvailability);
live.WorldAvailability,
atmosphericInputs);
// The world renderer runs INSIDE the frame's one backbuffer pass,
// which this phase opens, publishes on the scope, and closes.
worldSceneRenderer =
@ -474,14 +507,70 @@ internal sealed class FrameRootCompositionPhase
(d.Graphics as VulkanGameWindowGraphics)?.WorldPassScopeCore
?? throw new InvalidOperationException(
"The Vulkan world phase requires the Vulkan graphics handle."),
worldSceneRenderer);
worldSceneRenderer,
renderPackController,
atmosphericInputs,
renderPackSelection is null
? null
: renderPackSelection.ApplyAtFrameBoundary,
live.RenderSceneShadow,
live.DrawDispatcher,
foundation.Terrain);
}
Fault(FrameRootCompositionPoint.WorldRendererCreated);
bindings = new FrameRootRuntimeBindings();
WorldLifecycleAutomationController? lifecycleAutomation = null;
if (interaction.RetainedUi?.Screenshots is { } screenshots
&& d.Options.AutomationArtifactDirectory is { } artifactDirectory)
{
AcDream.UI.Abstractions.Panels.Settings.RenderPackSelectionSettings?
automationLastEnhancedSelection = null;
(bool Succeeded, string Error) SaveAutomationRenderPackSelection(
AcDream.UI.Abstractions.Panels.Settings.RenderPackSelectionSettings selection)
{
d.Settings.SaveDisplay(d.Settings.Display with
{
RenderPack = selection,
});
return d.Settings.Display.RenderPack == selection
? (true, string.Empty)
: (false, $"render-pack selection '{selection.PresetId}' was not persisted");
}
(bool Succeeded, string Error) SelectAutomationRenderPack(string preset)
{
var selection = string.Equals(
preset,
"retail",
StringComparison.Ordinal)
? AcDream.UI.Abstractions.Panels.Settings
.RenderPackSelectionSettings.Retail
: new AcDream.UI.Abstractions.Panels.Settings
.RenderPackSelectionSettings(
AcDream.App.Rendering.Packs
.BuiltInAtmosphericRenderPack.Id,
"1.0.0",
preset);
return SaveAutomationRenderPackSelection(selection);
}
(bool Succeeded, string Error) DisableAutomationRenderPack()
{
var current = d.Settings.Display.RenderPack;
if (!current.IsRetail)
automationLastEnhancedSelection = current;
return SaveAutomationRenderPackSelection(
AcDream.UI.Abstractions.Panels.Settings
.RenderPackSelectionSettings.Retail);
}
(bool Succeeded, string Error) ReenableAutomationRenderPack()
{
return automationLastEnhancedSelection is { } selection
? SaveAutomationRenderPackSelection(selection)
: (false, "render-pack re-enable requires a prior enhanced selection");
}
var resourceSnapshots =
new WorldLifecycleResourceSnapshotSource(
live.WorldState,
@ -515,7 +604,86 @@ internal sealed class FrameRootCompositionPhase
resourceSnapshots.Capture,
screenshots,
artifactDirectory,
message => d.Log("[UI-PROBE] " + message));
message => d.Log("[UI-PROBE] " + message),
() => renderPackController?.MinimumPerformanceSampleCount ?? 0,
() =>
{
if (renderPackController is null)
{
return (
false,
"render-pack performance automation is unavailable");
}
bool reset = renderPackController.TryResetPerformanceEvidence(
out string error);
return (reset, error);
},
() => renderPackController?.Snapshot.State ==
AcDream.App.Rendering.Packs.RenderPackActivationState.FailedToRetail,
getRenderPackStatus: () =>
{
AcDream.App.Rendering.Packs.RenderPackActivationSnapshot snapshot =
renderPackController?.Snapshot
?? new AcDream.App.Rendering.Packs.RenderPackActivationSnapshot(
AcDream.App.Rendering.Packs.RenderPackActivationState.Retail,
AcDream.UI.Abstractions.Panels.Settings
.RenderPackSelectionSettings.Retail,
ActivePackDisplayName: null,
Reason: null,
ActivationGeneration: 0);
var state = snapshot.State switch
{
AcDream.App.Rendering.Packs.RenderPackActivationState.Retail =>
AcDream.App.UI.Testing
.RetailUiAutomationRenderPackState.Retail,
AcDream.App.Rendering.Packs.RenderPackActivationState.CandidatePending =>
AcDream.App.UI.Testing
.RetailUiAutomationRenderPackState.CandidatePending,
AcDream.App.Rendering.Packs.RenderPackActivationState.Active =>
AcDream.App.UI.Testing
.RetailUiAutomationRenderPackState.Active,
AcDream.App.Rendering.Packs.RenderPackActivationState.FailedToRetail =>
AcDream.App.UI.Testing
.RetailUiAutomationRenderPackState.FailedToRetail,
_ => throw new ArgumentOutOfRangeException(),
};
return new AcDream.App.UI.Testing
.RetailUiAutomationRenderPackStatus(
state,
snapshot.Selection.PackId,
snapshot.Selection.PresetId,
snapshot.ActivationGeneration,
snapshot.Reason);
},
selectRenderPack: SelectAutomationRenderPack,
disableRenderPack: DisableAutomationRenderPack,
reenableRenderPack: ReenableAutomationRenderPack,
getFramebufferSize: () =>
{
var size = d.Window.FramebufferSize;
return (size.X, size.Y);
},
resizeFramebuffer: (width, height) =>
{
if (d.Settings.Display.Fullscreen)
{
return (
false,
"automation framebuffer resize requires windowed mode");
}
string resolution = $"{width}x{height}";
d.Settings.SaveDisplay(d.Settings.Display with
{
Resolution = resolution,
});
return string.Equals(
d.Settings.Display.Resolution,
resolution,
StringComparison.Ordinal)
? (true, string.Empty)
: (false, $"framebuffer resize '{resolution}' was not persisted");
},
requestClientClose: d.Window.Close);
bindings.Adopt(
"world lifecycle automation owner",
lifecycleAutomation);

View file

@ -52,7 +52,10 @@ internal sealed record HostInputCameraDependencies(
LocalPlayerModeState LocalPlayerMode,
ChaseCameraInputState ChaseCameraInput,
PointerPositionState PointerPosition,
IRenderFrameDiagnosticLog RenderDiagnosticLog);
IRenderFrameDiagnosticLog RenderDiagnosticLog,
float? InitialOrbitDistanceMeters = null,
float? InitialOrbitYawDegrees = null,
float? InitialOrbitPitchDegrees = null);
/// <summary>
/// The construction seam every backend differs at. Campaign V slice V6h widened
@ -101,7 +104,10 @@ internal interface IHostInputCameraCompositionFactory
IKeyboardSource keyboard,
IMouseSource mouse,
KeyBindings bindings);
CameraController CreateCameraController();
CameraController CreateCameraController(
float? initialOrbitDistanceMeters,
float? initialOrbitYawDegrees,
float? initialOrbitPitchDegrees);
IFramebufferCameraTarget CreateCameraTarget(CameraController camera);
CameraPointerInputController CreateCameraPointerInput(
IReadOnlyList<IMouse> mice,
@ -311,7 +317,10 @@ internal sealed class HostInputCameraCompositionPhase :
Fault(HostInputCameraCompositionPoint.CameraInputBound);
}
CameraController camera = _factory.CreateCameraController();
CameraController camera = _factory.CreateCameraController(
_dependencies.InitialOrbitDistanceMeters,
_dependencies.InitialOrbitYawDegrees,
_dependencies.InitialOrbitPitchDegrees);
_publication.PublishCameraController(camera);
Fault(HostInputCameraCompositionPoint.CameraPublished);
_dependencies.FramebufferResize.BindCamera(

View file

@ -92,7 +92,10 @@ internal sealed record InteractionRetainedUiDependencies(
// needed. gmMapUI::Update @0x004a1eb0 reads GameTime::current_game_time
// every 5s — MapPageController owns that cadence, this just supplies the
// current reading.
Func<AcDream.Core.World.DerethDateTime.Calendar> CurrentCalendar)
Func<AcDream.Core.World.DerethDateTime.Calendar> CurrentCalendar,
AcDream.App.Rendering.Packs.RenderPackCatalogSource? RenderPackCatalog = null,
Func<AcDream.App.Rendering.Packs.RenderPackDiagnosticsSnapshot>?
RenderPackDiagnostics = null)
{
public RuntimeActionState Actions => Runtime.ActionOwner;
@ -643,7 +646,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
screenshots = new FrameScreenshotController(
d.BackbufferReader,
Path.Combine(artifactDirectory, "screenshots"),
ProbeLog);
ProbeLog,
d.RenderPackDiagnostics);
}
checkpoint(InteractionRetainedUiCompositionPoint.UiProbeCreated);
@ -949,7 +953,53 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
LoadDisplay: () => d.Settings.Display,
SaveDisplay: d.Settings.SaveDisplay,
LoadAudio: () => d.Settings.Audio,
SaveAudio: d.Settings.SaveAudio),
SaveAudio: d.Settings.SaveAudio,
LoadRenderPackChoices: d.RenderPackCatalog is null
? null
: () => d.RenderPackCatalog.Snapshot().Entries
.Select(entry =>
new ConfigOptionsPageController.RenderPackChoice(
entry.Descriptor.Id,
entry.Descriptor.DisplayName,
entry.Descriptor.PackVersion.ToString(),
entry.IsCompatible,
entry.IncompatibilityReason,
entry.Descriptor.QualityPresets
.Select(preset =>
{
entry.PresetIncompatibilityReasons.TryGetValue(
preset.Id,
out string? reason);
return new ConfigOptionsPageController.RenderPackPresetChoice(
preset.Id,
preset.DisplayName,
entry.IsCompatible && reason is null,
reason ?? entry.IncompatibilityReason)
{
SettingOverrides = preset.SettingOverrides,
MaxResidentGpuBytes = preset.MaxResidentGpuBytes,
MaxIncrementalGpuMillisecondsP50 =
preset.MaxIncrementalGpuMillisecondsP50,
MaxIncrementalGpuMillisecondsP99 =
preset.MaxIncrementalGpuMillisecondsP99,
MaxIncrementalCpuMillisecondsP50 =
preset.MaxIncrementalCpuMillisecondsP50,
MaxIncrementalCpuMillisecondsP99 =
preset.MaxIncrementalCpuMillisecondsP99,
};
})
.ToArray())
{
FeatureSummary = entry.Descriptor.FeatureSummary,
Settings = entry.Descriptor.Settings,
})
.ToArray(),
LoadRenderPackCatalogRevision: d.RenderPackCatalog is null
? null
: () => d.RenderPackCatalog.Revision,
LoadRenderPackFailureNotice: d.RenderPackDiagnostics is null
? null
: () => d.RenderPackDiagnostics().FailureReason),
// Campaign FA slice FA3: the social panel's own bindings —
// FA2's typed Fellowship/Allegiance snapshot readers off the
// GameRuntime views, plus J4.1's Friends/Squelch owners

View file

@ -792,6 +792,67 @@ internal sealed class DeferredWorldLifecycleAutomationRuntime
!_deactivated && _target?.IsWorldViewportVisible == true;
public int PortalMaterializationCount =>
!_deactivated ? _target?.PortalMaterializationCount ?? 0 : 0;
public int RenderPackPerformanceSampleCount =>
!_deactivated ? _target?.RenderPackPerformanceSampleCount ?? 0 : 0;
public bool RenderPackFailedToRetail =>
!_deactivated && _target?.RenderPackFailedToRetail == true;
public RetailUiAutomationRenderPackStatus RenderPackStatus =>
!_deactivated
? _target?.RenderPackStatus
?? RetailUiAutomationRenderPackStatus.Retail
: RetailUiAutomationRenderPackStatus.Retail;
public int FramebufferWidth =>
!_deactivated ? _target?.FramebufferWidth ?? 0 : 0;
public int FramebufferHeight =>
!_deactivated ? _target?.FramebufferHeight ?? 0 : 0;
public bool TrySelectRenderPack(string presetId, out string error)
{
if (!_deactivated && _target is { } target)
return target.TrySelectRenderPack(presetId, out error);
error = "world lifecycle automation is not bound";
return false;
}
public bool TryDisableRenderPack(out string error)
{
if (!_deactivated && _target is { } target)
return target.TryDisableRenderPack(out error);
error = "world lifecycle automation is not bound";
return false;
}
public bool TryReenableRenderPack(out string error)
{
if (!_deactivated && _target is { } target)
return target.TryReenableRenderPack(out error);
error = "world lifecycle automation is not bound";
return false;
}
public bool TryResizeFramebuffer(int width, int height, out string error)
{
if (!_deactivated && _target is { } target)
return target.TryResizeFramebuffer(width, height, out error);
error = "world lifecycle automation is not bound";
return false;
}
public bool TryResetRenderPackPerformance(out string error)
{
if (!_deactivated && _target is { } target)
return target.TryResetRenderPackPerformance(out error);
error = "world lifecycle automation is not bound";
return false;
}
public bool TryRequestClientClose(out string error)
{
if (!_deactivated && _target is { } target)
return target.TryRequestClientClose(out error);
error = "world lifecycle automation is not bound";
return false;
}
public IDisposable Bind(IRetailUiAutomationRuntime target)
{

View file

@ -74,7 +74,9 @@ internal sealed record LivePresentationDependencies(
DeferredRenderFrameDiagnosticsSource? DevFrameDiagnostics,
DeferredRenderFrameDiagnosticsSource UiFrameDiagnostics,
Action<string> Log,
Action<string>? Toast)
Action<string>? Toast,
AcDream.App.Rendering.Packs.IRenderPackDiagnosticsSnapshotSource?
RenderPackDiagnostics = null)
{
public SelectionState Selection => Runtime.ActionOwner.Selection;
@ -448,7 +450,8 @@ internal sealed class LivePresentationCompositionPhase
LiveRenderProjectionJournal? liveRenderProjections =
renderSceneShadow?.BindLiveRuntime(
liveEntities,
new GpuWorldRenderTraversalOrderSource(worldState));
new GpuWorldRenderTraversalOrderSource(worldState),
d.PlayerIdentity);
Fault(LivePresentationCompositionPoint.CanonicalRuntimeCreated);
bindings.Adopt(
@ -803,7 +806,9 @@ internal sealed class LivePresentationCompositionPhase
d.TranslucencyFades,
selectionScene,
d.RetailAlphaQueue,
alphaScratchBudgets.DispatcherBytes),
alphaScratchBudgets.DispatcherBytes,
foundation.TerrainAtlas?.BuildingDetailTexture ?? default,
() => d.Settings.DisplayPreview.BuildingDetailTextures),
static value => value.Dispose());
var selectionQuery = new WorldSelectionQuery(
liveEntities,
@ -1238,9 +1243,11 @@ internal sealed class LivePresentationCompositionPhase
?? throw new InvalidOperationException(
"The graphics backend must publish a world pass scope."),
foundation.MeshAdapter!.MeshManager!,
envCellFrustum),
envCellFrustum,
foundation.TerrainAtlas?.EnvironmentDetailTexture ?? default,
() => d.Settings.DisplayPreview.BuildingDetailTextures),
static value => value.Dispose());
// The three pipelines ARE its program, built at construction — the
// The four pipelines ARE its program, built at construction — the
// raw-GL arm's separate Initialize(Shader) step was deleted at V11.
Fault(LivePresentationCompositionPoint.EnvironmentCellsCreated);
@ -1484,7 +1491,8 @@ internal sealed class LivePresentationCompositionPhase
new SilkRenderFrameTitleSink(d.Window),
d.RenderDiagnosticLog,
d.Options.UiProbeDump,
resourceDiagnostics);
resourceDiagnostics,
d.RenderPackDiagnostics);
if (d.DevFrameDiagnostics is { } devFrameDiagnostics)
{
bindings.Adopt(

View file

@ -1,4 +1,8 @@
using AcDream.App.Settings;
using AcDream.App.Plugins;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Packs;
using AcDream.UI.Abstractions.Panels.Settings;
using Silk.NET.Input;
namespace AcDream.App.Composition;
@ -18,11 +22,22 @@ namespace AcDream.App.Composition;
/// keybinds.json (not retail's <c>.keymap</c> format — register row AP-202).
/// </summary>
internal sealed record SettingsDevToolsResult(
AcDream.UI.Abstractions.Settings.QualitySettings ResolvedQuality);
AcDream.UI.Abstractions.Settings.QualitySettings ResolvedQuality)
{
internal RenderPackCatalogSource? RenderPacks { get; init; }
internal RenderPackSelectionSettings RenderPackSelection { get; init; } =
RenderPackSelectionSettings.Retail;
}
internal sealed record SettingsDevToolsDependencies(
RuntimeSettingsController Settings,
IRuntimeSettingsStartupTarget StartupTarget);
IRuntimeSettingsStartupTarget StartupTarget)
{
internal BufferedRenderPackRegistry? RenderPacks { get; init; }
internal IGpuDevice? GpuDevice { get; init; }
}
/// <summary>
/// Production Phase 3: applies the resolved startup display/audio settings.
@ -51,6 +66,19 @@ internal sealed class SettingsDevToolsCompositionPhase :
ArgumentNullException.ThrowIfNull(content);
_dependencies.Settings.ApplyStartup(_dependencies.StartupTarget);
return new SettingsDevToolsResult(_dependencies.Settings.ResolvedQuality);
RenderPackCatalogSource? renderPacks = null;
if (_dependencies.RenderPacks is { } registry
&& _dependencies.GpuDevice is { } gpu)
{
renderPacks = new RenderPackCatalogSource(
registry,
RenderPackCapabilityResolver.Resolve(gpu.Capabilities));
}
return new SettingsDevToolsResult(_dependencies.Settings.ResolvedQuality)
{
RenderPacks = renderPacks,
RenderPackSelection = _dependencies.Settings.Display.RenderPack,
};
}
}

View file

@ -82,8 +82,23 @@ internal sealed class VulkanHostInputCameraCompositionFactory
KeyBindings bindings) =>
InputDispatcher.CreateDetached(keyboard, mouse, bindings);
public CameraController CreateCameraController() =>
new(new OrbitCamera(), new FlyCamera());
public CameraController CreateCameraController(
float? initialOrbitDistanceMeters,
float? initialOrbitYawDegrees,
float? initialOrbitPitchDegrees)
{
var orbit = new OrbitCamera();
if (initialOrbitDistanceMeters is { } distance)
orbit.Distance = distance;
if (initialOrbitYawDegrees is { } yaw)
orbit.Yaw = DegreesToRadians(yaw);
if (initialOrbitPitchDegrees is { } pitch)
orbit.Pitch = DegreesToRadians(pitch);
return new CameraController(orbit, new FlyCamera());
}
private static float DegreesToRadians(float degrees) =>
degrees * (MathF.PI / 180f);
public IFramebufferCameraTarget CreateCameraTarget(CameraController camera) =>
new CameraFramebufferTarget(camera);

View file

@ -78,7 +78,10 @@ internal interface IGameWindowWorldRenderPublication
internal interface IWorldRenderCompositionFactory
{
WorldRegionData LoadRegion(IDatReaderWriter dats);
void InitializeEnvironment(WorldEnvironmentController environment, Region region);
void InitializeEnvironment(
WorldEnvironmentController environment,
Region region,
IDatReaderWriter dats);
/// <summary>
/// Campaign V slice V6i-2: the terrain atlas built through
/// <see cref="AcDream.App.Rendering.Gpu.IGpuDevice"/>. The raw-GL arm this
@ -188,11 +191,13 @@ internal sealed class RetailWorldRenderCompositionFactory
public void InitializeEnvironment(
WorldEnvironmentController environment,
Region region)
Region region,
IDatReaderWriter dats)
{
ArgumentNullException.ThrowIfNull(environment);
ArgumentNullException.ThrowIfNull(region);
environment.Initialize(region);
ArgumentNullException.ThrowIfNull(dats);
environment.Initialize(region, dats);
}
public TerrainAtlas AcquireBackendNeutralTerrainAtlas(
@ -452,7 +457,10 @@ internal sealed class WorldRenderCompositionPhase
WorldRegionData region = _factory.LoadRegion(content.Dats);
Fault(WorldRenderCompositionPoint.RegionLoaded);
_factory.InitializeEnvironment(_dependencies.Environment, region.Region);
_factory.InitializeEnvironment(
_dependencies.Environment,
region.Region,
content.Dats);
Fault(WorldRenderCompositionPoint.EnvironmentInitialized);
// Campaign V slice V6i-2: the atlas builds through IGpuDevice on