292 lines
9.7 KiB
C#
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.");
|
|
}
|
|
}
|