acdream/tests/AcDream.App.Tests/Rendering/Packs/RenderPackControllerTests.cs
Erik 132395e6f7 fix #425: pack resident budget is a 1080p ceiling that scales with pixel count; an explicit Apply retries a failed selection
Live Holtburg at 2560x1440: the Low preset needed 67,368,164 resident bytes
(screen-sized HDR/depth/ray targets are 44 MB of that) against an absolute
64 MiB ceiling that had only been validated at 1080p, so Options -> Apply
fell back to the default path; every later Apply was then refused by the
controller's failure memo, which treated the user's deliberate choice like
automatic re-activation.

RenderPackResidentBudget.Effective scales the declared 1080p figure by the
viewport's pixel-count ratio (never below 1), still capped by the hardware
MaxPackResidentBytes; both pack graphs use it and the performance-matrix
tool judges its resident column by the same rule (contract test updated).
RenderPackController.Request gains explicitUserChoice, which clears the
memo for that selection; RenderPackSelectionBinding passes it on every
display edge (Apply, including resolution changes) and keeps the memo for
the startup request.

Tests: RenderPackResidentBudgetTests (1080p/720p keep the declared
ceiling, 1440p = 16/9x, 4K = 4x, hardware cap wins, zero extent rejected);
controller explicit-retry; the binding test now proves the user's next
Apply activates once the cause is gone. App hermetic lane 6,068/0 (Release).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 10:00:11 +02:00

1226 lines
48 KiB
C#

