acdream/tests/AcDream.App.Tests/Rendering/WorldRenderDiagnosticsTests.cs

292 lines
9.7 KiB
C#

using System.Numerics;
using System.Reflection;
using AcDream.App.Rendering;
using AcDream.Core.Vfx;
namespace AcDream.App.Tests.Rendering;
public sealed class WorldRenderDiagnosticsTests
{
[Fact]
public void GlTripwire_DisabledPerformsNoGlRead()
{
var gl = new RecordingGlStateReader();
var log = new RecordingLog();
var diagnostics = new WorldRenderDiagnostics(gl, log);
diagnostics.EmitGlStateTripwireIfChanged(enabled: false);
Assert.Equal(0, gl.StateCaptureCount);
Assert.Empty(log.Messages);
}
[Fact]
public void GlTripwire_LogsOnlyChangesAndReportsStableFrameCount()
{
var gl = new RecordingGlStateReader
{
State = State(depthFunction: 0x201),
};
var log = new RecordingLog();
var diagnostics = new WorldRenderDiagnostics(gl, log);
diagnostics.EmitGlStateTripwireIfChanged(enabled: true);
diagnostics.EmitGlStateTripwireIfChanged(enabled: true);
gl.State = State(depthFunction: 0x203);
diagnostics.EmitGlStateTripwireIfChanged(enabled: true);
Assert.Equal(3, gl.StateCaptureCount);
Assert.Collection(
log.Messages,
first =>
{
Assert.StartsWith("[gl-state] frame=1 stable=0", first);
Assert.Contains("dfunc=0x201", first);
},
changed =>
{
Assert.StartsWith("[gl-state] frame=3 stable=1", changed);
Assert.Contains("dfunc=0x203", changed);
});
}
[Fact]
public void ScissorProbe_SequenceAdvancesAcrossSuppressedDuplicates()
{
var gl = new RecordingGlStateReader
{
Scissor = new RenderGlScissorSnapshot(
true,
new IntRenderRectangle(1, 2, 3, 4)),
};
var log = new RecordingLog();
var diagnostics = new WorldRenderDiagnostics(gl, log);
diagnostics.EmitClipRouteScissorProbe(true, true, new Vector4(-1, -1, 1, 1));
diagnostics.EmitClipRouteScissorProbe(true, true, new Vector4(-1, -1, 1, 1));
gl.Scissor = new RenderGlScissorSnapshot(
true,
new IntRenderRectangle(5, 6, 7, 8));
diagnostics.EmitClipRouteScissorProbe(true, true, new Vector4(-1, -1, 1, 1));
Assert.Equal(3, gl.ScissorCaptureCount);
Assert.Collection(
log.Messages,
first => Assert.StartsWith("[clip-route-scis] n=1", first),
changed => Assert.StartsWith("[clip-route-scis] n=3", changed));
}
[Fact]
public void Owner_RetainsNoBorrowedFrameProductsOrBroadRuntimeOwners()
{
FieldInfo[] fields = typeof(WorldRenderDiagnostics).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic);
Type[] forbidden =
[
typeof(GameWindow),
typeof(PortalVisibilityFrame),
typeof(ClipFrameAssembly),
typeof(InteriorEntityPartition.Result),
typeof(AcDream.Core.Physics.PhysicsEngine),
typeof(CameraController),
typeof(CellVisibility),
];
foreach (Type type in forbidden)
Assert.DoesNotContain(fields, field => field.FieldType == type);
Assert.DoesNotContain(
fields,
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
}
[Fact]
public void SilkGlReader_ConsumesEnteringErrorBeforeStateQueries()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"WorldRenderDiagnostics.cs"));
int captureStart = source.IndexOf(
"public RenderGlStateSnapshot CaptureState()",
StringComparison.Ordinal);
int error = source.IndexOf("_gl.GetError()", captureStart, StringComparison.Ordinal);
int depth = source.IndexOf(
"_gl.IsEnabled(EnableCap.DepthTest)",
captureStart,
StringComparison.Ordinal);
Assert.True(captureStart >= 0);
Assert.True(error > captureStart);
Assert.True(depth > error);
}
[Fact]
public void TerrainDiagnostics_RetryFailedPublicationWithoutLosingSamples()
{
var log = new ThrowOnceLog();
var diagnostics = new WorldRenderDiagnostics(new RecordingGlStateReader(), log);
var facts = new TerrainRenderDiagnosticFacts(3, 5, 8);
diagnostics.BeginTerrainDraw();
diagnostics.EndTerrainDraw();
Assert.Throws<InvalidOperationException>(() =>
diagnostics.PublishTerrainDiagnostics(facts));
diagnostics.BeginTerrainDraw();
diagnostics.EndTerrainDraw();
diagnostics.PublishTerrainDiagnostics(facts);
Assert.Single(log.Messages);
Assert.Contains("draws=3/frame", log.Messages[0]);
Assert.Contains("visible=3", log.Messages[0]);
Assert.Contains("loaded=5", log.Messages[0]);
Assert.Contains("capacity=8", log.Messages[0]);
}
[Fact]
public void RenderSignature_LogsOnlyChangesAndCarriesStableCount()
{
var log = new RecordingLog();
var diagnostics = new WorldRenderDiagnostics(new RecordingGlStateReader(), log);
EmitMinimalSignature(diagnostics, "flat");
EmitMinimalSignature(diagnostics, "flat");
EmitMinimalSignature(diagnostics, "pview");
Assert.Collection(
log.Messages,
first => Assert.StartsWith("[render-sig] frame=1 stable=0 branch=flat", first),
changed => Assert.StartsWith("[render-sig] frame=3 stable=1 branch=pview", changed));
}
[Fact]
public void OutStageParticleProbe_IsDisabledWithoutEnumerationAndSuppressesDuplicates()
{
var log = new RecordingLog();
var diagnostics = new WorldRenderDiagnostics(new RecordingGlStateReader(), log);
var particles = new ParticleSystem(new EmitterDescRegistry());
diagnostics.EmitOutStageParticles(false, particles, new HashSet<uint>());
diagnostics.EmitOutStageParticles(true, particles, new HashSet<uint>());
diagnostics.EmitOutStageParticles(true, particles, new HashSet<uint>());
Assert.Single(log.Messages);
Assert.Equal(
"[outstage-pt] ids=0 attachedEmitters=0 matched=0 unattached=0 matchedIds=[] unmatchedIds=[]",
log.Messages[0]);
}
private static RenderGlStateSnapshot State(int depthFunction) => new(
DepthTest: true,
DepthWrite: true,
DepthFunction: depthFunction,
Blend: false,
BlendSource: 1,
BlendDestination: 0,
CullFace: true,
CullMode: 0x405,
FrontFace: 0x900,
Scissor: false,
ScissorBox: new IntRenderRectangle(0, 0, 1280, 720),
Viewport: new IntRenderRectangle(0, 0, 1280, 720),
DrawFramebuffer: 0,
AlphaToCoverage: false,
Stencil: false,
ClipBits: 0,
Error: 0);
private sealed class RecordingGlStateReader : IRenderGlStateReader
{
public RenderGlStateSnapshot State { get; set; } = WorldRenderDiagnosticsTests.State(0x201);
public RenderGlScissorSnapshot Scissor { get; set; }
public int StateCaptureCount { get; private set; }
public int ScissorCaptureCount { get; private set; }
public RenderGlStateSnapshot CaptureState()
{
StateCaptureCount++;
return State;
}
public RenderGlScissorSnapshot CaptureScissor()
{
ScissorCaptureCount++;
return Scissor;
}
}
private sealed class RecordingLog : IRenderFrameDiagnosticLog
{
public List<string> Messages { get; } = [];
public void WriteLine(string message) => Messages.Add(message);
}
private sealed class ThrowOnceLog : IRenderFrameDiagnosticLog
{
private bool _throw = true;
public List<string> Messages { get; } = [];
public void WriteLine(string message)
{
if (_throw)
{
_throw = false;
throw new InvalidOperationException("diagnostic sink failed");
}
Messages.Add(message);
}
}
private static void EmitMinimalSignature(
WorldRenderDiagnostics diagnostics,
string branch) =>
diagnostics.EmitRenderSignatureIfChanged(
enabled: true,
branch: branch,
clipRoot: null,
viewerRoot: null,
playerRoot: null,
viewerCellId: 0,
playerCellId: 0,
playerIndoorGate: false,
cameraInsideCell: false,
renderSkyGate: false,
drawSkyThisFrame: false,
terrainDrawn: false,
terrainClipMode: TerrainClipMode.Skip,
skyDrawn: false,
depthClear: false,
outdoorSceneryDrawn: false,
outdoorPortalDrawn: false,
outdoorRootObjectCount: 0,
liveDynamicDrawnCount: 0,
sceneParticles: "none",
portalFrame: null,
clipAssembly: null,
drawableCells: null,
partition: null,
exteriorPortalFrame: null,
exteriorClipAssembly: null,
exteriorDrawableCells: null,
exteriorPartition: null,
cameraPosition: Vector3.Zero,
playerPosition: Vector3.Zero);
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.");
}
}