test: replace render source freezes

This commit is contained in:
Erik 2026-08-18 14:58:25 +02:00
parent c31a9ac411
commit 5a33369074
7 changed files with 425 additions and 345 deletions

View file

@ -38,12 +38,12 @@ pwsh ./tools/audit-test-inventory.ps1
The generated JSON lives under ignored `artifacts/test-audit/`; it is not a
second 6.8 MB checked-in source of truth. The script and this reviewed ledger
are durable, while paths and line numbers regenerate after every batch. The
inventory refreshed through Batch U reports:
inventory refreshed through Batch W reports:
| Syntax signal | Current count |
|---|---:|
| Tracked/parsed C# test files | 1,256 / 1,256 |
| Attributed test methods (not expanded theory cases) | 11,447 |
| Tracked/parsed C# test files | 1,254 / 1,254 |
| Attributed test methods (not expanded theory cases) | 11,416 |
| Exact duplicate-data rows | 0 |
| Reviewed body-equivalent groups / methods | 11 / 27 |
| Methods containing at least one empty `return;` | 138 |
@ -56,7 +56,7 @@ inventory refreshed through Batch U reports:
| Methods directly using `Thread.Sleep` / `Task.Delay` | 15 / 14 |
| Cancellable-infinite-only / elapsed-time methods | 5 / 24 |
| Methods directly reading environment variables | 47 |
| Methods directly / through same-file helpers reading `.cs` source text | 67 / 107 |
| Methods directly / through same-file helpers reading `.cs` source text | 63 / 98 |
The three remaining prerequisite-return candidates are all reviewed branch
false positives: the two Windows/Linux factory assertions and the launcher's
@ -140,7 +140,7 @@ identifiable in the lane report.
| T-009 wall-clock double-click tests | resolved in batch E | Four sleeps were replaced by a deterministic test clock behind an internal factory overload. The production factory still reads `Environment.TickCount64` exactly as before. |
| T-010 two useless cases | high-confidence cleanup batch A | Delete `SmokeTest.TestProject_IsWired` and `ChaseCameraTests.ImplementsICamera`; compilation already proves both claims. |
| T-011 diagnostic-only methods | resolved in batches C, L, and R | The reviewed current set is 82 methods / 103 cases. All carry `Purpose=Diagnostic`, preserving the apparatus while removing it from release pass totals. Batch R catches investigations whose only assertion validates fixture/DAT availability, which the original mechanical output-only scan could not distinguish from an oracle. |
| T-012 source-text freezes | direct-read map completed in batch I, helper-mediated gap corrected in batch U, and staged replacement approved | Seventeen whole-tree architecture rules and five cross-artifact contracts stay. The other 85 are literal implementation or test-model freezes; approval authorizes retiring each only beside its semantic/behavioral replacement. |
| T-012 source-text freezes | direct-read map completed in batch I, helper-mediated gap corrected in batch U, and staged replacement active | Seventeen whole-tree architecture rules and five cross-artifact contracts stay. Batch W retires the first nine literal implementation freezes beside stronger behavior or compiled-metadata evidence; 76 staged replacements remain. |
| T-013 controller self-comparison | high-confidence cleanup batch A | Capture the first controller next to the first body and compare every retry with that reference. |
| T-014 seven load-sensitive tests | six mechanisms repaired in batch F; one product defect classified | Virtual/wall-clock mixing, tiered-JIT allocation noise, a live controller clock, and a ThreadPool-start timing oracle were removed without weakening behavioral contracts. `DatSoundCache` #321 is a real in-flight-entry race and now runs as `Status=KnownFailure` pending a product fix. |
| T-015 four non-prerequisite skips | resolved in batches A/B | PVS scaffold deleted with rationale preserved; redundant chat/radar generators deleted in favor of the comprehensive Manual lane; tower oracle is `Status=KnownFailure`. |
@ -1027,3 +1027,61 @@ Verification:
and
- the no-retry complete hermetic Release gate passes 14,351/14,351 with zero
skips or failures across all 12 test assemblies, exactly 28 below Batch U.
## Batch W render delegation and cleanup source-freeze replacement
Batch W retires the first nine of the 85 approved implementation-text freezes
without changing product code. Six test methods now inspect runtime behavior,
reflection metadata, or compiled call/new-object edges; three redundant source
tests are removed because an existing behavioral trace already asserts the
same contract more strongly.
The exact decisions are:
- `TextRendererConstructorOwnsExactlyOneDeviceResource` no longer counts
constructor strings. `TextRendererConstructionCreatesAndDisposesOnlyOnePipeline`
constructs the real renderer through `RecordingGpuDevice`, proves that only
one pipeline is added (no buffer, texture, sampler, or texture slot), and
proves that exact pipeline is disposed. The V4a/V6d rollback rationale stays
beside the test. The recording device gained read-only created-resource lists
solely so tests can observe those device calls.
- `Renderer_source_preserves_retail_stage_order_through_typed_operations` is
removed. The existing outdoor and interior `DrawInside` tests already drive
the real renderer and assert the typed operation sequence, including early
and late landscape, particle flush, interior clear, masks, shells, and entity
routes.
- `Production_uses_retained_routes_without_rebuilding_legacy_partition` is
removed. The existing production-product test is renamed to state this
contract and continues to prove one actual retained candidate with zero
compare-only referee work. Exact private selector spellings are not a product
oracle.
- `Production_builder_preserves_the_frame_preparation_order` is removed. The
existing builder test already supplies recording implementations of every
typed source and asserts the complete call order plus the borrowed result.
- executor reset/diagnostic bracketing, the one-executor/one-PView composition,
lighting snapshot-before-UBO order, typed world-frame build, local frame-root
composition, and GameWindow delegation now inspect compiled call/new-object
edges and reflected owner fields. `CompiledCallGraph` is shared test-only
infrastructure that parses the built method body; formatting, comments, local
variable names, and source paths cannot satisfy or break these contracts.
Two mixed source assertions were deliberately narrowed rather than silently
carried forward. Absence of `Console.WriteLine` is an implementation-style
check with no output oracle and is not treated as render behavior. The old
world-scene source test also embedded a separate teardown-order claim; that
claim remains with the dedicated lifetime suite and is reconciled in the
approved host/lifetime replacement batch instead of coupling teardown to frame
construction.
Verification:
- the four directly affected App suites pass 39/39;
- the complete locked Release build covers all 44 projects with zero warnings
and zero errors;
- the no-retry complete hermetic Release gate passes 14,348/14,348 with zero
skips or failures across all 12 test assemblies; the exact three-case
reduction is the three redundant source tests above; and
- the regenerated 1,254-file inventory parses every file and reduces direct
source readers from 67 to 63 and total direct/helper readers from 107 to 98.
The remaining 98 reconcile to the 22 approved retained policies/contracts
and 76 staged replacements.

