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 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 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 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 | | Syntax signal | Current count |
|---|---:| |---|---:|
| Tracked/parsed C# test files | 1,256 / 1,256 | | Tracked/parsed C# test files | 1,254 / 1,254 |
| Attributed test methods (not expanded theory cases) | 11,447 | | Attributed test methods (not expanded theory cases) | 11,416 |
| Exact duplicate-data rows | 0 | | Exact duplicate-data rows | 0 |
| Reviewed body-equivalent groups / methods | 11 / 27 | | Reviewed body-equivalent groups / methods | 11 / 27 |
| Methods containing at least one empty `return;` | 138 | | 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 | | Methods directly using `Thread.Sleep` / `Task.Delay` | 15 / 14 |
| Cancellable-infinite-only / elapsed-time methods | 5 / 24 | | Cancellable-infinite-only / elapsed-time methods | 5 / 24 |
| Methods directly reading environment variables | 47 | | 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 The three remaining prerequisite-return candidates are all reviewed branch
false positives: the two Windows/Linux factory assertions and the launcher's 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-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-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-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-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-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`. | | 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 and
- the no-retry complete hermetic Release gate passes 14,351/14,351 with zero - 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. 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<GpuRecordedCall> _calls = [];
private readonly List<Action> _queuedActions = []; 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 Dictionary<GpuSamplerDescription, RecordingGpuSampler> _samplers = [];
private readonly byte[] _ring; private readonly byte[] _ring;
private readonly Stack<uint> _freeTextureSlots = new(); private readonly Stack<uint> _freeTextureSlots = new();
@ -151,8 +154,18 @@ internal sealed class RecordingGpuDevice : IGpuDevice
public void Clear() => _calls.Clear(); public void Clear() => _calls.Clear();
public IGpuBuffer CreateBuffer(in GpuBufferDescription description) => public IReadOnlyList<RecordingGpuBuffer> CreatedBuffers => _createdBuffers;
new RecordingGpuBuffer(description);
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> /// <summary>
/// Campaign V slice V6i-2: every image this device made, in creation order. /// 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); RecordingGpuSampler created = new(description);
_samplers.Add(description, created); _samplers.Add(description, created);
_createdSamplers.Add(created);
return created; return created;
} }
public IGpuPipeline CreatePipeline(GpuPipelineDescription description) public IGpuPipeline CreatePipeline(GpuPipelineDescription description)
{ {
ArgumentNullException.ThrowIfNull(description); ArgumentNullException.ThrowIfNull(description);
return new RecordingGpuPipeline(description); var pipeline = new RecordingGpuPipeline(description);
_createdPipelines.Add(pipeline);
return pipeline;
} }
public IGpuRenderTarget CreateRenderTarget(in GpuRenderTargetDescription description) => public IGpuRenderTarget CreateRenderTarget(in GpuRenderTargetDescription description) =>

View file

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

View file

