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(() => 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()); diagnostics.EmitOutStageParticles(true, particles, new HashSet()); diagnostics.EmitOutStageParticles(true, particles, new HashSet()); 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 Messages { get; } = []; public void WriteLine(string message) => Messages.Add(message); } private sealed class ThrowOnceLog : IRenderFrameDiagnosticLog { private bool _throw = true; public List 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."); } }