View file

@ -0,0 +1,109 @@
using System.Reflection;
using System.Reflection.Emit;
namespace AcDream.App.Tests.Architecture;
internal readonly record struct CompiledCall(int Offset, MethodBase Target);
/// <summary>
/// Reads compiled call/new-object edges from a method body. Architecture tests
/// use this when the contract is an ownership or ordering edge that cannot be
/// exercised through a public result, avoiding formatting- and comment-sensitive
/// source-string assertions.
/// </summary>
internal static class CompiledCallGraph
{
private static readonly IReadOnlyDictionary<short, OpCode> OpCodesByValue =
typeof(OpCodes)
.GetFields(BindingFlags.Public | BindingFlags.Static)
.Where(field => field.FieldType == typeof(OpCode))
.Select(field => (OpCode)field.GetValue(null)!)
.ToDictionary(opCode => opCode.Value);
public static IReadOnlyList<CompiledCall> Read(MethodBase method)
{
ArgumentNullException.ThrowIfNull(method);
byte[] il = method.GetMethodBody()?.GetILAsByteArray()
?? throw new InvalidOperationException(
$"{method.DeclaringType?.FullName}.{method.Name} has no compiled body.");
Type[]? declaringArguments = method.DeclaringType?.IsGenericType == true
? method.DeclaringType.GetGenericArguments()
: null;
Type[]? methodArguments = method.IsGenericMethod
? method.GetGenericArguments()
: null;
var calls = new List<CompiledCall>();
for (int cursor = 0; cursor < il.Length;)
{
int instructionOffset = cursor;
OpCode opCode = ReadOpCode(il, ref cursor);
if (opCode.OperandType == OperandType.InlineMethod)
{
int token = BitConverter.ToInt32(il, cursor);
MethodBase? target = method.Module.ResolveMethod(
token,
declaringArguments,
methodArguments);
if (target is not null && opCode is { Value: var value }
&& value is 0x28 or 0x6F or 0x73)
{
calls.Add(new CompiledCall(instructionOffset, target));
}
}
cursor += OperandSize(opCode.OperandType, il, cursor);
}
return calls;
}
public static int IndexOf(
IReadOnlyList<CompiledCall> calls,
Type declaringType,
string methodName,
int startIndex = 0) =>
Enumerable.Range(startIndex, calls.Count - startIndex)
.FirstOrDefault(
index => calls[index].Target.DeclaringType == declaringType
&& calls[index].Target.Name == methodName,
-1);
private static OpCode ReadOpCode(byte[] il, ref int cursor)
{
byte first = il[cursor++];
short value = first == 0xFE
? unchecked((short)(0xFE00 | il[cursor++]))
: first;
return OpCodesByValue.TryGetValue(value, out OpCode opCode)
? opCode
: throw new InvalidOperationException(
$"Unknown IL opcode 0x{unchecked((ushort)value):X4}.");
}
private static int OperandSize(OperandType operandType, byte[] il, int cursor) =>
operandType switch
{
OperandType.InlineNone => 0,
OperandType.ShortInlineBrTarget or
OperandType.ShortInlineI or
OperandType.ShortInlineVar => 1,
OperandType.InlineVar => 2,
OperandType.InlineBrTarget or
OperandType.InlineField or
OperandType.InlineI or
OperandType.InlineMethod or
OperandType.InlineSig or
OperandType.InlineString or
OperandType.InlineTok or
OperandType.InlineType or
OperandType.ShortInlineR => 4,
OperandType.InlineI8 or OperandType.InlineR => 8,
OperandType.InlineSwitch =>
sizeof(int) + (BitConverter.ToInt32(il, cursor) * sizeof(int)),
_ => throw new ArgumentOutOfRangeException(
nameof(operandType),
operandType,
"Unsupported IL operand type."),
};
}

