test: replace frame orchestration source freezes

This commit is contained in:
Erik 2026-08-18 16:40:56 +02:00
parent 9bd5d47c47
commit 9b94050229
3 changed files with 501 additions and 466 deletions

View file

@ -1,4 +1,13 @@
using System.Reflection;
using System.Reflection.Emit;
using AcDream.App.Diagnostics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming;
using AcDream.App.Tests.Architecture;
using AcDream.App.World;
using AcDream.Core.World;
namespace AcDream.App.Tests.Rendering;
@ -47,36 +56,40 @@ public sealed class RenderFrameResourceControllerTests
[Fact]
public void Production_begin_resources_preserve_the_exact_frame_order()
{
string source = ResourceSource();
// Campaign V slice V4a: the world-HUD and retained-UI TextRenderers no
// longer take a per-slot begin call — they now read the shared
// IGpuFrame ring reset once per frame by IGpuDevice.BeginFrame() (see
// GpuDeviceFrameLifetime), so _worldText/_uiText are gone from this
// sequence.
AssertAppearsInOrder(
source,
"_textures?.BeginCompositeTextureFrame();",
"_textures?.TickCompositeTextureCache();",
"_dispatcher?.BeginFrame(gpuSlot);",
"_environmentCells?.BeginFrame(gpuSlot);",
"_portalDepth?.BeginFrame(gpuSlot);",
"_clip?.BeginFrame(gpuSlot);",
"_terrain?.BeginFrame(gpuSlot);",
"_lighting?.BeginFrame(gpuSlot);");
Assert.DoesNotContain("_profiler.FrameBoundary(", source);
MethodInfo begin = RequiredMethod(
typeof(RuntimeRenderFrameBeginResources),
nameof(RuntimeRenderFrameBeginResources.Begin));
AssertCallOrder(
begin,
(typeof(TextureCache), nameof(TextureCache.BeginCompositeTextureFrame)),
(typeof(TextureCache), nameof(TextureCache.TickCompositeTextureCache)),
(typeof(WbDrawDispatcher), nameof(WbDrawDispatcher.BeginFrame)),
(typeof(EnvCellRenderer), nameof(EnvCellRenderer.BeginFrame)),
(typeof(PortalDepthMaskRenderer), nameof(PortalDepthMaskRenderer.BeginFrame)),
(typeof(ClipFrame), nameof(ClipFrame.BeginFrame)),
(typeof(TerrainModernRenderer), nameof(TerrainModernRenderer.BeginFrame)),
(typeof(SceneLightingUboBinding), nameof(SceneLightingUboBinding.BeginFrame)));
Assert.DoesNotContain(
CompiledCallGraph.Read(begin),
call => call.Target.DeclaringType == typeof(FrameProfiler)
&& call.Target.Name == "FrameBoundary");
}
[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);");
AssertCallOrder(
RequiredMethod(
typeof(RuntimeRenderFrameLivePreparation),
nameof(RuntimeRenderFrameLivePreparation.Prepare)),
(typeof(WbMeshAdapter), nameof(WbMeshAdapter.Tick)),
(typeof(WorldRevealCoordinator), nameof(WorldRevealCoordinator.PrepareAndEvaluate)),
(typeof(ParticleRenderer), nameof(ParticleRenderer.BeginFrame)));
}
// Campaign V slice V11 deleted RuntimeRenderFrameClearPhase (the raw-GL
@ -101,11 +114,18 @@ public sealed class RenderFrameResourceControllerTests
controller.Tick(0.5d);
Assert.Equal(0.75d, controller.ElapsedSeconds);
AssertAppearsInOrder(
ResourceSource(),
"_weather.Tick(",
"nowSeconds: _elapsedSeconds,",
"_elapsedSeconds += deltaSeconds;");
MethodInfo tick = RequiredMethod(
typeof(RenderWeatherFrameController),
nameof(RenderWeatherFrameController.Tick));
CompiledCall weather = Assert.Single(
CompiledCallGraph.Read(tick),
call => call.Target.DeclaringType == typeof(WeatherSystem)
&& call.Target.Name == nameof(WeatherSystem.Tick));
CompiledFieldReference elapsedStore = Assert.Single(
CompiledCallGraph.ReadFieldReferences(tick),
reference => reference.Field.Name == "_elapsedSeconds"
&& reference.OpCode == OpCodes.Stfld);
Assert.True(weather.Offset < elapsedStore.Offset);
}
[Fact]
@ -127,38 +147,30 @@ public sealed class RenderFrameResourceControllerTests
Assert.False(live.IsWaitingForLogin);
}
private static string ResourceSource() => File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"RenderFrameResourceController.cs"));
private static MethodInfo RequiredMethod(Type owner, string name) =>
owner.GetMethod(
name,
BindingFlags.Instance | BindingFlags.Static
| BindingFlags.Public | BindingFlags.NonPublic)
?? throw new MissingMethodException(owner.FullName, name);
private static void AssertAppearsInOrder(string source, params string[] needles)
private static void AssertCallOrder(
MethodBase method,
params (Type Type, string Method)[] expected)
{
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
int cursor = -1;
foreach (string needle in needles)
foreach ((Type type, string name) in expected)
{
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;
int found = Enumerable.Range(cursor + 1, calls.Count - cursor - 1)
.FirstOrDefault(index => calls[index].Target.DeclaringType == type
&& calls[index].Target.Name == name, -1);
Assert.True(found > cursor,
$"Missing compiled edge after {cursor}: {type.FullName}.{name}.");
cursor = found;
}
}
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; }