@ -1,7 +1,9 @@
using System.Numerics; using System.Numerics;
using System.Reflection; using System.Reflection;
using AcDream.App.Composition;
using AcDream.App.Rendering; using AcDream.App.Rendering;
using AcDream.App.Rendering.Scene; using AcDream.App.Rendering.Scene;
using AcDream.App.Tests.Architecture;
using AcDream.Core.World; using AcDream.Core.World;
namespace AcDream.App.Tests.Rendering; namespace AcDream.App.Tests.Rendering;
@ -219,7 +221,7 @@ public sealed class RetailPViewPassExecutorTests
} }
[Fact] [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); LoadedCell interior = InteriorWithExit(0xA9B40100u);
WorldEntity dynamic = Entity( WorldEntity dynamic = Entity(
@ -274,70 +276,6 @@ public sealed class RetailPViewPassExecutorTests
Assert.Contains("look-in-punch", executor.Operations); 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] [Fact]
public void Extracted_contracts_retain_no_window_callbacks_or_visibility_owner() public void Extracted_contracts_retain_no_window_callbacks_or_visibility_owner()
{ {
@ -360,61 +298,61 @@ public sealed class RetailPViewPassExecutorTests
[Fact] [Fact]
public void Concrete_executor_forwards_frame_reset_and_brackets_terrain_diagnostics() public void Concrete_executor_forwards_frame_reset_and_brackets_terrain_diagnostics()
{ {
string source = ExecutorClassSource(); MethodInfo begin = typeof(RetailPViewPassExecutor).GetMethod(
string begin = MethodBody(source, "public void BeginFrame()"); nameof(RetailPViewPassExecutor.BeginFrame))!;
Assert.Contains("_particleClassifications.BeginFrame();", begin); IReadOnlyList<CompiledCall> beginCalls = CompiledCallGraph.Read(begin);
Assert.True(
CompiledCallGraph.IndexOf(
beginCalls,
typeof(RetailPViewParticleClassifications),
nameof(RetailPViewParticleClassifications.BeginFrame)) >= 0);
string landscape = MethodBody(source, "public void DrawLandscapeSlice("); MethodInfo landscape = typeof(RetailPViewPassExecutor).GetMethod(
AssertAppearsInOrder( nameof(RetailPViewPassExecutor.DrawLandscapeSlice))!;
landscape, IReadOnlyList<CompiledCall> landscapeCalls = CompiledCallGraph.Read(landscape);
"_terrainDiagnostics.Begin();", int diagnosticsBegin = RequiredCallIndex(
"_terrain?.Draw(", landscapeCalls,
"_terrainDiagnostics.Complete();"); 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.True(diagnosticsBegin < terrainDraw);
Assert.DoesNotContain("Console.WriteLine", RendererSource()); Assert.True(terrainDraw < diagnosticsComplete);
} }
[Fact] [Fact]
public void GameWindow_composes_one_executor_instead_of_a_draw_callback_bag() public void GameWindow_composes_one_executor_instead_of_a_draw_callback_bag()
{ {
string source = File.ReadAllText(Path.Combine( MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod(
FindRepoRoot(), "ComposeCore",
"src", BindingFlags.Instance | BindingFlags.NonPublic)!;
"AcDream.App", IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
"Composition",
"FrameRootComposition.cs"));
Assert.Contains("new RetailPViewPassExecutor(", source); int executor = RequiredCallIndex(
Assert.Contains("new WorldScenePViewRenderer(", source); calls,
Assert.DoesNotContain("new AcDream.App.Rendering.RetailPViewFrameInput", source); typeof(RetailPViewPassExecutor),
Assert.DoesNotContain("RetailPViewDrawContext", source); ".ctor");
Assert.DoesNotContain("DrawLandscapeSlice =", source); int renderer = RequiredCallIndex(
Assert.DoesNotContain("DrawExitPortalMasks =", source); calls,
} typeof(WorldScenePViewRenderer),
".ctor");
private static string RendererSource() => File.ReadAllText(Path.Combine( Assert.True(executor < renderer);
FindRepoRoot(), Assert.Single(
"src", calls,
"AcDream.App", call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor)
"Rendering", && call.Target.Name == ".ctor");
"RetailPViewRenderer.cs")); Assert.Single(
calls,
private static string ExecutorSource() => File.ReadAllText(Path.Combine( call => call.Target.DeclaringType == typeof(WorldScenePViewRenderer)
FindRepoRoot(), && call.Target.Name == ".ctor");
"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..];
} }
private static RetailPViewFrameInput Frame( private static RetailPViewFrameInput Frame(
@ -565,23 +503,6 @@ public sealed class RetailPViewPassExecutorTests
ParentCellId = parentCellId, 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) private static void AssertAppearsInOrder(string source, params string[] needles)
{ {
int cursor = -1; 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); int index = CompiledCallGraph.IndexOf(calls, declaringType, methodName);
while (directory is not null) Assert.True(index >= 0, $"Missing compiled call: {declaringType.Name}.{methodName}");
{ return index;
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 DictionaryCellSource( private sealed class DictionaryCellSource(

View file

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

View file

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