View file

@ -84,6 +84,9 @@ internal sealed class RecordingGpuDevice : IGpuDevice
private readonly List<GpuRecordedCall> _calls = [];
private readonly List<Action> _queuedActions = [];
private readonly List<RecordingGpuBuffer> _createdBuffers = [];
private readonly List<RecordingGpuPipeline> _createdPipelines = [];
private readonly List<RecordingGpuSampler> _createdSamplers = [];
private readonly Dictionary<GpuSamplerDescription, RecordingGpuSampler> _samplers = [];
private readonly byte[] _ring;
private readonly Stack<uint> _freeTextureSlots = new();
@ -151,8 +154,18 @@ internal sealed class RecordingGpuDevice : IGpuDevice
public void Clear() => _calls.Clear();
public IGpuBuffer CreateBuffer(in GpuBufferDescription description) =>
new RecordingGpuBuffer(description);
public IReadOnlyList<RecordingGpuBuffer> CreatedBuffers => _createdBuffers;
public IReadOnlyList<RecordingGpuPipeline> CreatedPipelines => _createdPipelines;
public IReadOnlyList<RecordingGpuSampler> CreatedSamplers => _createdSamplers;
public IGpuBuffer CreateBuffer(in GpuBufferDescription description)
{
var buffer = new RecordingGpuBuffer(description);
_createdBuffers.Add(buffer);
return buffer;
}
/// <summary>
/// Campaign V slice V6i-2: every image this device made, in creation order.
@ -185,13 +198,16 @@ internal sealed class RecordingGpuDevice : IGpuDevice
RecordingGpuSampler created = new(description);
_samplers.Add(description, created);
_createdSamplers.Add(created);
return created;
}
public IGpuPipeline CreatePipeline(GpuPipelineDescription description)
{
ArgumentNullException.ThrowIfNull(description);
return new RecordingGpuPipeline(description);
var pipeline = new RecordingGpuPipeline(description);
_createdPipelines.Add(pipeline);
return pipeline;
}
public IGpuRenderTarget CreateRenderTarget(in GpuRenderTargetDescription description) =>

View file

