acdream/tests/AcDream.App.Tests/RuntimeOptionsTests.cs
Erik e8a4c1af3f feat(render): Campaign V slice V5 - Vulkan bring-up, dark
Instance, physical-device selection, logical device, queues, swapchain, and the
three-layer capability gate, behind ACDREAM_RENDER_BACKEND=vulkan. Nothing of
the game renders through it. OpenGL stays the default and the only live backend
until V10, and with the variable unset or set to gl the GL path executes not one
new statement.

The shape of the slice. Plan §4.11 asks the Vulkan gate to mirror the GL one
exactly - passive record, active probes, an Evaluate producing operator-facing
sentences, NotSupportedException into Program.cs's exit-code-4 contract, and an
atomic JSON report. The harder question was where to put the seam, because a
capability gate is precisely the code you cannot exercise on the machine that
already passes it: this box has one discrete GPU, so device ranking, the split-
queue path, an sRGB-only surface, a minimised window and a device missing
descriptorBindingVariableDescriptorCount are all unreachable by running the
client. So every decision the gate makes is a pure function over plain records,
and the Silk interop layer only has to be right about which Vulkan field feeds
which property. VulkanPhysicalDeviceSelection ranks candidates,
VulkanExtensionSelection does the required-versus-optional set arithmetic,
VulkanSwapchainConfigurationFactory chooses format, present mode, image count,
extent, usage, transform and composite alpha, VulkanSwapchainRecreationPolicy
classifies every acquire and present result, and
VulkanCapabilityRequirements.Evaluate turns a captured record into failure
sentences. All of it is unit-tested with no driver, no device and no window.

This commit is the integration of that work onto the post-revert tree. The V5
branch was written on b064668b, before V4c/V4d were reverted, so GameWindow.cs
had to be merged rather than taken: the file here is eb2ba4e5's GameWindow plus
V5's fifteen-line backend branch, and it keeps _terrainModernShader, which the
revert restored and which the V5 branch never had. Every other file is byte-
identical to the branch - git diff e1ef4313 over Rendering/Gpu/Vk,
tests/.../Gpu/Vk and RenderBackendKind.cs is empty, no BOM was introduced, and
CRLF is uniform across all seventeen files.

Gate results, recorded verbatim.

Release build: succeeded, 0 warnings, 0 errors.

App tests, Release: Failed 0, Passed 3981, Skipped 3, Total 3984 - the 3,866
baseline plus V5's 115 new tests, exactly.

Offline pixel gate against eb2ba4e5: PASS world-offline.png, differing fraction
1.06534090909091E-05, which is 6 differing pixels out of the 563,200 compared
after the top 280 sky rows are masked. §5.1's re-measured same-commit control
band is 15-23 pixels at fraction <= 4.1e-05, so this sits below the noise floor
rather than merely inside it - the expected result for a slice that adds no
statement to the GL path.

Vulkan check (a), ACDREAM_RENDER_BACKEND=vulkan on the RX 9070 XT with an
automation artifact directory:

  vulkan: capability gate passed (Windows, AMD Radeon RX 9070 XT, Vulkan
  1.4.349, vendor 0x1002, device 0x7550, driver 2.0.395 (raw 0x0080018B));
  swapchain B8G8R8A8Unorm/PresentModeImmediateKhr 1280x720 x3
  vulkan: device selection - automatic: 'AMD Radeon RX 9070 XT' (DiscreteGpu,
  15.92 GiB device-local) ranked first of 2 enumerated device(s).
  [world-gate] screenshot-complete name=vulkan-bringup path=...
  artifacts\vk-bringup\vulkan-bringup.png size=1280x720
  vulkan: presented 64609 clear-colour frame(s); shutting down.

Exit code 0 on CloseMainWindow. The PNG is 5,238 bytes, 1280x720, and uniformly
RGBA(11,19,39,255) - exactly ClearColor [0.043, 0.075, 0.153, 1] scaled to
UNORM. Orientation is right-side-up by construction rather than by inspection,
which a uniform clear could not show: VulkanBackbufferSwizzle.ToGlOriginRgba
writes source row y into destination row height-1-y precisely because
FrameScreenshotController flips again on the way to the PNG, so the two
cancel. That double-flip is unit-tested.

Vulkan check (b), ACDREAM_VULKAN_FORCE_UNSUPPORTED=timelineSemaphore:

  [ERR] acdream's Vulkan renderer is unsupported by the selected device.
  Platform: win-x64, Windows, AMD Radeon RX 9070 XT (DiscreteGpu), Vulkan
  1.4.349, vendor 0x1002, device 0x7550, driver 2.0.395 (raw 0x0080018B)
   - timelineSemaphore is required; the frame serial is the semaphore value.
  Full capability report: ...\diagnostics\graphical-capabilities-vulkan.json

Exit code 4. The report records ForcedUnsupportedFeature timelineSemaphore,
TimelineSemaphore false against an otherwise complete feature set, and the
matching SupportFailures sentence, so the injected rejection is distinguishable
from a genuinely absent feature. Both enumerated devices, all five surface
formats, all four present modes and a clean FunctionProbe with no failures are
recorded beside it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 07:11:50 +02:00

493 lines
18 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);
}
[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!));
}
/// <summary>
/// Campaign V slice V5. OpenGL is the shipping backend until slice V10, so
/// the default must be GL and a typo must never silently start the dark
/// Vulkan host.
/// </summary>
[Fact]
public void RenderBackend_DefaultsToGl()
{
Assert.Equal(
RenderBackendKind.Gl,
RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).RenderBackend);
}
[Theory]
[InlineData("vulkan")]
[InlineData("Vulkan")]
[InlineData("VULKAN")]
public void RenderBackend_SelectsVulkanCaseInsensitively(string value)
{
Assert.Equal(
RenderBackendKind.Vulkan,
RuntimeOptions.Parse(
AnyDatDir,
Env(new() { ["ACDREAM_RENDER_BACKEND"] = value })).RenderBackend);
}
[Theory]
[InlineData("gl")]
[InlineData("opengl")]
[InlineData("")]
[InlineData("vulcan")]
[InlineData("vk")]
[InlineData(" vulkan")]
public void RenderBackend_AnythingElseStaysOnGl(string value)
{
Assert.Equal(
RenderBackendKind.Gl,
RuntimeOptions.Parse(
AnyDatDir,
Env(new() { ["ACDREAM_RENDER_BACKEND"] = value })).RenderBackend);
}
[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);
}
}