Move ordered resource begin, clear/state setup, live upload/reveal, weather timing, and display pacing behind focused owners while preserving the accepted frame order and retryable shutdown ownership. Co-authored-by: Codex <codex@openai.com>
194 lines
6.4 KiB
C#
194 lines
6.4 KiB
C#
using AcDream.App.Rendering;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public sealed class RenderFrameResourceControllerTests
|
|
{
|
|
[Fact]
|
|
public void Prepare_runs_begin_clear_and_live_in_order_for_one_gpu_slot()
|
|
{
|
|
List<string> calls = [];
|
|
var clearResult = new RenderFrameFoundation(
|
|
PortalViewportVisible: true,
|
|
Sky: default,
|
|
Atmosphere: default);
|
|
var slots = new AdvancingSlotSource(2);
|
|
var controller = new RenderFrameResourceController(
|
|
slots,
|
|
new RecordingBegin(calls),
|
|
new RecordingClear(calls, clearResult),
|
|
new RecordingLive(calls));
|
|
|
|
controller.Prepare(new RenderFrameInput(1d / 60d, 1280, 720));
|
|
|
|
Assert.Equal(["begin:2", "clear", "live:2"], calls);
|
|
Assert.Equal(1, slots.ReadCount);
|
|
Assert.Equal(clearResult, controller.Foundation);
|
|
}
|
|
|
|
[Fact]
|
|
public void Required_phase_dependencies_fail_fast()
|
|
{
|
|
var slots = new AdvancingSlotSource(0);
|
|
var begin = new RecordingBegin([]);
|
|
var clear = new RecordingClear([], default);
|
|
var live = new RecordingLive([]);
|
|
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new RenderFrameResourceController(null!, begin, clear, live));
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new RenderFrameResourceController(slots, null!, clear, live));
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new RenderFrameResourceController(slots, begin, null!, live));
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new RenderFrameResourceController(slots, begin, clear, null!));
|
|
}
|
|
|
|
[Fact]
|
|
public void Production_begin_resources_preserve_the_exact_frame_order()
|
|
{
|
|
string source = ResourceSource();
|
|
|
|
AssertAppearsInOrder(
|
|
source,
|
|
"_textures?.BeginCompositeTextureFrame();",
|
|
"_textures?.TickCompositeTextureCache();",
|
|
"_dispatcher?.BeginFrame(gpuSlot);",
|
|
"_environmentCells?.BeginFrame(gpuSlot);",
|
|
"_portalDepth?.BeginFrame(gpuSlot);",
|
|
"_worldText?.BeginFrame(gpuSlot);",
|
|
"_uiText?.BeginFrame(gpuSlot);",
|
|
"_clip?.BeginFrame(gpuSlot);",
|
|
"_terrain?.BeginFrame(gpuSlot);",
|
|
"_lighting?.BeginFrame(gpuSlot);",
|
|
"_profiler.FrameBoundary(_gl);");
|
|
}
|
|
|
|
[Fact]
|
|
public void Production_live_preparation_publishes_meshes_before_reveal_and_particles()
|
|
{
|
|
string source = ResourceSource();
|
|
|
|
AssertAppearsInOrder(
|
|
source,
|
|
"_meshes?.Tick();",
|
|
"_worldReveal?.PrepareAndEvaluate(revealCell);",
|
|
"_particles?.BeginFrame(gpuSlot);");
|
|
}
|
|
|
|
[Fact]
|
|
public void Production_clear_phase_preserves_atmosphere_and_gl_state_order()
|
|
{
|
|
string source = ResourceSource();
|
|
|
|
AssertAppearsInOrder(
|
|
source,
|
|
"bool portalViewportVisible = _portal.IsPortalViewportVisible;",
|
|
"_particleVisibility.Reset();",
|
|
"SkyKeyframe sky = _worldTime.CurrentSky;",
|
|
"AtmosphereSnapshot atmosphere = _weather.Snapshot(in sky);",
|
|
"_gl.DepthMask(true);",
|
|
"_gl.Clear(",
|
|
"_gl.CullFace(TriangleFace.Back);",
|
|
"_gl.FrontFace(FrontFaceDirection.CW);",
|
|
"_diagnostics.EmitGlStateTripwireIfChanged(");
|
|
}
|
|
|
|
[Fact]
|
|
public void Weather_frame_clock_advances_only_after_the_weather_tick()
|
|
{
|
|
var controller = new RenderWeatherFrameController(
|
|
new AcDream.Core.World.WorldTimeService(
|
|
AcDream.Core.World.SkyStateProvider.Default()),
|
|
new AcDream.Core.World.WeatherSystem());
|
|
|
|
controller.Tick(0.25d);
|
|
controller.Tick(0.5d);
|
|
|
|
Assert.Equal(0.75d, controller.ElapsedSeconds);
|
|
AssertAppearsInOrder(
|
|
ResourceSource(),
|
|
"_weather.Tick(",
|
|
"nowSeconds: _elapsedSeconds,",
|
|
"_elapsedSeconds += deltaSeconds;");
|
|
}
|
|
|
|
[Fact]
|
|
public void Login_state_is_latched_by_the_shared_player_mode_source()
|
|
{
|
|
var mode = new AcDream.App.Input.LocalPlayerModeState();
|
|
var live = new AcDream.App.Rendering.RenderLoginStateSource(
|
|
liveMode: true,
|
|
mode);
|
|
var offline = new AcDream.App.Rendering.RenderLoginStateSource(
|
|
liveMode: false,
|
|
mode);
|
|
|
|
Assert.True(live.IsWaitingForLogin);
|
|
Assert.False(offline.IsWaitingForLogin);
|
|
|
|
mode.ChaseModeEverEntered = true;
|
|
|
|
Assert.False(live.IsWaitingForLogin);
|
|
}
|
|
|
|
private static string ResourceSource() => File.ReadAllText(Path.Combine(
|
|
FindRepoRoot(),
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"RenderFrameResourceController.cs"));
|
|
|
|
private static void AssertAppearsInOrder(string source, params string[] needles)
|
|
{
|
|
int cursor = -1;
|
|
foreach (string needle in needles)
|
|
{
|
|
int next = source.IndexOf(needle, cursor + 1, StringComparison.Ordinal);
|
|
Assert.True(next >= 0, $"Missing expected source fragment: {needle}");
|
|
Assert.True(next > cursor, $"Out-of-order source fragment: {needle}");
|
|
cursor = next;
|
|
}
|
|
}
|
|
|
|
private static string FindRepoRoot()
|
|
{
|
|
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 DirectoryNotFoundException("Could not find AcDream.slnx.");
|
|
}
|
|
|
|
private sealed class AdvancingSlotSource(int firstSlot) : IRenderFrameSlotSource
|
|
{
|
|
public int ReadCount { get; private set; }
|
|
|
|
public int CurrentSlot => firstSlot + ReadCount++;
|
|
}
|
|
|
|
private sealed class RecordingBegin(List<string> calls) : IRenderFrameBeginResources
|
|
{
|
|
public void Begin(int gpuSlot) => calls.Add($"begin:{gpuSlot}");
|
|
}
|
|
|
|
private sealed class RecordingClear(
|
|
List<string> calls,
|
|
RenderFrameFoundation result) : IRenderFrameClearPhase
|
|
{
|
|
public RenderFrameFoundation Clear()
|
|
{
|
|
calls.Add("clear");
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private sealed class RecordingLive(List<string> calls) : IRenderFrameLivePreparation
|
|
{
|
|
public void Prepare(int gpuSlot) => calls.Add($"live:{gpuSlot}");
|
|
}
|
|
}
|