Vulkan is the sole, user-signed-off backend (V10 landed) and step 1 already removed ImGui/Studio/DevTools. This step deletes the GL rendering backend itself: every Gpu/Gl/** implementation, the Wb ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/ BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache, RenderBootstrap, and RenderFrameGlStateController. GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/ OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone — there is nothing left to select between. The five world-draw dual-arm renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer, ParticleRenderer, SkyRenderer) and the composition roots (WorldRenderComposition, HostInputCameraComposition, LivePresentationComposition, FrameRootComposition) collapse to their RHI-only arm. GL-only diagnostic properties with a live external reader (DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op rather than disappearing, since the reader is out of this commit's scope. A few GL-flavored mechanisms turned out to be backend-neutral once isolated: GlConstructionCleanupLedger is renamed ResourceConstructionCleanupLedger (exception-chain walking has nothing to do with GL), and GlfwNativePlatformProbe moved out of the otherwise GL-only GraphicalCapabilityRecord.cs into GraphicalWindowBackendSelection.cs before the rest of that file was deleted. Test files with no surviving subject are deleted outright (GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests, PortalDepthShaderParityTests, TextureCacheBindlessTests, TextRendererFailureSafetyTests, ClipFrameUploadTests, every Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests); others get their dead GL-only members trimmed while their live assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now reads GpuBindingModel.StorageClipRegions, the same binding index under its new backend-neutral name; GpuResourceRetirementTransactionTests drops its OpenGLGraphicsDevice-subclassing test double and the two GL queue tests it existed for). EnvCellRendererTests' construction helper now builds a real ObjectMeshManager via VulkanMeshPipelineDevice instead of passing null through a null-forgiving operator, since the RHI constructor never tolerated a null mesh manager and the old GL constructor (which did) is gone. Deferred to the next two steps, deliberately not touched here: the Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl (WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale csproj comment (the package itself is still load-bearing — TextureFormat and friends are used well beyond the deleted ManagedGLUniformBuffer), and the CI/gate scripts. Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors. Tests: full-solution `dotnet test` green across every project (App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all others 100%); the 2 App.Tests names that flake under full-suite parallel execution (#250-family, documented pre-existing) pass in isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
202 lines
10 KiB
C#
202 lines
10 KiB
C#
using System;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using AcDream.App.Rendering.Residency;
|
|
using AcDream.App.Streaming;
|
|
|
|
namespace AcDream.App;
|
|
|
|
/// <summary>
|
|
/// Typed bundle of startup-time configuration read from the process
|
|
/// environment. Built once in <c>Program.cs</c> and passed to
|
|
/// <c>GameWindow</c> so the rest of the app reads its config through
|
|
/// strongly-typed fields instead of scattered
|
|
/// <c>Environment.GetEnvironmentVariable</c> calls.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <strong>Scope:</strong> startup-time only — values that don't change
|
|
/// once the window is up. Runtime diagnostic toggles
|
|
/// (e.g. <c>ACDREAM_DUMP_MOTION</c>, <c>ACDREAM_PROBE_*</c>) belong in
|
|
/// diagnostic owner classes (see <c>AcDream.Core.Physics.PhysicsDiagnostics</c>
|
|
/// for the template), not here.
|
|
/// </para>
|
|
/// <para>
|
|
/// See <c>docs/architecture/code-structure.md</c> §2 Rule 4 for the
|
|
/// rule that drove this extraction, and §4 Step 1 for the broader
|
|
/// extraction sequence this is the first cut of.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed record RuntimeOptions(
|
|
string DatDir,
|
|
string PreparedAssetPath,
|
|
bool LiveMode,
|
|
string LiveHost,
|
|
int LivePort,
|
|
string? LiveUser,
|
|
string? LivePass,
|
|
bool DevTools,
|
|
bool UncappedRendering,
|
|
bool DumpMoveTruth,
|
|
bool DumpSky,
|
|
bool NoAudio,
|
|
bool EnableSkyPesDebug,
|
|
int HidePartIndex,
|
|
bool RetailCloseDegrades,
|
|
bool DumpSceneryZ,
|
|
bool DumpLiveSpawns,
|
|
bool DumpClothing,
|
|
int? LegacyStreamRadius,
|
|
bool RetailUi,
|
|
string? AcDir,
|
|
bool UiProbeDump,
|
|
string? UiProbeScript,
|
|
string? AutomationArtifactDirectory,
|
|
int? ForcedDayGroupIndex,
|
|
float? PinnedWorldDayFraction,
|
|
float? SkyAnimationPhaseSeconds,
|
|
float FogStartMultiplier,
|
|
float FogEndMultiplier,
|
|
ResidencyBudgetOptions ResidencyBudgets,
|
|
StreamingWorkBudgetOptions StreamingWorkBudgets,
|
|
string? VulkanDeviceOverride,
|
|
string? VulkanForcedUnsupportedFeature,
|
|
bool VulkanCapabilityProbe,
|
|
int VulkanCapabilityProbeFrames)
|
|
{
|
|
/// <summary>
|
|
/// Build options from the process environment. Used by
|
|
/// <c>Program.cs</c> at startup.
|
|
/// </summary>
|
|
public static RuntimeOptions FromEnvironment(string datDir)
|
|
=> Parse(datDir, Environment.GetEnvironmentVariable);
|
|
|
|
/// <summary>
|
|
/// Build options from a custom environment getter. Used by tests to
|
|
/// inject controlled env values without touching the process
|
|
/// environment.
|
|
/// </summary>
|
|
/// <param name="datDir">Resolved dat-file directory.</param>
|
|
/// <param name="env">Function returning the value for an env-var
|
|
/// name, or <c>null</c> when unset.</param>
|
|
public static RuntimeOptions Parse(string datDir, Func<string, string?> env)
|
|
{
|
|
if (datDir is null) throw new ArgumentNullException(nameof(datDir));
|
|
if (env is null) throw new ArgumentNullException(nameof(env));
|
|
|
|
return new RuntimeOptions(
|
|
DatDir: datDir,
|
|
PreparedAssetPath: NullIfEmpty(env("ACDREAM_PAK_PATH"))
|
|
?? Path.Combine(datDir, "acdream.pak"),
|
|
LiveMode: IsExactlyOne(env("ACDREAM_LIVE")),
|
|
LiveHost: env("ACDREAM_TEST_HOST") ?? "127.0.0.1",
|
|
LivePort: TryParseInt(env("ACDREAM_TEST_PORT")) ?? 9000,
|
|
LiveUser: NullIfEmpty(env("ACDREAM_TEST_USER")),
|
|
LivePass: NullIfEmpty(env("ACDREAM_TEST_PASS")),
|
|
DevTools: IsExactlyOne(env("ACDREAM_DEVTOOLS")),
|
|
// Normal presentation is always bounded by VSync or a
|
|
// refresh-rate software pacer. This explicit diagnostic is the
|
|
// sole way to measure truly uncapped renderer throughput.
|
|
UncappedRendering: IsExactlyOne(env("ACDREAM_UNCAPPED_RENDER")),
|
|
DumpMoveTruth: IsExactlyOne(env("ACDREAM_DUMP_MOVE_TRUTH")),
|
|
DumpSky: IsExactlyOne(env("ACDREAM_DUMP_SKY")),
|
|
NoAudio: IsExactlyOne(env("ACDREAM_NO_AUDIO")),
|
|
EnableSkyPesDebug: IsExactlyOne(env("ACDREAM_ENABLE_SKY_PES")),
|
|
HidePartIndex: TryParseInt(env("ACDREAM_HIDE_PART")) ?? -1,
|
|
// Default-on: any value other than the literal string "0" enables
|
|
// retail close-detail degrades. Set ACDREAM_RETAIL_CLOSE_DEGRADES=0
|
|
// only for before/after diagnostic comparisons.
|
|
RetailCloseDegrades: !string.Equals(env("ACDREAM_RETAIL_CLOSE_DEGRADES"), "0", StringComparison.Ordinal),
|
|
DumpSceneryZ: IsExactlyOne(env("ACDREAM_DUMP_SCENERY_Z")),
|
|
DumpLiveSpawns: IsExactlyOne(env("ACDREAM_DUMP_LIVE_SPAWNS")),
|
|
DumpClothing: IsExactlyOne(env("ACDREAM_DUMP_CLOTHING")),
|
|
// Legacy override for ACDREAM_STREAM_RADIUS. Caller applies it on
|
|
// top of the quality preset's radii. Null when unset or invalid.
|
|
LegacyStreamRadius: TryParseNonNegativeInt(env("ACDREAM_STREAM_RADIUS")),
|
|
RetailUi: IsExactlyOne(env("ACDREAM_RETAIL_UI")),
|
|
AcDir: NullIfEmpty(env("ACDREAM_AC_DIR")),
|
|
UiProbeDump: IsExactlyOne(env("ACDREAM_UI_PROBE_DUMP")),
|
|
UiProbeScript: NullIfEmpty(env("ACDREAM_UI_PROBE_SCRIPT")),
|
|
AutomationArtifactDirectory:
|
|
NullIfEmpty(env("ACDREAM_AUTOMATION_ARTIFACT_DIR")),
|
|
ForcedDayGroupIndex:
|
|
TryParseNonNegativeInt(env("ACDREAM_DAY_GROUP")),
|
|
// Campaign V slice V7 instrument determinism: pins the Dereth day
|
|
// fraction, and therefore the sun direction, the sky keyframe and
|
|
// every lit surface. Distinct from the /time slash command, which is
|
|
// deliberately transient (the next TimeSync clears it) and so cannot
|
|
// hold a connected route still. Accepted only inside [0, 1);
|
|
// anything else -- unset, unparseable, negative, >= 1 -- leaves the
|
|
// server clock alone, which is every ordinary run.
|
|
PinnedWorldDayFraction:
|
|
TryParseDayFraction(env("ACDREAM_WORLD_TIME")),
|
|
// Campaign V slice V7 instrument determinism: pins the sky's UV
|
|
// scroll phase — the cloud sheet — to a fixed elapsed-seconds value
|
|
// instead of the wall clock, so two launches of the differential
|
|
// gate agree about where the clouds are. ACDREAM_DAY_GROUP and the
|
|
// AcdreamCycleTimeOfDay override pin the OTHER sky clock (day group,
|
|
// keyframe, sun angle); this one is independent of both by design,
|
|
// because retail's clouds drift with real time rather than with the
|
|
// date. Unset — the default and every ordinary run — keeps the wall
|
|
// clock. Negative values are accepted: the offset is taken modulo 1
|
|
// per axis, so any finite number is a valid phase.
|
|
SkyAnimationPhaseSeconds:
|
|
TryParseFloat(env("ACDREAM_SKY_PHASE_SECONDS")),
|
|
FogStartMultiplier: TryParseFloat(env("ACDREAM_FOG_START_MULT")) ?? 0.7f,
|
|
FogEndMultiplier: TryParseFloat(env("ACDREAM_FOG_END_MULT")) ?? 0.95f,
|
|
ResidencyBudgets: ResidencyBudgetOptions.Parse(env),
|
|
StreamingWorkBudgets: StreamingWorkBudgetOptions.Parse(env),
|
|
// Physical-device override, matched as a decimal index first and then
|
|
// as a case-insensitive device-name substring. Recorded verbatim in
|
|
// graphical-capabilities-vulkan.json whether or not it matched.
|
|
VulkanDeviceOverride:
|
|
NullIfEmpty(env("ACDREAM_VULKAN_DEVICE")),
|
|
// Slice V5 gate knob: names one required capability to report as
|
|
// absent so the NotSupportedException -> exit-code-4 -> report path
|
|
// can be exercised on hardware that actually supports everything.
|
|
VulkanForcedUnsupportedFeature:
|
|
NullIfEmpty(env("ACDREAM_VULKAN_FORCE_UNSUPPORTED")),
|
|
// Campaign V slice V6h: with ACDREAM_RENDER_BACKEND=vulkan, run the
|
|
// V5/V6c bring-up harness — capability gate plus the synthetic
|
|
// verification scenes — instead of the real composition host. A
|
|
// diagnostic for "does this machine pass the Vulkan gate, and does
|
|
// the backend draw?"; ignored on OpenGL.
|
|
VulkanCapabilityProbe:
|
|
IsExactlyOne(env("ACDREAM_VULKAN_PROBE")),
|
|
// Campaign V slice V9: bound the probe harness to a frame budget so
|
|
// it can run unattended. The harness otherwise presents until its
|
|
// window closes, which is right for a developer answering "does this
|
|
// machine pass?" at a desk and impossible for CI, where nothing ever
|
|
// closes the window. Zero -- unset, unparseable, or an explicit 0 --
|
|
// keeps the interactive behaviour, so no existing invocation changes.
|
|
VulkanCapabilityProbeFrames:
|
|
TryParseNonNegativeInt(env("ACDREAM_VULKAN_PROBE_FRAMES")) ?? 0);
|
|
}
|
|
|
|
/// <summary>True iff live-mode credentials are present and valid for connecting.</summary>
|
|
public bool HasLiveCredentials =>
|
|
LiveMode && !string.IsNullOrEmpty(LiveUser) && !string.IsNullOrEmpty(LivePass);
|
|
|
|
/// <summary>True when the opt-in retail UI automation probe should be constructed.</summary>
|
|
public bool UiProbeEnabled => UiProbeDump || !string.IsNullOrEmpty(UiProbeScript);
|
|
|
|
private static bool IsExactlyOne(string? s)
|
|
=> string.Equals(s, "1", StringComparison.Ordinal);
|
|
|
|
private static string? NullIfEmpty(string? s)
|
|
=> string.IsNullOrEmpty(s) ? null : s;
|
|
|
|
private static int? TryParseInt(string? s)
|
|
=> int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) ? v : null;
|
|
|
|
private static int? TryParseNonNegativeInt(string? s)
|
|
=> TryParseInt(s) is { } v && v >= 0 ? v : null;
|
|
|
|
private static float? TryParseDayFraction(string? s)
|
|
=> TryParseFloat(s) is { } value && value >= 0f && value < 1f ? value : null;
|
|
|
|
private static float? TryParseFloat(string? s)
|
|
=> float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)
|
|
? value
|
|
: null;
|
|
}
|