@ -1,4 +1,6 @@
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Tests.Rendering.Gpu;
namespace AcDream.App.Tests.Rendering;
@ -88,48 +90,34 @@ public sealed class ResourceCleanupGroupTests
/// re-growing a second resource without re-growing the rollback fails here.
/// </summary>
[Fact]
public void TextRendererConstructorOwnsExactlyOneDeviceResource()
public void TextRendererConstructionCreatesAndDisposesOnlyOnePipeline()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"TextRenderer.cs"));
using var device = new RecordingGpuDevice();
int buffersBefore = device.CreatedBuffers.Count;
int pipelinesBefore = device.CreatedPipelines.Count;
int samplersBefore = device.CreatedSamplers.Count;
int texturesBefore = device.CreatedTextures.Count;
int slotsBefore = device.LiveTextureSlotCount;
Assert.Equal(1, CountOccurrences(source, "device.CreatePipeline("));
Assert.Equal(0, CountOccurrences(source, "device.CreateTexture("));
Assert.Equal(0, CountOccurrences(source, "device.CreateBuffer("));
Assert.Equal(0, CountOccurrences(source, "device.CreateSampler("));
Assert.Equal(0, CountOccurrences(source, "device.RegisterTexture("));
var renderer = new TextRenderer(
device,
new NullGpuFrameSource(),
shaderDir: "unused");
// And the one resource is released.
Assert.Contains("public void Dispose() => _pipeline.Dispose();", source, StringComparison.Ordinal);
RecordingGpuPipeline pipeline = Assert.Single(
device.CreatedPipelines.Skip(pipelinesBefore));
Assert.Equal(buffersBefore, device.CreatedBuffers.Count);
Assert.Equal(samplersBefore, device.CreatedSamplers.Count);
Assert.Equal(texturesBefore, device.CreatedTextures.Count);
Assert.Equal(slotsBefore, device.LiveTextureSlotCount);
renderer.Dispose();
Assert.True(pipeline.IsDisposed);
}
private static int CountOccurrences(string source, string needle)
private sealed class NullGpuFrameSource : ICurrentGpuFrameSource
{
int count = 0;
for (int i = source.IndexOf(needle, StringComparison.Ordinal);
i >= 0;
i = source.IndexOf(needle, i + needle.Length, StringComparison.Ordinal))
{
count++;
}
return count;
}
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.");
public IGpuFrame? CurrentFrame => null;
}
}

View file

