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>
567 lines
21 KiB
C#
567 lines
21 KiB
C#
using System.Collections.Generic;
|
|
using AcDream.App;
|
|
using AcDream.App.Rendering.Residency;
|
|
using AcDream.App.Streaming;
|
|
|
|
namespace AcDream.App.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for <see cref="RuntimeOptions"/> startup-time parsing.
|
|
/// Behavior-preservation only: every assertion locks in the exact
|
|
/// boolean / numeric / nullability semantics from the env reads that
|
|
/// previously lived in <c>GameWindow.cs</c>.
|
|
/// </summary>
|
|
public sealed class RuntimeOptionsTests
|
|
{
|
|
[Fact]
|
|
public void ResidencyBudgetsPreserveCurrentProductionDefaults()
|
|
{
|
|
RuntimeOptions options = RuntimeOptions.Parse(
|
|
"D:\\dat",
|
|
_ => null);
|
|
|
|
Assert.Equal(ResidencyBudgetOptions.Default, options.ResidencyBudgets);
|
|
Assert.Equal(
|
|
StreamingWorkBudgetOptions.Default,
|
|
options.StreamingWorkBudgets);
|
|
}
|
|
|
|
[Fact]
|
|
public void StreamingWorkBudgetOverridesAreOneTypedProfile()
|
|
{
|
|
var values = new Dictionary<string, string?>
|
|
{
|
|
["ACDREAM_STREAM_WORK_MS"] = "1.75",
|
|
["ACDREAM_STREAM_WORK_COMPLETIONS"] = "31",
|
|
["ACDREAM_STREAM_WORK_CPU_MIB"] = "6",
|
|
["ACDREAM_STREAM_WORK_ENTITY_OPS"] = "144",
|
|
["ACDREAM_STREAM_WORK_GPU_MIB"] = "5",
|
|
["ACDREAM_STREAM_WORK_GL_RETIRE_OPS"] = "23",
|
|
["ACDREAM_STREAM_WORK_DEST_RESERVE_PERCENT"] = "60",
|
|
};
|
|
|
|
RuntimeOptions options = RuntimeOptions.Parse(
|
|
"D:\\dat",
|
|
name => values.GetValueOrDefault(name));
|
|
|
|
Assert.Equal(1.75, options.StreamingWorkBudgets.MaxUpdateMilliseconds);
|
|
Assert.Equal(31, options.StreamingWorkBudgets.MaxCompletionAdmissions);
|
|
Assert.Equal(
|
|
6 * StreamingWorkBudgetOptions.MiB,
|
|
options.StreamingWorkBudgets.MaxAdoptedCpuBytes);
|
|
Assert.Equal(144, options.StreamingWorkBudgets.MaxEntityOperations);
|
|
Assert.Equal(
|
|
5 * StreamingWorkBudgetOptions.MiB,
|
|
options.StreamingWorkBudgets.MaxGpuUploadBytes);
|
|
Assert.Equal(23, options.StreamingWorkBudgets.MaxGlRetireOperations);
|
|
Assert.Equal(0.60f, options.StreamingWorkBudgets.DestinationReserveFraction);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("0")]
|
|
[InlineData("-1")]
|
|
[InlineData("NaN")]
|
|
[InlineData("Infinity")]
|
|
[InlineData("bad")]
|
|
public void InvalidStreamingWorkValuesFallBackIndependently(string value)
|
|
{
|
|
RuntimeOptions options = RuntimeOptions.Parse(
|
|
"D:\\dat",
|
|
name => name switch
|
|
{
|
|
"ACDREAM_STREAM_WORK_MS" => value,
|
|
"ACDREAM_STREAM_WORK_DEST_RESERVE_PERCENT" => value,
|
|
_ => null,
|
|
});
|
|
|
|
Assert.Equal(
|
|
StreamingWorkBudgetOptions.Default.MaxUpdateMilliseconds,
|
|
options.StreamingWorkBudgets.MaxUpdateMilliseconds);
|
|
Assert.Equal(
|
|
StreamingWorkBudgetOptions.Default.DestinationReserveFraction,
|
|
options.StreamingWorkBudgets.DestinationReserveFraction);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResidencyBudgetOverridesAreTypedMebibytesAndCounts()
|
|
{
|
|
var values = new Dictionary<string, string?>
|
|
{
|
|
["ACDREAM_RESIDENCY_MESH_GPU_MIB"] = "768",
|
|
["ACDREAM_RESIDENCY_MESH_UNOWNED_ENTRIES"] = "72",
|
|
["ACDREAM_RESIDENCY_ANIMATION_MIB"] = "48",
|
|
["ACDREAM_RESIDENCY_ANIMATION_ENTRIES"] = "300",
|
|
["ACDREAM_RESIDENCY_AUDIO_MIB"] = "24",
|
|
};
|
|
|
|
RuntimeOptions options = RuntimeOptions.Parse(
|
|
"D:\\dat",
|
|
name => values.GetValueOrDefault(name));
|
|
|
|
Assert.Equal(
|
|
768 * ResidencyBudgetOptions.MiB,
|
|
options.ResidencyBudgets.ObjectMeshGpuBytes);
|
|
Assert.Equal(
|
|
72,
|
|
options.ResidencyBudgets.ObjectMeshUnownedEntries);
|
|
Assert.Equal(
|
|
48 * ResidencyBudgetOptions.MiB,
|
|
options.ResidencyBudgets.AnimationBytes);
|
|
Assert.Equal(300, options.ResidencyBudgets.AnimationEntries);
|
|
Assert.Equal(
|
|
24 * ResidencyBudgetOptions.MiB,
|
|
options.ResidencyBudgets.AudioBytes);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("0")]
|
|
[InlineData("-1")]
|
|
[InlineData("bad")]
|
|
[InlineData("8796093022208")]
|
|
public void InvalidResidencyBudgetFallsBackToCurrentDefault(string value)
|
|
{
|
|
RuntimeOptions options = RuntimeOptions.Parse(
|
|
"D:\\dat",
|
|
name => name == "ACDREAM_RESIDENCY_MESH_GPU_MIB"
|
|
? value
|
|
: null);
|
|
|
|
Assert.Equal(
|
|
ResidencyBudgetOptions.Default.ObjectMeshGpuBytes,
|
|
options.ResidencyBudgets.ObjectMeshGpuBytes);
|
|
}
|
|
|
|
private const string AnyDatDir = "C:/Users/test/dats";
|
|
|
|
private static Func<string, string?> Env(Dictionary<string, string?> values)
|
|
=> name => values.TryGetValue(name, out var v) ? v : null;
|
|
|
|
private static Func<string, string?> EmptyEnv() => _ => null;
|
|
|
|
[Fact]
|
|
public void Defaults_AllSafeOff_WhenEnvironmentIsEmpty()
|
|
{
|
|
var opts = RuntimeOptions.Parse(AnyDatDir, EmptyEnv());
|
|
|
|
Assert.Equal(AnyDatDir, opts.DatDir);
|
|
Assert.Equal(
|
|
Path.Combine(AnyDatDir, "acdream.pak"),
|
|
opts.PreparedAssetPath);
|
|
Assert.False(opts.LiveMode);
|
|
Assert.Equal("127.0.0.1", opts.LiveHost);
|
|
Assert.Equal(9000, opts.LivePort);
|
|
Assert.Null(opts.LiveUser);
|
|
Assert.Null(opts.LivePass);
|
|
Assert.False(opts.DevTools);
|
|
Assert.False(opts.UncappedRendering);
|
|
Assert.False(opts.DumpMoveTruth);
|
|
Assert.False(opts.NoAudio);
|
|
Assert.False(opts.EnableSkyPesDebug);
|
|
Assert.Equal(-1, opts.HidePartIndex);
|
|
// Default-on: RetailCloseDegrades is true unless explicitly disabled.
|
|
Assert.True(opts.RetailCloseDegrades);
|
|
Assert.False(opts.DumpSceneryZ);
|
|
Assert.False(opts.DumpClothing);
|
|
Assert.Null(opts.LegacyStreamRadius);
|
|
Assert.False(opts.UiProbeDump);
|
|
Assert.Null(opts.UiProbeScript);
|
|
Assert.Null(opts.AutomationArtifactDirectory);
|
|
Assert.Equal(0.7f, opts.FogStartMultiplier);
|
|
Assert.Equal(0.95f, opts.FogEndMultiplier);
|
|
Assert.False(opts.UiProbeEnabled);
|
|
Assert.False(opts.HasLiveCredentials);
|
|
}
|
|
|
|
[Fact]
|
|
public void PreparedAssetPath_DefaultsBesideDats_AndAllowsOneOverride()
|
|
{
|
|
Assert.Equal(
|
|
Path.Combine(AnyDatDir, "acdream.pak"),
|
|
RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).PreparedAssetPath);
|
|
|
|
var overridden = RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_PAK_PATH"] = "D:/prepared/acdream.pak" }));
|
|
Assert.Equal("D:/prepared/acdream.pak", overridden.PreparedAssetPath);
|
|
}
|
|
|
|
[Fact]
|
|
public void LiveMode_Set_ExactlyByValue1()
|
|
{
|
|
Assert.True(RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_LIVE"] = "1" })).LiveMode);
|
|
Assert.False(RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_LIVE"] = "0" })).LiveMode);
|
|
Assert.False(RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_LIVE"] = "true" })).LiveMode);
|
|
Assert.False(RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_LIVE"] = "" })).LiveMode);
|
|
}
|
|
|
|
[Fact]
|
|
public void LiveHostAndPort_FallBackToDefaults_WhenUnsetOrInvalid()
|
|
{
|
|
var withDefaults = RuntimeOptions.Parse(AnyDatDir, EmptyEnv());
|
|
Assert.Equal("127.0.0.1", withDefaults.LiveHost);
|
|
Assert.Equal(9000, withDefaults.LivePort);
|
|
|
|
var withOverrides = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_TEST_HOST"] = "play.example.com",
|
|
["ACDREAM_TEST_PORT"] = "9123",
|
|
}));
|
|
Assert.Equal("play.example.com", withOverrides.LiveHost);
|
|
Assert.Equal(9123, withOverrides.LivePort);
|
|
|
|
// Non-numeric port falls back to default; we don't throw at parse time.
|
|
var withBadPort = RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_TEST_PORT"] = "abc" }));
|
|
Assert.Equal(9000, withBadPort.LivePort);
|
|
}
|
|
|
|
[Fact]
|
|
public void LiveUserPass_NullWhenEmptyOrUnset()
|
|
{
|
|
var emptyValues = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_TEST_USER"] = "",
|
|
["ACDREAM_TEST_PASS"] = "",
|
|
}));
|
|
Assert.Null(emptyValues.LiveUser);
|
|
Assert.Null(emptyValues.LivePass);
|
|
|
|
var realValues = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_TEST_USER"] = "testaccount",
|
|
["ACDREAM_TEST_PASS"] = "testpassword",
|
|
}));
|
|
Assert.Equal("testaccount", realValues.LiveUser);
|
|
Assert.Equal("testpassword", realValues.LivePass);
|
|
}
|
|
|
|
[Fact]
|
|
public void HasLiveCredentials_RequiresLiveModeAndBothUserAndPass()
|
|
{
|
|
// Live mode off → no credentials regardless of user/pass.
|
|
var noLive = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_TEST_USER"] = "u",
|
|
["ACDREAM_TEST_PASS"] = "p",
|
|
}));
|
|
Assert.False(noLive.HasLiveCredentials);
|
|
|
|
// Live mode on but missing user → no credentials.
|
|
var missingUser = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_LIVE"] = "1",
|
|
["ACDREAM_TEST_PASS"] = "p",
|
|
}));
|
|
Assert.False(missingUser.HasLiveCredentials);
|
|
|
|
// Live mode on but missing pass → no credentials.
|
|
var missingPass = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_LIVE"] = "1",
|
|
["ACDREAM_TEST_USER"] = "u",
|
|
}));
|
|
Assert.False(missingPass.HasLiveCredentials);
|
|
|
|
// All three present → credentials available.
|
|
var ok = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_LIVE"] = "1",
|
|
["ACDREAM_TEST_USER"] = "u",
|
|
["ACDREAM_TEST_PASS"] = "p",
|
|
}));
|
|
Assert.True(ok.HasLiveCredentials);
|
|
}
|
|
|
|
[Fact]
|
|
public void HidePartIndex_MinusOneWhenUnset_ParsesIntegers()
|
|
{
|
|
Assert.Equal(-1, RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).HidePartIndex);
|
|
Assert.Equal(7, RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_HIDE_PART"] = "7" })).HidePartIndex);
|
|
// Invalid → fall back to -1 (preserves the int.TryParse failure semantics).
|
|
Assert.Equal(-1, RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_HIDE_PART"] = "abc" })).HidePartIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void RetailCloseDegrades_DefaultOn_ExceptWhenValueIsExactlyZero()
|
|
{
|
|
// Unset → on.
|
|
Assert.True(RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).RetailCloseDegrades);
|
|
|
|
// Exactly "0" → off. Matches the pre-refactor semantics:
|
|
// !string.Equals(env, "0", StringComparison.Ordinal)
|
|
Assert.False(RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_RETAIL_CLOSE_DEGRADES"] = "0",
|
|
})).RetailCloseDegrades);
|
|
|
|
// Any other value → on (including "1", "false", "True"). The original
|
|
// code only checked for the literal "0"; preserve that.
|
|
Assert.True(RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_RETAIL_CLOSE_DEGRADES"] = "1",
|
|
})).RetailCloseDegrades);
|
|
Assert.True(RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_RETAIL_CLOSE_DEGRADES"] = "false",
|
|
})).RetailCloseDegrades);
|
|
}
|
|
|
|
[Fact]
|
|
public void LegacyStreamRadius_NullWhenUnsetOrInvalid_ParsesNonNegativeIntegers()
|
|
{
|
|
Assert.Null(RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).LegacyStreamRadius);
|
|
Assert.Null(RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_STREAM_RADIUS"] = "abc" })).LegacyStreamRadius);
|
|
// Negative values are filtered out by the pre-refactor `sr >= 0` guard.
|
|
Assert.Null(RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_STREAM_RADIUS"] = "-3" })).LegacyStreamRadius);
|
|
|
|
Assert.Equal(0, RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_STREAM_RADIUS"] = "0" })).LegacyStreamRadius);
|
|
Assert.Equal(5, RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_STREAM_RADIUS"] = "5" })).LegacyStreamRadius);
|
|
Assert.Equal(12, RuntimeOptions.Parse(AnyDatDir, Env(new() { ["ACDREAM_STREAM_RADIUS"] = "12" })).LegacyStreamRadius);
|
|
}
|
|
|
|
[Fact]
|
|
public void FogMultipliers_ParseInvariantFloats_AndFallBackIndependently()
|
|
{
|
|
var parsed = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_FOG_START_MULT"] = "0.625",
|
|
["ACDREAM_FOG_END_MULT"] = "1.125",
|
|
}));
|
|
Assert.Equal(0.625f, parsed.FogStartMultiplier);
|
|
Assert.Equal(1.125f, parsed.FogEndMultiplier);
|
|
|
|
var invalid = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_FOG_START_MULT"] = "not-a-number",
|
|
["ACDREAM_FOG_END_MULT"] = "",
|
|
}));
|
|
Assert.Equal(0.7f, invalid.FogStartMultiplier);
|
|
Assert.Equal(0.95f, invalid.FogEndMultiplier);
|
|
}
|
|
|
|
[Fact]
|
|
public void DayGroupOverride_IsReadOnceIntoTypedOptions()
|
|
{
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
EmptyEnv()).ForcedDayGroupIndex);
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_DAY_GROUP"] = "-1" }))
|
|
.ForcedDayGroupIndex);
|
|
Assert.Equal(
|
|
7,
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_DAY_GROUP"] = "7" }))
|
|
.ForcedDayGroupIndex);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign V slice V7. Pins the Dereth day fraction -- the sun, the
|
|
/// keyframe, and every lit surface. Range-checked rather than merely parsed:
|
|
/// a day fraction outside [0, 1) is not a clamp candidate, it is a typo, and
|
|
/// silently pinning the world at 12.5 would be worse than ignoring it.
|
|
/// </summary>
|
|
[Fact]
|
|
public void WorldTimeOverride_IsReadOnceIntoTypedOptions()
|
|
{
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).PinnedWorldDayFraction);
|
|
Assert.Equal(
|
|
0.5f,
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_WORLD_TIME"] = "0.5" }))
|
|
.PinnedWorldDayFraction);
|
|
Assert.Equal(
|
|
0f,
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_WORLD_TIME"] = "0" }))
|
|
.PinnedWorldDayFraction);
|
|
foreach (string rejected in new[] { "1", "1.5", "-0.1", "midnight", "" })
|
|
{
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_WORLD_TIME"] = rejected }))
|
|
.PinnedWorldDayFraction);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign V slice V7. The sky has two clocks and this pins the one
|
|
/// ACDREAM_DAY_GROUP cannot reach — the cloud sheet's UV scroll, which
|
|
/// accumulates against real elapsed time. Unset must stay unset: every
|
|
/// ordinary run keeps the wall clock, and only the backend differential gate
|
|
/// asks for a pin.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SkyPhaseOverride_IsReadOnceIntoTypedOptions()
|
|
{
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).SkyAnimationPhaseSeconds);
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_SKY_PHASE_SECONDS"] = "not-a-number" }))
|
|
.SkyAnimationPhaseSeconds);
|
|
// Zero is a real request, not "unset": it is the cloud sheet's authored
|
|
// origin and the value the gate script pins by default.
|
|
Assert.Equal(
|
|
0f,
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_SKY_PHASE_SECONDS"] = "0" }))
|
|
.SkyAnimationPhaseSeconds);
|
|
Assert.Equal(
|
|
12.5f,
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_SKY_PHASE_SECONDS"] = "12.5" }))
|
|
.SkyAnimationPhaseSeconds);
|
|
}
|
|
|
|
[Fact]
|
|
public void DiagnosticFlags_RespectExactValueOne()
|
|
{
|
|
var allOn = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_DEVTOOLS"] = "1",
|
|
["ACDREAM_UNCAPPED_RENDER"] = "1",
|
|
["ACDREAM_DUMP_MOVE_TRUTH"] = "1",
|
|
["ACDREAM_DUMP_SKY"] = "1",
|
|
["ACDREAM_NO_AUDIO"] = "1",
|
|
["ACDREAM_ENABLE_SKY_PES"] = "1",
|
|
["ACDREAM_DUMP_SCENERY_Z"] = "1",
|
|
["ACDREAM_DUMP_CLOTHING"] = "1",
|
|
}));
|
|
Assert.True(allOn.DevTools);
|
|
Assert.True(allOn.UncappedRendering);
|
|
Assert.True(allOn.DumpMoveTruth);
|
|
Assert.True(allOn.DumpSky);
|
|
Assert.True(allOn.NoAudio);
|
|
Assert.True(allOn.EnableSkyPesDebug);
|
|
Assert.True(allOn.DumpSceneryZ);
|
|
Assert.True(allOn.DumpClothing);
|
|
|
|
// Any non-"1" value leaves them off, matching the
|
|
// string.Equals(env, "1", StringComparison.Ordinal) check.
|
|
var anyOther = RuntimeOptions.Parse(AnyDatDir, Env(new()
|
|
{
|
|
["ACDREAM_DEVTOOLS"] = "true",
|
|
["ACDREAM_UNCAPPED_RENDER"] = "true",
|
|
["ACDREAM_DUMP_MOVE_TRUTH"] = "yes",
|
|
["ACDREAM_NO_AUDIO"] = "2",
|
|
["ACDREAM_ENABLE_SKY_PES"] = "on",
|
|
["ACDREAM_DUMP_SCENERY_Z"] = " 1",
|
|
["ACDREAM_DUMP_CLOTHING"] = "true",
|
|
}));
|
|
Assert.False(anyOther.DevTools);
|
|
Assert.False(anyOther.UncappedRendering);
|
|
Assert.False(anyOther.DumpMoveTruth);
|
|
Assert.False(anyOther.NoAudio);
|
|
Assert.False(anyOther.EnableSkyPesDebug);
|
|
Assert.False(anyOther.DumpSceneryZ);
|
|
Assert.False(anyOther.DumpClothing);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_RejectsNullDatDirOrEnv()
|
|
{
|
|
Assert.Throws<ArgumentNullException>(() => RuntimeOptions.Parse(null!, EmptyEnv()));
|
|
Assert.Throws<ArgumentNullException>(() => RuntimeOptions.Parse(AnyDatDir, null!));
|
|
}
|
|
|
|
[Fact]
|
|
public void VulkanDeviceOverride_IsNullWhenUnsetOrEmpty()
|
|
{
|
|
Assert.Null(RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).VulkanDeviceOverride);
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_VULKAN_DEVICE"] = "" })).VulkanDeviceOverride);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The override is carried verbatim — index or name substring — because the
|
|
/// capability report records exactly what the operator asked for, matched or
|
|
/// not.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("1")]
|
|
[InlineData("Radeon")]
|
|
public void VulkanDeviceOverride_IsCarriedVerbatim(string value)
|
|
{
|
|
Assert.Equal(
|
|
value,
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_VULKAN_DEVICE"] = value })).VulkanDeviceOverride);
|
|
}
|
|
|
|
[Fact]
|
|
public void VulkanForcedUnsupportedFeature_DrivesTheExitFourGateKnob()
|
|
{
|
|
Assert.Null(
|
|
RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).VulkanForcedUnsupportedFeature);
|
|
Assert.Equal(
|
|
"timelineSemaphore",
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_VULKAN_FORCE_UNSUPPORTED"] = "timelineSemaphore" }))
|
|
.VulkanForcedUnsupportedFeature);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign V slice V9. The frame budget is what lets the probe harness run
|
|
/// unattended in CI. Zero is the interactive default, so every invocation
|
|
/// that predates the slice keeps presenting until its window closes.
|
|
/// </summary>
|
|
[Fact]
|
|
public void VulkanCapabilityProbeFrames_DefaultsToUnbounded()
|
|
{
|
|
Assert.Equal(
|
|
0,
|
|
RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).VulkanCapabilityProbeFrames);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("1", 1)]
|
|
[InlineData("30", 30)]
|
|
[InlineData("0", 0)]
|
|
public void VulkanCapabilityProbeFrames_ParsesANonNegativeBudget(
|
|
string value,
|
|
int expected)
|
|
{
|
|
Assert.Equal(
|
|
expected,
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_VULKAN_PROBE_FRAMES"] = value }))
|
|
.VulkanCapabilityProbeFrames);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A malformed budget falls back to the interactive default rather than to
|
|
/// some invented number: a CI step that meant to bound the run and mistyped
|
|
/// it should hang and be noticed, not silently capture at a frame count
|
|
/// nobody asked for.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("")]
|
|
[InlineData("-1")]
|
|
[InlineData("many")]
|
|
[InlineData("30.5")]
|
|
public void VulkanCapabilityProbeFrames_RejectsMalformedValues(string value)
|
|
{
|
|
Assert.Equal(
|
|
0,
|
|
RuntimeOptions.Parse(
|
|
AnyDatDir,
|
|
Env(new() { ["ACDREAM_VULKAN_PROBE_FRAMES"] = value }))
|
|
.VulkanCapabilityProbeFrames);
|
|
}
|
|
}
|