using System.Numerics;
using AcDream.App.Plugins;
using AcDream.App.Tests.Rendering.Gpu;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Vk;
using AcDream.App.Rendering.Packs;
using AcDream.App.Settings;
using AcDream.Plugin.Abstractions.Rendering;
using AcDream.UI.Abstractions.Panels.Settings;
using Silk.NET.Vulkan;
namespace AcDream.App.Tests.Rendering.Packs;
public sealed class RenderPackControllerTests
{
private static RenderPackActivationExtent Extent => new(1280, 720, 4);
[Fact]
public void Discovery_buffers_descriptor_without_opening_assets()
{
var assets = new StubAssets();
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(Descriptor(), assets);
IReadOnlyList<BufferedRenderPackRegistration> snapshot = registry.Snapshot();
Assert.Single(snapshot);
Assert.Equal("test.pack", snapshot[0].Descriptor.Id);
Assert.Equal(0, assets.OpenCount);
}
[Fact]
public void Withdrawing_registration_removes_catalog_entry()
{
using var registry = new BufferedRenderPackRegistry();
IDisposable registration = registry.Register(Descriptor(), new StubAssets());
Assert.Single(registry.Snapshot());
registration.Dispose();
Assert.Empty(registry.Snapshot());
}
[Fact]
public void Discovery_rejects_a_missing_user_facing_feature_summary()
{
RenderPackValidationResult result = RenderPackValidator.ValidateDescriptor(
Descriptor() with { FeatureSummary = " " },
RenderPackHostCapabilities.Conformance);
Assert.False(result.Success);
Assert.Equal("Pack 'test.pack' has no feature summary.", result.Reason);
}
[Fact]
public void UnifiedShadowSubmissionHintRequiresTheLowDirectionalShadowGraph()
{
RenderQualityPreset low = Preset() with
{
Semantic = RenderQualitySemantic.Low,
ExecutionHints =
RenderQualityExecutionHints.MultiviewDirectionalShadowCascades,
};
RenderPackValidationResult result = RenderPackValidator.ValidateDescriptor(
Descriptor() with { QualityPresets = [low] },
RenderPackHostCapabilities.Conformance);
Assert.False(result.Success);
Assert.Contains(
"without the directional-shadow graph",
result.Reason,
StringComparison.Ordinal);
}
[Fact]
public void Retail_request_builds_nothing_and_keeps_no_runtime()
{
using var registry = new BufferedRenderPackRegistry();
var factory = new StubFactory();
using var controller = Controller(registry, factory);
controller.Request(RenderPackSelectionSettings.Retail);
RenderPackActivationSnapshot snapshot = controller.ApplyAtFrameBoundary(Extent);
Assert.Equal(RenderPackActivationState.Retail, snapshot.State);
Assert.Null(controller.ActiveRuntime);
Assert.Equal(0, factory.BuildCount);
}
[Fact]
public void Selected_noop_pack_validates_assets_then_activates_atomically()
{
var assets = new StubAssets();
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(Descriptor(), assets);
var factory = new StubFactory();
using var controller = Controller(registry, factory);
controller.Request(Selection());
RenderPackActivationSnapshot snapshot = controller.ApplyAtFrameBoundary(Extent);
Assert.Equal(RenderPackActivationState.Active, snapshot.State);
Assert.NotNull(controller.ActiveRuntime);
Assert.Equal(1, factory.BuildCount);
// The conformance descriptor has no shaders, so selected validation
// still has no asset to open.
Assert.Equal(0, assets.OpenCount);
}
[Fact]
public void PipelineCreationConsumesTheSingleImmutableValidatedAssetSnapshot()
{
const string vertexKey = "stateful.vert.spv";
const string fragmentKey = "stateful.frag.spv";
string shaderDirectory = Path.Combine(
RepositoryRoot(), "src", "AcDream.App", "Rendering", "Shaders", "spv");
byte[] expectedVertex = File.ReadAllBytes(
Path.Combine(shaderDirectory, "atmospheric_filmic.vert.spv"));
byte[] expectedFragment = File.ReadAllBytes(
Path.Combine(shaderDirectory, "atmospheric_filmic.frag.spv"));
var assets = new StatefulAssets(new Dictionary<string, byte[]>
{
[vertexKey] = expectedVertex,
[fragmentKey] = expectedFragment,
});
RenderPackDescriptor descriptor = Descriptor() with
{
Id = "stateful.pack",
RequiredCapabilities =
[
RenderCapability.MainWorldColorIntermediate,
RenderCapability.FullscreenPasses,
],
Passes =
[
new RenderPassDeclaration(
"filmic",
RenderPassHook.ToneMap,
vertexKey,
fragmentKey,
[RenderSemanticInput.WorldColor, RenderSemanticInput.FrameTime],
[],
[]),
],
};
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(descriptor, assets);
var device = new RecordingGpuDevice();
var factory = new AtmosphericRenderPackRuntimeFactory(device);
using var controller = new RenderPackController(
() => RenderPackCatalog.Build(
registry.Snapshot(),
RenderPackHostCapabilities.Conformance),
factory,
preparationScheduler: InlineRenderPackPreparationScheduler.Instance);
controller.Request(new RenderPackSelectionSettings(
descriptor.Id,
descriptor.PackVersion.ToString(),
"low"));
RenderPackActivationSnapshot snapshot = controller.ApplyAtFrameBoundary(Extent);
Assert.Equal(RenderPackActivationState.Active, snapshot.State);
Assert.Equal(1, assets.OpenCount(vertexKey));
Assert.Equal(1, assets.OpenCount(fragmentKey));
RecordingGpuPipeline pipeline = Assert.Single(device.CreatedPipelines);
Assert.Equal(expectedVertex, pipeline.Description.Shaders.VertexSpirv.ToArray());
Assert.Equal(expectedFragment, pipeline.Description.Shaders.FragmentSpirv.ToArray());
}
[Fact]
public void Valid_user_setting_overrides_are_forwarded_by_stable_id()
{
RenderPackDescriptor descriptor = Descriptor() with
{
Settings = Settings(),
};
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(descriptor, new StubAssets());
var factory = new StubFactory();
using var controller = Controller(registry, factory);
RenderPackSettingOverrides overrides = new Dictionary<string, string>
{
["enabled"] = "true",
["exposure"] = "1.25",
["samples"] = "4",
["quality"] = "high",
}.ToRenderPackOverrides();
controller.Request(Selection() with { SettingOverrides = overrides });
RenderPackActivationSnapshot snapshot = controller.ApplyAtFrameBoundary(Extent);
Assert.Equal(RenderPackActivationState.Active, snapshot.State);
Assert.Equal(overrides, factory.LastUserSettingOverrides);
}
[Fact]
public void Unknown_user_setting_override_atomically_retires_to_retail()
{
RenderPackDescriptor descriptor = Descriptor() with { Settings = Settings() };
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(descriptor, new StubAssets());
var factory = new StubFactory();
using var controller = Controller(registry, factory);
controller.Request(Selection());
controller.ApplyAtFrameBoundary(Extent);
StubRuntime active = Assert.IsType<StubRuntime>(controller.ActiveRuntime);
controller.Request(Selection() with
{
SettingOverrides = new RenderPackSettingOverrides(
new Dictionary<string, string> { ["removed-setting"] = "1" }),
});
RenderPackActivationSnapshot snapshot = controller.ApplyAtFrameBoundary(Extent);
Assert.Equal(RenderPackActivationState.FailedToRetail, snapshot.State);
Assert.Equal(
"Render pack 'test.pack' has a user override for unknown setting 'removed-setting'.",
snapshot.Reason);
Assert.True(active.Disposed);
Assert.Null(controller.ActiveRuntime);
Assert.Equal(1, factory.BuildCount);
}
[Theory]
[InlineData("enabled", "yes", "Boolean")]
[InlineData("exposure", "1,25", "Float")]
[InlineData("exposure", "1.1", "Float")]
[InlineData("samples", "3", "Integer")]
[InlineData("samples", "12", "Integer")]
[InlineData("quality", "ultra", "Choice")]
public void Invalid_user_setting_value_fails_to_retail_without_building(
string settingId,
string value,
string kind)
{
RenderPackDescriptor descriptor = Descriptor() with { Settings = Settings() };
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(descriptor, new StubAssets());
var factory = new StubFactory();
using var controller = Controller(registry, factory);
controller.Request(Selection() with
{
SettingOverrides = new RenderPackSettingOverrides(
new Dictionary<string, string> { [settingId] = value }),
});
RenderPackActivationSnapshot snapshot = controller.ApplyAtFrameBoundary(Extent);
Assert.Equal(RenderPackActivationState.FailedToRetail, snapshot.State);
Assert.Equal(
$"Render pack 'test.pack' user override '{settingId}' has invalid {kind} value '{value}'.",
snapshot.Reason);
Assert.Null(controller.ActiveRuntime);
Assert.Equal(0, factory.BuildCount);
}
[Fact]
public void Invalid_spirv_fails_complete_pack_to_retail_and_does_not_retry()
{
RenderPackDescriptor descriptor = Descriptor() with
{
Passes =
[
new RenderPassDeclaration(
"tone-map",
RenderPassHook.ToneMap,
"shaders/fullscreen.vert.spv",
"shaders/tone-map.frag.spv",
[RenderSemanticInput.WorldColor],
[],
[]),
],
};
var assets = new StubAssets([1, 2, 3, 4]);
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(descriptor, assets);
var factory = new StubFactory();
using var controller = Controller(registry, factory);
controller.Request(Selection());
RenderPackActivationSnapshot first = controller.ApplyAtFrameBoundary(Extent);
controller.Request(Selection());
RenderPackActivationSnapshot second = controller.ApplyAtFrameBoundary(Extent);
Assert.Equal(RenderPackActivationState.FailedToRetail, first.State);
Assert.Contains("not valid SPIR-V", first.Reason, StringComparison.Ordinal);
Assert.Equal(RenderPackActivationState.FailedToRetail, second.State);
Assert.Contains("will not be retried", second.Reason, StringComparison.Ordinal);
Assert.Equal(0, factory.BuildCount);
Assert.Equal(1, assets.OpenCount);
}
[Fact]
public void An_explicit_user_request_retries_a_selection_that_failed_earlier()
{
// #425 (Campaign VM VM7 owner gate): a preset that failed its resident
// budget once locked the user out of the pack until restart, because
// the Options panel's Apply went through the same memo as automatic
// re-activation. An explicit choice is a fresh attempt — here the
// second attempt opens the assets again and fails on its own merits
// (still invalid SPIR-V), not with the "will not be retried" memo.
RenderPackDescriptor descriptor = Descriptor() with
{
Passes =
[
new RenderPassDeclaration(
"tone-map",
RenderPassHook.ToneMap,
"shaders/fullscreen.vert.spv",
"shaders/tone-map.frag.spv",
[RenderSemanticInput.WorldColor],
[],
[]),
],
};
var assets = new StubAssets([1, 2, 3, 4]);
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(descriptor, assets);
var factory = new StubFactory();
using var controller = Controller(registry, factory);
controller.Request(Selection());
RenderPackActivationSnapshot first = controller.ApplyAtFrameBoundary(Extent);
controller.Request(Selection(), explicitUserChoice: true);
RenderPackActivationSnapshot second = controller.ApplyAtFrameBoundary(Extent);
Assert.Equal(RenderPackActivationState.FailedToRetail, first.State);
Assert.Equal(RenderPackActivationState.FailedToRetail, second.State);
Assert.Contains("not valid SPIR-V", second.Reason, StringComparison.Ordinal);
Assert.DoesNotContain("will not be retried", second.Reason, StringComparison.Ordinal);
Assert.Equal(2, assets.OpenCount);
}
[Fact]
public void Arbitrary_plugin_asset_exception_is_contained_as_a_retail_fallback()
{
RenderPackDescriptor descriptor = Descriptor() with
{
Passes =
[
new RenderPassDeclaration(
"tone-map",
RenderPassHook.ToneMap,
"fullscreen.vert.spv",
"tone-map.frag.spv",
[],
[],
[]),
],
};
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(
descriptor,
new ThrowingAssets(new InvalidOperationException("plugin stream failed")));
var factory = new StubFactory();
using var controller = Controller(registry, factory);
controller.Request(Selection());
RenderPackActivationSnapshot snapshot = controller.ApplyAtFrameBoundary(Extent);
Assert.Equal(RenderPackActivationState.FailedToRetail, snapshot.State);
Assert.Contains("plugin stream failed", snapshot.Reason, StringComparison.Ordinal);
Assert.Null(controller.ActiveRuntime);
Assert.Equal(0, factory.BuildCount);
}
[Fact]
public void Candidate_build_failure_disposes_old_runtime_and_returns_to_retail()
{
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(Descriptor(), new StubAssets());
var factory = new StubFactory();
using var controller = Controller(registry, factory);
controller.Request(Selection());
controller.ApplyAtFrameBoundary(Extent);
StubRuntime first = Assert.IsType<StubRuntime>(controller.ActiveRuntime);
factory.Failure = new InvalidOperationException("pipeline rejected");
controller.Request(Selection() with { PresetId = "medium" });
RenderPackActivationSnapshot failed = controller.ApplyAtFrameBoundary(Extent);
Assert.Equal(RenderPackActivationState.FailedToRetail, failed.State);
Assert.Contains("pipeline rejected", failed.Reason, StringComparison.Ordinal);
Assert.True(first.Disposed);
Assert.Null(controller.ActiveRuntime);
}
[Theory]
[InlineData(Result.ErrorDeviceLost)]
[InlineData(Result.ErrorOutOfHostMemory)]
[InlineData(Result.ErrorOutOfDeviceMemory)]
public void FatalVulkanCandidatePreparationDisposesCandidateAndRethrows(Result result)
{
RenderPackDescriptor descriptor = Descriptor();
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(descriptor, new StubAssets());
var failure = new VulkanCallException("candidate target", result);
var runtime = new ThrowingGraphRuntime(descriptor, Preset(), failure);
var factory = new StubFactory
{
RuntimeFactory = (_, _) => runtime,
};
using var controller = Controller(registry, factory);
controller.Request(Selection());
VulkanCallException thrown = Assert.Throws<VulkanCallException>(
() => controller.ApplyAtFrameBoundary(Extent));
Assert.Same(failure, thrown);
Assert.True(runtime.Disposed);
Assert.Null(controller.ActiveRuntime);
}
[Fact]
public void NonTerminalVulkanCandidateFailureDisposesCandidateAndFallsBack()
{
RenderPackDescriptor descriptor = Descriptor();
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(descriptor, new StubAssets());
var runtime = new ThrowingGraphRuntime(
descriptor,
Preset(),
new VulkanCallException("candidate target", Result.ErrorFormatNotSupported));
var factory = new StubFactory
{
RuntimeFactory = (_, _) => runtime,
};
using var controller = Controller(registry, factory);
controller.Request(Selection());
RenderPackActivationSnapshot snapshot = controller.ApplyAtFrameBoundary(Extent);
Assert.Equal(RenderPackActivationState.FailedToRetail, snapshot.State);
Assert.Contains("ErrorFormatNotSupported", snapshot.Reason, StringComparison.Ordinal);
Assert.True(runtime.Disposed);
Assert.Null(controller.ActiveRuntime);
}
[Fact]
public void Active_pack_remains_renderable_until_completed_candidate_publishes_at_boundary()
{
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(Descriptor(), new StubAssets());
var factory = new StubFactory();
var scheduler = new ControlledPreparationScheduler();
using var controller = new RenderPackController(
() => RenderPackCatalog.Build(
registry.Snapshot(),
RenderPackHostCapabilities.Conformance),
factory,
preparationScheduler: scheduler);
controller.Request(Selection());
RenderPackActivationSnapshot initialPending =
controller.ApplyAtFrameBoundary(Extent);
Assert.Equal(RenderPackActivationState.CandidatePending, initialPending.State);
Assert.Null(controller.ActiveRuntime);
scheduler.CompleteNext();
Assert.Null(controller.ActiveRuntime);
controller.ApplyAtFrameBoundary(Extent);
StubRuntime first = Assert.IsType<StubRuntime>(controller.ActiveRuntime);
controller.Request(Selection() with { PresetId = "medium" });
RenderPackActivationSnapshot replacementPending =
controller.ApplyAtFrameBoundary(Extent);
Assert.Equal(RenderPackActivationState.CandidatePending, replacementPending.State);
Assert.Same(first, controller.ActiveRuntime);
Assert.False(first.Disposed);
scheduler.CompleteNext();
// Worker completion owns no publication authority.
Assert.Same(first, controller.ActiveRuntime);
Assert.False(first.Disposed);
RenderPackActivationSnapshot published = controller.ApplyAtFrameBoundary(Extent);
Assert.Equal(RenderPackActivationState.Active, published.State);
StubRuntime second = Assert.IsType<StubRuntime>(controller.ActiveRuntime);
Assert.NotSame(first, second);
Assert.Equal("medium", second.Preset.Id);
Assert.True(first.Disposed);
}
[Fact]
public void Preparation_failure_is_diagnostic_and_only_falls_back_at_a_boundary()
{
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(Descriptor(), new StubAssets());
var factory = new StubFactory();
var scheduler = new ControlledPreparationScheduler();
using var controller = new RenderPackController(
() => RenderPackCatalog.Build(
registry.Snapshot(),
RenderPackHostCapabilities.Conformance),
factory,
preparationScheduler: scheduler);
controller.Request(Selection());
controller.ApplyAtFrameBoundary(Extent);
scheduler.CompleteNext();
controller.ApplyAtFrameBoundary(Extent);
StubRuntime first = Assert.IsType<StubRuntime>(controller.ActiveRuntime);
factory.Failure = new InvalidOperationException("background pipeline rejected");
controller.Request(Selection() with { PresetId = "medium" });
controller.ApplyAtFrameBoundary(Extent);
scheduler.CompleteNext();
Assert.Same(first, controller.ActiveRuntime);
Assert.False(first.Disposed);
Assert.Equal(RenderPackActivationState.CandidatePending, controller.Snapshot.State);
RenderPackActivationSnapshot failed = controller.ApplyAtFrameBoundary(Extent);
Assert.Equal(RenderPackActivationState.FailedToRetail, failed.State);
Assert.Contains("could not be prepared", failed.Reason, StringComparison.Ordinal);
Assert.Contains("background pipeline rejected", failed.Reason, StringComparison.Ordinal);
Assert.True(first.Disposed);
Assert.Null(controller.ActiveRuntime);
}
[Fact]
public void Descriptor_validation_reports_exact_missing_capability()
{
RenderPackDescriptor descriptor = Descriptor() with
{
RequiredCapabilities = [RenderCapability.DirectionalShadowMaps],
};
var capabilities = new RenderPackHostCapabilities(
new HashSet<RenderCapability>(),
4096,
4,
64L * 1024 * 1024);
RenderPackValidationResult result =
RenderPackValidator.ValidateDescriptor(descriptor, capabilities);
Assert.False(result.Success);
Assert.Equal(
"Pack 'test.pack' requires unsupported capability 'DirectionalShadowMaps'.",
result.Reason);
}
[Fact]
public void Malformed_null_preset_list_remains_a_precisely_incompatible_catalog_entry()
{
RenderPackDescriptor malformed = Descriptor() with
{
QualityPresets = null!,
};
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(malformed, new StubAssets());
RenderPackCatalog catalog = RenderPackCatalog.Build(
registry.Snapshot(),
RenderPackHostCapabilities.Conformance);
RenderPackCatalogEntry entry = Assert.Single(catalog.Entries);
Assert.False(entry.IsCompatible);
Assert.Equal(
"Pack 'test.pack' has a null quality-preset declaration list.",
entry.IncompatibilityReason);
Assert.Empty(entry.PresetIncompatibilityReasons);
}
[Fact]
public void Diagnostics_record_retail_failure_and_active_pack_ownership_without_gpu_waits()
{
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(Descriptor(), new StubAssets());
var factory = new StubFactory();
using var controller = Controller(registry, factory);
RenderPackDiagnosticsSnapshot retail = controller.CaptureDiagnostics();
controller.Request(Selection());
controller.ApplyAtFrameBoundary(Extent);
RenderPackDiagnosticsSnapshot active = controller.CaptureDiagnostics();
Assert.True(retail.IsRetail);
Assert.Equal(RenderPackActivationState.Retail, retail.State);
Assert.Equal("test.pack", active.PackId);
Assert.Equal("low", active.PresetId);
Assert.Equal("low", active.EffectiveQuality);
Assert.Equal(RenderPackActivationState.Active, active.State);
Assert.Contains("pack=test.pack@1.0.0", RenderPackDiagnosticsFormatter.Format(active));
}
[Theory]
[InlineData(
AuthoredCelestialShadowSourceKind.Sun,
5,
0x01001348u,
0.25f,
-0.5f,
0.8291562f,
0.8291562f,
"shadowSource=Sun/obj5/0x01001348/dir(0.2500,-0.5000,0.8292)/elevSin=0.8292")]
[InlineData(
AuthoredCelestialShadowSourceKind.DominantMoon,
3,
0x01001F6Au,
-0.6f,
0.2f,
0.7745967f,
0.7745967f,
"shadowSource=DominantMoon/obj3/0x01001F6A/dir(-0.6000,0.2000,0.7746)/elevSin=0.7746")]
[InlineData(
AuthoredCelestialShadowSourceKind.SecondaryMoon,
2,
0x01001F67u,
0.4f,
0.8f,
0.4472136f,
0.4472136f,
"shadowSource=SecondaryMoon/obj2/0x01001F67/dir(0.4000,0.8000,0.4472)/elevSin=0.4472")]
[InlineData(
AuthoredCelestialShadowSourceKind.None,
-1,
0u,
0f,
0f,
1f,
0f,
"shadowSource=None/obj-1/0x00000000/dir(0.0000,0.0000,1.0000)/elevSin=0.0000")]
internal void DiagnosticsPropagateSunMoonAndNoneMetadataToStableFormattedOutput(
AuthoredCelestialShadowSourceKind sourceKind,
int sourceObjectIndex,
uint sourceGfxObjId,
float directionX,
float directionY,
float directionZ,
float elevationSin,
string expectedFormattedSource)
{
var direction = new Vector3(directionX, directionY, directionZ);
RenderPackRuntimeDiagnostics runtimeDiagnostics =
RenderPackRuntimeDiagnostics.Empty("low") with
{
DirectionalShadowSourceKind = sourceKind,
DirectionalShadowSourceObjectIndex = sourceObjectIndex,
DirectionalShadowSourceGfxObjId = sourceGfxObjId,
DirectionalShadowSurfaceToLightDirection = direction,
DirectionalShadowLightElevationSin = elevationSin,
};
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(
Descriptor(),
new StubAssets());
var factory = new StubFactory
{
RuntimeFactory = (descriptor, preset) =>
new DiagnosticStubRuntime(
descriptor,
preset,
runtimeDiagnostics),
};
using var controller = Controller(registry, factory);
controller.Request(Selection());
controller.ApplyAtFrameBoundary(Extent);
RenderPackDiagnosticsSnapshot snapshot = controller.CaptureDiagnostics();
string formatted = RenderPackDiagnosticsFormatter.Format(snapshot);
Assert.Equal(sourceKind, snapshot.DirectionalShadowSourceKind);
Assert.Equal(
sourceObjectIndex,
snapshot.DirectionalShadowSourceObjectIndex);
Assert.Equal(sourceGfxObjId, snapshot.DirectionalShadowSourceGfxObjId);
Assert.Equal(
direction,
snapshot.DirectionalShadowSurfaceToLightDirection);
Assert.Equal(elevationSin, snapshot.DirectionalShadowLightElevationSin);
Assert.Contains(expectedFormattedSource, formatted, StringComparison.Ordinal);
}
[Fact]
public void Deferred_diagnostics_are_retail_before_bind_and_after_owner_release()
{
var deferred = new DeferredRenderPackDiagnosticsSource();
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(Descriptor(), new StubAssets());
var factory = new StubFactory();
using var controller = Controller(registry, factory);
Assert.True(deferred.CaptureDiagnostics().IsRetail);
using (deferred.BindOwned(controller))
{
controller.Request(Selection());
controller.ApplyAtFrameBoundary(Extent);
Assert.Equal("test.pack", deferred.CaptureDiagnostics().PackId);
}
Assert.True(deferred.CaptureDiagnostics().IsRetail);
}
[Fact]
public void Selection_binding_activates_only_at_boundary_and_persists_failed_fallback()
{
var storage = new SelectionStorage();
var settings = new RuntimeSettingsController(storage, log: static _ => { });
using var registry = new BufferedRenderPackRegistry();
using IDisposable registration = registry.Register(Descriptor(), new StubAssets());
var factory = new StubFactory();
using var controller = Controller(registry, factory);
using var binding = new RenderPackSelectionBinding(settings, controller);
binding.ApplyAtFrameBoundary(Extent);
settings.SaveDisplay(settings.Display with { RenderPack = Selection() });
Assert.Null(controller.ActiveRuntime);
binding.ApplyAtFrameBoundary(Extent);
Assert.NotNull(controller.ActiveRuntime);
factory.Failure = new InvalidOperationException("pipeline rejected");
settings.SaveDisplay(settings.Display with
{
RenderPack = Selection() with { PresetId = "medium" },
});
RenderPackActivationSnapshot failed = binding.ApplyAtFrameBoundary(Extent);
Assert.Equal(RenderPackActivationState.FailedToRetail, failed.State);
Assert.True(settings.Display.RenderPack.IsRetail);
Assert.Contains("pipeline rejected", controller.Snapshot.Reason, StringComparison.Ordinal);
Assert.Null(controller.ActiveRuntime);
// #425: the user's next Apply of the SAME selection is an explicit
// choice — with the failure cause gone it activates instead of being
// refused with the "will not be retried" memo.
factory.Failure = null;
settings.SaveDisplay(settings.Display with
{
RenderPack = Selection() with { PresetId = "medium" },
});
RenderPackActivationSnapshot retried = binding.ApplyAtFrameBoundary(Extent);
Assert.NotEqual(RenderPackActivationState.FailedToRetail, retried.State);
Assert.DoesNotContain("will not be retried", retried.Reason ?? string.Empty, StringComparison.Ordinal);
Assert.NotNull(controller.ActiveRuntime);
}
[Fact]
public void Built_in_atmospheric_pack_is_a_public_contract_conformant_tier2plus_pack()
{
RenderPackValidationResult result = RenderPackValidator.ValidateDescriptor(
BuiltInAtmosphericRenderPack.Descriptor,
RenderPackHostCapabilities.Conformance);
Assert.True(result.Success, result.Reason);
Assert.Equal(RenderPackTier.Tier2Plus, BuiltInAtmosphericRenderPack.Descriptor.HighestTier);
Assert.Contains(
BuiltInAtmosphericRenderPack.Descriptor.SceneReplays,
replay => replay.CasterClasses.HasFlag(RenderCasterClass.Terrain)
&& replay.CasterClasses.HasFlag(RenderCasterClass.OpaqueWorld)
&& replay.CasterClasses.HasFlag(RenderCasterClass.AlphaCutoutWorld)
&& replay.CasterClasses.HasFlag(RenderCasterClass.AnimatedOpaque)
&& replay.CasterClasses.HasFlag(RenderCasterClass.AnimatedAlphaCutout));
Assert.Contains(BuiltInAtmosphericRenderPack.Descriptor.QualityPresets, value => value.Id == "low");
Assert.Contains(BuiltInAtmosphericRenderPack.Descriptor.QualityPresets, value => value.Id == "medium");
Assert.Contains(BuiltInAtmosphericRenderPack.Descriptor.QualityPresets, value => value.Id == "high");
Assert.Contains(BuiltInAtmosphericRenderPack.Descriptor.QualityPresets, value => value.Id == "auto");
Assert.Equal(
"0.8",
BuiltInAtmosphericRenderPack
.Descriptor
.Settings
.Single(value => value.Semantic == RenderSettingSemantic.Exposure)
.DefaultValue);
Assert.Equal(0.25, ResourceScale("low", "volumetric"));
Assert.Equal(0.25, ResourceScale("medium", "volumetric"));
Assert.Equal(0.5, ResourceScale("high", "volumetric"));
Assert.Equal(0.25, ResourceScale("low", "sun-rays"));
Assert.Equal(0.5, ResourceScale("medium", "sun-rays"));
Assert.Equal(0.5, ResourceScale("high", "sun-rays"));
Assert.Equal(0.25, ResourceScale("low", "sun-mask"));
Assert.Equal(0.5, ResourceScale("medium", "sun-mask"));
Assert.Equal(0.5, ResourceScale("high", "sun-mask"));
double ResourceScale(string presetId, string resourceId) =>
BuiltInAtmosphericRenderPack
.Descriptor
.QualityPresets
.Single(value => value.Id == presetId)
.ResourceOverrides
.Single(value => value.ResourceId == resourceId)
.Extent!
.Width;
}
[Fact]
public void Atmospheric_executor_rejects_a_resource_semantic_with_the_wrong_shape()
{
RenderPackDescriptor source = BuiltInAtmosphericRenderPack.Descriptor;
RenderPackDescriptor descriptor = source with
{
Resources = source.Resources.Select(value =>
value.Semantic == RenderResourceSemantic.DirectionalShadowDepth
? value with { Kind = RenderResourceKind.Image2D }
: value).ToArray(),
};
RenderPackValidationResult result = RenderPackValidator.ValidateDescriptor(
descriptor,
RenderPackHostCapabilities.Conformance);
Assert.False(result.Success);
Assert.Contains(
"does not match the fixed atmospheric executor's kind, format, extent, usage, and lifetime contract",
result.Reason,
StringComparison.Ordinal);
}
[Fact]
public void Atmospheric_executor_rejects_a_variant_semantic_with_the_wrong_shape()
{
RenderPackDescriptor source = BuiltInAtmosphericRenderPack.Descriptor;
RenderPackDescriptor descriptor = source with
{
PipelineVariants = source.PipelineVariants.Select(value =>
value.Semantic == RenderPipelineVariantSemantic.WorldAlphaCutoutDirectionalShadowCaster
? value with { CompatibleMaterials = RenderMaterialClass.Opaque }
: value).ToArray(),
};
RenderPackValidationResult result = RenderPackValidator.ValidateDescriptor(
descriptor,
RenderPackHostCapabilities.Conformance);
Assert.False(result.Success);
Assert.Contains(
"does not match the fixed atmospheric executor's base, material, and input contract",
result.Reason,
StringComparison.Ordinal);
}
[Fact]
public void Atmospheric_executor_rejects_a_replay_that_drops_a_headline_caster_class()
{
RenderPackDescriptor source = BuiltInAtmosphericRenderPack.Descriptor;
RenderPackDescriptor descriptor = source with
{
SceneReplays = source.SceneReplays.Select(value => value with
{
CasterClasses = value.CasterClasses & ~RenderCasterClass.AnimatedAlphaCutout,
}).ToArray(),
};
RenderPackValidationResult result = RenderPackValidator.ValidateDescriptor(
descriptor,
RenderPackHostCapabilities.Conformance);
Assert.False(result.Success);
Assert.Contains("all five headline caster classes", result.Reason, StringComparison.Ordinal);
}
[Fact]
public void Api_v1_rejects_publicly_reserved_buffer_and_storage_declarations()
{
RenderPackDescriptor descriptor = Descriptor() with
{
Resources =
[
new RenderResourceDeclaration(
"future-buffer",
RenderResourceKind.Buffer,
RenderFormatClass.StructuredData,
Extent: null,
SizeBytes: 64,
RenderResourceUsage.Storage,
RenderResourceLifetime.ActivePack,
EstimatedResidentBytes: 64),
],
};
RenderPackValidationResult result = RenderPackValidator.ValidateDescriptor(
descriptor,
RenderPackHostCapabilities.Conformance);
Assert.False(result.Success);
Assert.Contains("reserved for a future render-pack API", result.Reason, StringComparison.Ordinal);
}
[Fact]
public void Api_v1_rejects_a_pass_that_exceeds_four_ordered_texture_slots()
{
RenderResourceDeclaration Resource(string id) => new(
id,
RenderResourceKind.Image2D,
RenderFormatClass.HdrColor,
new RenderExtentDeclaration(RenderExtentMode.RelativeToMainWorld, 0.5, 0.5),
SizeBytes: 0,
RenderResourceUsage.Sampled | RenderResourceUsage.ColorAttachment,
RenderResourceLifetime.ActivePack,
EstimatedResidentBytes: 1024);
RenderPassDeclaration Writer(string id) => new(
$"write-{id}",
RenderPassHook.AtmosphereBeforeToneMap,
"fullscreen.vert.spv",
"write.frag.spv",
[],
[],
[id]);
string[] resourceIds = ["a", "b", "c", "d"];
RenderPackDescriptor descriptor = Descriptor() with
{
Resources = resourceIds.Select(Resource).ToArray(),
Passes =
[
.. resourceIds.Select(Writer),
new RenderPassDeclaration(
"too-many-inputs",
RenderPassHook.ToneMap,
"fullscreen.vert.spv",
"tone.frag.spv",
[RenderSemanticInput.WorldColor],
resourceIds,
[]),
],
};
RenderPackValidationResult result = RenderPackValidator.ValidateDescriptor(
descriptor,
RenderPackHostCapabilities.Conformance);
Assert.False(result.Success);
Assert.Contains("provides four ordered texture slots", result.Reason, StringComparison.Ordinal);
}
[Fact]
public void Built_in_filmic_pass_maps_exactly_to_texture_slots_a_through_d()
{
RenderPackDescriptor descriptor = BuiltInAtmosphericRenderPack.Descriptor;
RenderPassDeclaration pass = descriptor.Passes.Single(value =>
value.Id == "filmic-composite");
IReadOnlyList<RenderPackTextureInput> slots = RenderPackTextureBindingResolver.Resolve(
pass,
descriptor.Resources.ToDictionary(static value => value.Id));
Assert.Equal(4, slots.Count);
Assert.Equal(RenderSemanticInput.WorldColor, slots[0].Semantic);
Assert.Equal("bloom-a", slots[1].ResourceId);
Assert.Equal("sun-rays", slots[2].ResourceId);
Assert.Equal("volumetric", slots[3].ResourceId);
}
[Theory]
[InlineData("../escape.spv")]
[InlineData("/absolute.spv")]
[InlineData("shaders\\escape.spv")]
public void Selected_asset_validation_rejects_unsafe_keys(string key)
{
RenderPackDescriptor descriptor = Descriptor() with
{
Passes =
[
new RenderPassDeclaration(
"tone-map",
RenderPassHook.ToneMap,
key,
"safe.frag.spv",
[],
[],
[]),
],
};
var assets = new StubAssets([3, 2, 35, 7]);
RenderPackValidationResult result =
RenderPackValidator.ValidateSelectedAssets(descriptor, assets);
Assert.False(result.Success);
Assert.Contains("unsafe asset key", result.Reason, StringComparison.Ordinal);
Assert.Equal(0, assets.OpenCount);
}
private static RenderPackController Controller(
BufferedRenderPackRegistry registry,
StubFactory factory) => new(
() => RenderPackCatalog.Build(
registry.Snapshot(),
RenderPackHostCapabilities.Conformance),
factory,
preparationScheduler: InlineRenderPackPreparationScheduler.Instance);
private static RenderPackSelectionSettings Selection() => new(
"test.pack",
"1.0.0",
"low");
private static RenderPackDescriptor Descriptor() => new(
"test.pack",
"Test Pack",
new Version(1, 0, 0),
RenderPackApi.Current,
RenderPackTier.Tier1,
[],
[],
[],
[],
[],
[],
[Preset(), Preset() with { Id = "medium", DisplayName = "Medium" }],
[],
null)
{
FeatureSummary = "Test render pack.",
};
private static RenderQualityPreset Preset() => new(
"low",
"Low",
[],
[],
[],
64L * 1024 * 1024,
2.0,
3.0,
0.15,
0.50);
private static string RepositoryRoot()
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
return directory.FullName;
directory = directory.Parent;
}
throw new InvalidOperationException("Could not locate the repository root.");
}
private static RenderSettingDeclaration[] Settings() =>
[
new("enabled", "Enabled", RenderSettingKind.Boolean, "false", null, null, null, []),
new("exposure", "Exposure", RenderSettingKind.Float, "1", 0, 2, 0.25, []),
new("samples", "Samples", RenderSettingKind.Integer, "2", 0, 10, 2, []),
new("quality", "Quality", RenderSettingKind.Choice, "low", null, null, null,
["low", "high"]),
];
private sealed class StubAssets(byte[]? bytes = null) : IRenderPackAssets
{
private readonly byte[] _bytes = bytes ?? [];
internal int OpenCount { get; private set; }
public Stream OpenRead(string assetKey)
{
OpenCount++;
return new MemoryStream(_bytes, writable: false);
}
}
private sealed class ThrowingAssets(Exception error) : IRenderPackAssets
{
public Stream OpenRead(string assetKey) => throw error;
}
private sealed class StatefulAssets(IReadOnlyDictionary<string, byte[]> firstValues)
: IRenderPackAssets
{
private readonly Dictionary<string, int> _openCounts =
new(StringComparer.Ordinal);
internal int OpenCount(string key) => _openCounts.GetValueOrDefault(key);
public Stream OpenRead(string assetKey)
{
int count = _openCounts.GetValueOrDefault(assetKey) + 1;
_openCounts[assetKey] = count;
if (count != 1)
{
// A second read is intentionally different, exposing any
// validate-then-reopen TOCTOU path deterministically.
return new MemoryStream([3, 2, 35, 7], writable: false);
}
return new MemoryStream(firstValues[assetKey], writable: false);
}
}
private sealed class SelectionStorage : IRuntimeSettingsStorage
{
public SettingsStore? LayoutStore => null;
public string Location => "memory://render-pack";
public DisplaySettings Display { get; private set; } = DisplaySettings.Default;
public DisplaySettings LoadDisplay() => Display;
public AudioSettings LoadAudio() => AudioSettings.Default;
public ChatSettings LoadChat() => ChatSettings.Default;
public CharacterSettings LoadCharacter(string toonKey) => CharacterSettings.Default;
public CameraTurningSettings LoadCameraTurning() => CameraTurningSettings.Default;
public void SaveDisplay(DisplaySettings display) => Display = display;
public void SaveAudio(AudioSettings audio)
{
}
public void SaveChat(ChatSettings chat)
{
}
public void SaveCameraTurning(CameraTurningSettings cameraTurning)
{
}
}
private sealed class StubFactory : IRenderPackRuntimeFactory
{
internal int BuildCount { get; private set; }
internal IReadOnlyDictionary<string, string>? LastUserSettingOverrides { get; private set; }
internal Exception? Failure { get; set; }
internal Func<RenderPackDescriptor, RenderQualityPreset, IRenderPackRuntime>?
RuntimeFactory { get; init; }
public IRenderPackRuntime Build(
RenderPackDescriptor descriptor,
ValidatedRenderPackShaderAssets assets,
RenderQualityPreset preset,
IReadOnlyDictionary<string, string> userSettingOverrides)
{
BuildCount++;
LastUserSettingOverrides = new Dictionary<string, string>(
userSettingOverrides,
StringComparer.OrdinalIgnoreCase);
if (Failure is { } failure)
throw failure;
if (RuntimeFactory is { } create)
return create(descriptor, preset);
return new StubRuntime(descriptor, preset);
}
}
private sealed class ControlledPreparationScheduler : IRenderPackPreparationScheduler
{
private readonly Queue<(Action Work, TaskCompletionSource Completion)> _pending = [];
public Task Schedule(Action preparation)
{
var completion = new TaskCompletionSource(
TaskCreationOptions.RunContinuationsAsynchronously);
_pending.Enqueue((preparation, completion));
return completion.Task;
}
internal void CompleteNext()
{
(Action work, TaskCompletionSource completion) = _pending.Dequeue();
try
{
work();
completion.SetResult();
}
catch (Exception error)
{
completion.SetException(error);
}
}
}
private sealed class StubRuntime(
RenderPackDescriptor descriptor,
RenderQualityPreset preset) : IRenderPackRuntime
{
public RenderPackDescriptor Descriptor { get; } = descriptor;
public RenderQualityPreset Preset { get; } = preset;
internal bool Disposed { get; private set; }
public void Dispose() => Disposed = true;
}
private sealed class DiagnosticStubRuntime(
RenderPackDescriptor descriptor,
RenderQualityPreset preset,
RenderPackRuntimeDiagnostics diagnostics)
: IRenderPackRuntime, IRenderPackRuntimeDiagnosticsSource
{
public RenderPackDescriptor Descriptor { get; } = descriptor;
public RenderQualityPreset Preset { get; } = preset;
public RenderPackRuntimeDiagnostics CaptureDiagnostics() => diagnostics;
public void Dispose()
{
}
}
private sealed class ThrowingGraphRuntime(
RenderPackDescriptor descriptor,
RenderQualityPreset preset,
Exception failure) : IAtmosphericWorldGraphRuntime
{
public RenderPackDescriptor Descriptor { get; } = descriptor;
public RenderQualityPreset Preset { get; } = preset;
internal bool Disposed { get; private set; }
public IGpuRenderTarget PrepareWorldTarget(int width, int height, int sampleCount) =>
throw failure;
public void RenderPostProcess(IGpuFrame frame, in AtmosphericFrameInputs inputs) =>
throw new InvalidOperationException("The candidate never activates.");
public void Dispose() => Disposed = true;
}
}
file static class RenderPackControllerTestExtensions
{
internal static RenderPackSettingOverrides ToRenderPackOverrides(
this IReadOnlyDictionary<string, string> values) => new(values);
}