@ -1,7 +1,9 @@
using System.Numerics;
using System.Reflection;
using AcDream.App.Composition;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Scene;
using AcDream.App.Tests.Architecture;
using AcDream.Core.World;
namespace AcDream.App.Tests.Rendering;
@ -219,7 +221,7 @@ public sealed class RetailPViewPassExecutorTests
}
[Fact]
public void DrawInside_production_product_does_not_require_compare_only_referee()
public void DrawInside_production_product_uses_retained_routes_without_legacy_partition_when_diagnostics_are_disabled()
{
LoadedCell interior = InteriorWithExit(0xA9B40100u);
WorldEntity dynamic = Entity(
@ -274,70 +276,6 @@ public sealed class RetailPViewPassExecutorTests
Assert.Contains("look-in-punch", executor.Operations);
}
[Fact]
public void Renderer_source_preserves_retail_stage_order_through_typed_operations()
{
string source = RendererSource();
string drawInside = MethodBody(source, "public RetailPViewFrameResult DrawInside(");
AssertAppearsInOrder(
drawInside,
"passes.BeginFrame();",
"passes.EmitDiagnostics(ctx, result);",
"DrawLandscapeThroughOutsideView(",
"DrawExitPortalMasks(ctx, passes",
"DrawEnvCellShells(passes, pvFrame);",
"DrawCellObjectLists(",
"DrawDynamicsLast(");
string landscape = MethodBody(
source,
"private void DrawLandscapeThroughOutsideView(");
AssertAppearsInOrder(
landscape,
"passes.SetTerrainClip(slice.Planes);",
"passes.DrawLandscapeSlice(",
"DrawBuildingLookIns(",
"passes.DrawLandscapeSliceLate(",
"passes.DrawUnattachedSceneParticles(ctx);",
"passes.FlushLandscapeAlpha();",
"passes.ClearInteriorDepth();");
string lookIns = MethodBody(source, "private void DrawBuildingLookIns(");
AssertAppearsInOrder(
lookIns,
"passes.DrawLookInPortalPunch(ctx",
"passes.DrawOpaqueCellShells(_shellBatch);",
"passes.DrawCellParticles(ctx");
}
[Fact]
public void Production_uses_retained_routes_without_rebuilding_legacy_partition()
{
string source = RendererSource();
string drawInside = MethodBody(
source,
"public RetailPViewFrameResult DrawInside(");
Assert.Contains(
"if (_sceneFrameProduct is null || LegacyPartitionDiagnosticsEnabled)",
drawInside,
StringComparison.Ordinal);
AssertAppearsInOrder(
drawInside,
"_sceneFrameProduct.BuildAndBorrow(",
"if (_sceneFrameProduct is null || LegacyPartitionDiagnosticsEnabled)",
"InteriorEntityPartition.Partition(");
Assert.Contains(
"RenderFrameRouteOwnerSelector.Replace(",
source,
StringComparison.Ordinal);
Assert.Contains(
"RenderFrameRouteOwnerSelector.ExceptRoute(",
source,
StringComparison.Ordinal);
}
[Fact]
public void Extracted_contracts_retain_no_window_callbacks_or_visibility_owner()
{
@ -360,61 +298,61 @@ public sealed class RetailPViewPassExecutorTests
[Fact]
public void Concrete_executor_forwards_frame_reset_and_brackets_terrain_diagnostics()
{
string source = ExecutorClassSource();
string begin = MethodBody(source, "public void BeginFrame()");
Assert.Contains("_particleClassifications.BeginFrame();", begin);
MethodInfo begin = typeof(RetailPViewPassExecutor).GetMethod(
nameof(RetailPViewPassExecutor.BeginFrame))!;
IReadOnlyList<CompiledCall> beginCalls = CompiledCallGraph.Read(begin);
Assert.True(
CompiledCallGraph.IndexOf(
beginCalls,
typeof(RetailPViewParticleClassifications),
nameof(RetailPViewParticleClassifications.BeginFrame)) >= 0);
string landscape = MethodBody(source, "public void DrawLandscapeSlice(");
AssertAppearsInOrder(
landscape,
"_terrainDiagnostics.Begin();",
"_terrain?.Draw(",
"_terrainDiagnostics.Complete();");
MethodInfo landscape = typeof(RetailPViewPassExecutor).GetMethod(
nameof(RetailPViewPassExecutor.DrawLandscapeSlice))!;
IReadOnlyList<CompiledCall> landscapeCalls = CompiledCallGraph.Read(landscape);
int diagnosticsBegin = RequiredCallIndex(
landscapeCalls,
typeof(TerrainDrawDiagnosticsController),
nameof(TerrainDrawDiagnosticsController.Begin));
int terrainDraw = RequiredCallIndex(
landscapeCalls,
typeof(TerrainModernRenderer),
nameof(TerrainModernRenderer.Draw));
int diagnosticsComplete = RequiredCallIndex(
landscapeCalls,
typeof(TerrainDrawDiagnosticsController),
nameof(TerrainDrawDiagnosticsController.Complete));
Assert.DoesNotContain("Console.WriteLine", source);
Assert.DoesNotContain("Console.WriteLine", RendererSource());
Assert.True(diagnosticsBegin < terrainDraw);
Assert.True(terrainDraw < diagnosticsComplete);
}
[Fact]
public void GameWindow_composes_one_executor_instead_of_a_draw_callback_bag()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"FrameRootComposition.cs"));
MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod(
"ComposeCore",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
Assert.Contains("new RetailPViewPassExecutor(", source);
Assert.Contains("new WorldScenePViewRenderer(", source);
Assert.DoesNotContain("new AcDream.App.Rendering.RetailPViewFrameInput", source);
Assert.DoesNotContain("RetailPViewDrawContext", source);
Assert.DoesNotContain("DrawLandscapeSlice =", source);
Assert.DoesNotContain("DrawExitPortalMasks =", source);
}
int executor = RequiredCallIndex(
calls,
typeof(RetailPViewPassExecutor),
".ctor");
int renderer = RequiredCallIndex(
calls,
typeof(WorldScenePViewRenderer),
".ctor");
private static string RendererSource() => File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"RetailPViewRenderer.cs"));
private static string ExecutorSource() => File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"RetailPViewPassExecutor.cs"));
private static string ExecutorClassSource()
{
string source = ExecutorSource();
int start = source.IndexOf(
"internal sealed class RetailPViewPassExecutor",
StringComparison.Ordinal);
Assert.True(start >= 0, "Missing concrete RetailPViewPassExecutor.");
return source[start..];
Assert.True(executor < renderer);
Assert.Single(
calls,
call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor)
&& call.Target.Name == ".ctor");
Assert.Single(
calls,
call => call.Target.DeclaringType == typeof(WorldScenePViewRenderer)
&& call.Target.Name == ".ctor");
}
private static RetailPViewFrameInput Frame(
@ -565,23 +503,6 @@ public sealed class RetailPViewPassExecutorTests
ParentCellId = parentCellId,
};
private static string MethodBody(string source, string signature)
{
int start = source.IndexOf(signature, StringComparison.Ordinal);
Assert.True(start >= 0, $"Missing method: {signature}");
int open = source.IndexOf('{', start);
int depth = 0;
for (int index = open; index < source.Length; index++)
{
if (source[index] == '{')
depth++;
else if (source[index] == '}' && --depth == 0)
return source[start..(index + 1)];
}
throw new InvalidOperationException($"Unterminated method: {signature}");
}
private static void AssertAppearsInOrder(string source, params string[] needles)
{
int cursor = -1;
@ -593,17 +514,14 @@ public sealed class RetailPViewPassExecutorTests
}
}
private static string FindRepoRoot()
private static int RequiredCallIndex(
IReadOnlyList<CompiledCall> calls,
Type declaringType,
string methodName)
{
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.");
int index = CompiledCallGraph.IndexOf(calls, declaringType, methodName);
Assert.True(index >= 0, $"Missing compiled call: {declaringType.Name}.{methodName}");
return index;
}
private sealed class DictionaryCellSource(

View file

@ -1,7 +1,11 @@
using System.Numerics;
using System.Reflection;
using AcDream.App.Composition;
using AcDream.App.Input;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming;
using AcDream.App.Tests.Architecture;
using AcDream.App.World;
using AcDream.Core.Lighting;
using AcDream.Core.Physics;
@ -290,113 +294,111 @@ public sealed class WorldRenderFrameBuilderTests
Assert.Contains(hiddenLight, lighting.PointSnapshot);
}
[Fact]
public void Production_builder_preserves_the_frame_preparation_order()
{
string source = BuilderSource();
AssertAppearsInOrder(
source,
"WorldCameraFrame camera = _camera.Resolve();",
"_visibility.Begin(in camera, waitingForLogin);",
"_settings.Apply(in camera);",
"WorldRootFrame roots = _roots.Resolve(in camera);",
"_environment.Prepare(in camera, in roots, in foundation, activeDayGroup);",
"_visibility.PublishViewProjection(in camera);",
"HashSet<uint> animated = _animated.Capture();",
"WorldBuildingFrame buildings = _buildings.Gather(");
}
[Fact]
public void Environment_preparation_keeps_lighting_snapshot_before_ubo_upload()
{
string source = BuilderSource();
MethodInfo prepare = typeof(RuntimeWorldFrameEnvironmentPreparation).GetMethod(
nameof(RuntimeWorldFrameEnvironmentPreparation.Prepare))!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(prepare);
AssertAppearsInOrder(
source,
"UpdateSunFromSky(foundation.Sky, roots.PlayerInsideCell);",
"_lighting.UpdateViewerLight(roots.PlayerViewPosition);",
"_lighting.Tick(camera.Position);",
"_lighting.BuildPointLightSnapshot(",
"_dispatcher?.SetSceneLights(_lighting.PointSnapshot);",
"_environmentCells?.SetPointSnapshot(_lighting.PointSnapshot);",
"SceneLightingUbo ubo = SceneLightingUbo.Build(",
"_lightingUbo?.Upload(ubo);",
"RenderingDiagnostics.EmitLight(");
Type[] ownerOrder =
[
typeof(RuntimeWorldFrameEnvironmentPreparation),
typeof(LightManager),
typeof(LightManager),
typeof(LightManager),
typeof(WbDrawDispatcher),
typeof(EnvCellRenderer),
typeof(SceneLightingUbo),
typeof(SceneLightingUboBinding),
typeof(RenderingDiagnostics),
];
string[] methodOrder =
[
"UpdateSunFromSky",
nameof(LightManager.UpdateViewerLight),
nameof(LightManager.Tick),
nameof(LightManager.BuildPointLightSnapshot),
nameof(WbDrawDispatcher.SetSceneLights),
nameof(EnvCellRenderer.SetPointSnapshot),
nameof(SceneLightingUbo.Build),
nameof(SceneLightingUboBinding.Upload),
nameof(RenderingDiagnostics.EmitLight),
];
AssertCompiledCallOrder(calls, ownerOrder, methodOrder);
}
[Fact]
public void World_scene_uses_the_typed_builder_and_orchestrator_owns_its_local_composition()
{
string gameWindow = GameWindowSource();
string lifetime = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"GameWindowLifetime.cs"));
string frameRoot = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"FrameRootComposition.cs"));
string worldScene = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"WorldSceneRenderer.cs"));
MethodInfo[] windowMethods = typeof(GameWindow).GetMethods(
BindingFlags.Instance
| BindingFlags.Static
| BindingFlags.Public
| BindingFlags.NonPublic
| BindingFlags.DeclaredOnly);
Assert.DoesNotContain(
windowMethods,
method => method.Name is "UpdateSunFromSky" or "UpdateSkyPes" or "ParseEnvFloat");
Assert.Equal(
1,
CountOccurrences(worldScene, "_frames.Build("));
Assert.DoesNotContain("private void UpdateSunFromSky(", gameWindow, StringComparison.Ordinal);
Assert.DoesNotContain("private void UpdateSkyPes(", gameWindow, StringComparison.Ordinal);
Assert.DoesNotContain("private static float ParseEnvFloat(", gameWindow, StringComparison.Ordinal);
FieldInfo[] windowFields = typeof(GameWindow).GetFields(
BindingFlags.Instance
| BindingFlags.Static
| BindingFlags.Public
| BindingFlags.NonPublic
| BindingFlags.DeclaredOnly);
Assert.DoesNotContain(
"private AcDream.App.Rendering.WorldRenderFrameBuilder?",
gameWindow,
StringComparison.Ordinal);
Assert.DoesNotContain(
"private AcDream.App.Rendering.SkyPesFrameController?",
gameWindow,
StringComparison.Ordinal);
windowFields,
field => field.FieldType == typeof(WorldRenderFrameBuilder)
|| field.FieldType == typeof(SkyPesFrameController));
// Gate-fix round (2026-08-17): the renderer's own IsWaitingForLogin
// short-circuit is deleted — the frame gate
// (LocalPlayerTeleportRenderStateSource) folds the waiting state
// into PortalViewportVisible, so the portal-visible return above
// covers every waiting frame (one gate computes, this phase
// enforces).
AssertAppearsInOrder(
worldScene,
"_alpha.BeginFrame();",
"_frames.Build(",
"_passes.DrawFlatTerrain(",
"NormalWorldDrawn: true");
AssertAppearsInOrder(
frameRoot,
"var skyPesFrame = new SkyPesFrameController(",
"var worldRenderFrameBuilder =",
"new RenderFrameOrchestrator(",
"d.FrameGraphs.PublishOwned(");
AssertAppearsInOrder(
lifetime,
"frame.FrameGraphPublication?.Dispose()",
"Hard(\"equipped children\"",
"Hard(\"effect network state\"",
"Hard(\"audio\"",
"Hard(\"mesh draw dispatcher\"",
"Hard(\"environment cells\"",
"Hard(\"scene lighting\"");
}
MethodInfo render = typeof(WorldSceneRenderer).GetMethod(
nameof(WorldSceneRenderer.Render))!;
IReadOnlyList<CompiledCall> renderCalls = CompiledCallGraph.Read(render);
AssertCompiledCallOrder(
renderCalls,
[
typeof(IWorldSceneAlphaFrame),
typeof(IWorldRenderFrameBuilder),
typeof(IWorldScenePassExecutor),
typeof(WorldRenderFrameOutcome),
],
[
nameof(IWorldSceneAlphaFrame.BeginFrame),
nameof(IWorldRenderFrameBuilder.Build),
nameof(IWorldScenePassExecutor.DrawFlatTerrain),
".ctor",
]);
Assert.Single(
renderCalls,
call => call.Target.DeclaringType == typeof(IWorldRenderFrameBuilder)
&& call.Target.Name == nameof(IWorldRenderFrameBuilder.Build));
private static string BuilderSource() => File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"WorldRenderFrameBuilder.cs"));
MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod(
"ComposeCore",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> composeCalls = CompiledCallGraph.Read(compose);
AssertCompiledCallOrder(
composeCalls,
[
typeof(SkyPesFrameController),
typeof(WorldRenderFrameBuilder),
typeof(RenderFrameOrchestrator),
typeof(GameFrameGraphSlot),
],
[".ctor", ".ctor", ".ctor", nameof(GameFrameGraphSlot.PublishOwned)]);
// The former source assertion also mixed in teardown ordering. That
// distinct lifetime contract remains covered by the dedicated lifetime
// suite and is reconciled with the later host/lifetime R3 batch.
}
private static WorldCameraFrame CameraFrame(FlyCamera camera) => new(
camera,
@ -419,51 +421,27 @@ public sealed class WorldRenderFrameBuilderTests
seenOutside,
containmentBsp: null);
private static string GameWindowSource() => File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
private static int CountOccurrences(string source, string needle)
{
int count = 0;
int cursor = 0;
while ((cursor = source.IndexOf(needle, cursor, StringComparison.Ordinal)) >= 0)
{
count++;
cursor += needle.Length;
}
return count;
}
private static void AssertAppearsInOrder(string source, params string[] needles)
private static void AssertCompiledCallOrder(
IReadOnlyList<CompiledCall> calls,
IReadOnlyList<Type> declaringTypes,
IReadOnlyList<string> methodNames)
{
Assert.Equal(declaringTypes.Count, methodNames.Count);
int cursor = -1;
foreach (string needle in needles)
for (int index = 0; index < declaringTypes.Count; index++)
{
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}");
int next = CompiledCallGraph.IndexOf(
calls,
declaringTypes[index],
methodNames[index],
cursor + 1);
Assert.True(
next > cursor,
$"Missing or out-of-order compiled call: {declaringTypes[index].Name}.{methodNames[index]}");
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 RecordingCamera(
List<string> calls,
WorldCameraFrame result) : IWorldFrameCameraSource

View file

@ -1,9 +1,11 @@
using System.Numerics;
using System.Reflection;
using AcDream.App.Composition;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Selection;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
using AcDream.App.Tests.Architecture;
using AcDream.Core.Rendering;
using AcDream.Core.World;
using AcDream.Runtime;
@ -326,20 +328,46 @@ public sealed class WorldSceneRendererTests
[Fact]
public void GameWindow_DelegatesWorldRenderingWithoutOwningDrawBranches()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"FrameRootComposition.cs"));
MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod(
"ComposeCore",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
Assert.Contains("new WorldSceneRenderer(", source);
Assert.Contains("new RenderFrameOrchestrator(", source);
Assert.DoesNotContain("_worldSceneRenderer", source);
Assert.DoesNotContain("_retailPViewRenderer.DrawInside(", source);
Assert.DoesNotContain("_retailAlphaQueue.BeginFrame();", source);
Assert.DoesNotContain("_terrainDrawDiagnostics!.Begin();", source);
Assert.DoesNotContain("SkipWorldGeometry:", source);
int worldRenderer = RequiredCallIndex(
calls,
typeof(WorldSceneRenderer),
".ctor");
int frameOrchestrator = RequiredCallIndex(
calls,
typeof(RenderFrameOrchestrator),
".ctor");
Assert.True(worldRenderer < frameOrchestrator);
Assert.Single(
calls,
call => call.Target.DeclaringType == typeof(WorldSceneRenderer)
&& call.Target.Name == ".ctor");
Assert.Single(
calls,
call => call.Target.DeclaringType == typeof(RenderFrameOrchestrator)
&& call.Target.Name == ".ctor");
FieldInfo[] windowFields = typeof(GameWindow).GetFields(
BindingFlags.Instance
| BindingFlags.Public
| BindingFlags.NonPublic
| BindingFlags.DeclaredOnly);
Assert.DoesNotContain(
windowFields,
field => field.FieldType == typeof(WorldSceneRenderer)
|| field.FieldType == typeof(RetailPViewRenderer)
|| field.FieldType == typeof(TerrainDrawDiagnosticsController)
|| field.FieldType == typeof(RenderFrameOrchestrator));
FieldInfo worldPhase = Assert.Single(
typeof(RenderFrameOrchestrator).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic),
field => field.FieldType == typeof(IWorldSceneFramePhase));
Assert.Equal("_world", worldPhase.Name);
}
[Fact]
@ -771,28 +799,13 @@ public sealed class WorldSceneRendererTests
[]);
}
private static int CountOccurrences(string source, string value)
private static int RequiredCallIndex(
IReadOnlyList<CompiledCall> calls,
Type declaringType,
string methodName)
{
int count = 0;
int cursor = 0;
while ((cursor = source.IndexOf(value, cursor, StringComparison.Ordinal)) >= 0)
{
count++;
cursor += value.Length;
}
return count;
}
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.");
int index = CompiledCallGraph.IndexOf(calls, declaringType, methodName);
Assert.True(index >= 0, $"Missing compiled call: {declaringType.Name}.{methodName}");
return index;
}
}