using System.Numerics; using System.Diagnostics; using System.Text; using AcDream.Core.Vfx; using AcDream.Core.World; namespace AcDream.App.Rendering; internal readonly record struct IntRenderRectangle(int X, int Y, int Width, int Height); internal readonly record struct RenderGlStateSnapshot( bool DepthTest, bool DepthWrite, int DepthFunction, bool Blend, int BlendSource, int BlendDestination, bool CullFace, int CullMode, int FrontFace, bool Scissor, IntRenderRectangle ScissorBox, IntRenderRectangle Viewport, int DrawFramebuffer, bool AlphaToCoverage, bool Stencil, int ClipBits, int Error); internal readonly record struct RenderGlScissorSnapshot( bool Enabled, IntRenderRectangle Box); internal readonly record struct TerrainRenderDiagnosticFacts( int VisibleSlots, int Draws, int LoadedSlots, int CapacitySlots); internal interface IRenderGlStateReader { RenderGlStateSnapshot CaptureState(); RenderGlScissorSnapshot CaptureScissor(); } /// /// Owns print-on-change world-render probes and their reusable scratch. Inputs /// are borrowed for one call; the owner retains only copied signatures and IDs. /// internal sealed class WorldRenderDiagnostics { private readonly IRenderGlStateReader _gl; private readonly IRenderFrameDiagnosticLog _log; private readonly Stopwatch _terrainStopwatch = new(); private readonly RollingTimingSampleWindow _terrainSamples = new(256); private string? _lastRenderSignature; private int _renderSignatureFrame; private int _renderSignatureStableFrames; private string? _lastGlStateSignature; private long _glStateFrame; private long _glStateStableFrames; private string? _lastPostWorldGlStateSignature; private long _postWorldGlStateFrame; private long _postWorldGlStateStableFrames; private string? _lastScissorSignature; private long _scissorSequence; private string? _lastClipRouteSignature; private long _clipRouteSequence; private readonly List _clipRouteCellKeys = []; public WorldRenderDiagnostics( IRenderGlStateReader gl, IRenderFrameDiagnosticLog log) { _gl = gl ?? throw new ArgumentNullException(nameof(gl)); _log = log ?? throw new ArgumentNullException(nameof(log)); } public void BeginTerrainDraw() => _terrainStopwatch.Restart(); public void EndTerrainDraw() { _terrainStopwatch.Stop(); _terrainSamples.PushHundredthsMicroseconds( (long)(_terrainStopwatch.Elapsed.TotalMicroseconds * 100.0)); } /// S3 chunk 3 fix round 1 (F3): the walk path's per-frame /// analogue of — pushes ONE precomputed /// elapsed-time sample (the sum of every land-cell batch's own /// Stopwatch.GetTimestamp() delta this frame, converted by the caller) /// instead of stopping this owner's own stopwatch, since the walk path /// times many small batches per frame rather than one bracketed call. public void PushTerrainSample(long elapsedHundredthsMicroseconds) => _terrainSamples.PushHundredthsMicroseconds(elapsedHundredthsMicroseconds); public void PublishTerrainDiagnostics(TerrainRenderDiagnosticFacts facts) { RollingTimingPercentiles timing = _terrainSamples.Snapshot(); double medianMicroseconds = timing.MedianHundredthsMicroseconds / 100.0; double p95Microseconds = timing.Percentile95HundredthsMicroseconds / 100.0; string budget = medianMicroseconds > 1000.0 ? " BUDGET_OVER" : string.Empty; _log.WriteLine( $"[TERRAIN-DIAG]{budget} cpu_us={medianMicroseconds:F2}m/" + $"{p95Microseconds:F2}p95 draws={facts.Draws}/frame " + $"visible={facts.VisibleSlots} loaded={facts.LoadedSlots} " + $"capacity={facts.CapacitySlots}"); } public void EmitGlStateTripwireIfChanged(bool enabled) { if (!enabled) return; _glStateFrame++; string signature = FormatGlState(_gl.CaptureState()); if (signature == _lastGlStateSignature) { _glStateStableFrames++; return; } _log.WriteLine( $"[gl-state] frame={_glStateFrame} stable={_glStateStableFrames} {signature}"); _lastGlStateSignature = signature; _glStateStableFrames = 0; } /// /// Second sample of the same snapshot, taken at the END of the normal-world /// phase instead of at the frame clear. /// /// /// samples immediately after the /// clear phase has run RestoreFrameDefaults, so it can only observe /// state that survives from one frame into the next. State that a world pass /// establishes and something in private presentation puts back before the /// next clear is invisible to it — including the draw framebuffer, which no /// frame-global restore touches. Sampling here as well brackets the world /// phase, so a binding that the world's geometry drew into but the retained /// UI did not shows up as a difference between the two lines rather than as /// no line at all. /// public void EmitPostWorldGlStateIfChanged(bool enabled) { if (!enabled) return; _postWorldGlStateFrame++; string signature = FormatGlState(_gl.CaptureState()); if (signature == _lastPostWorldGlStateSignature) { _postWorldGlStateStableFrames++; return; } _log.WriteLine( $"[gl-state-postworld] frame={_postWorldGlStateFrame} " + $"stable={_postWorldGlStateStableFrames} {signature}"); _lastPostWorldGlStateSignature = signature; _postWorldGlStateStableFrames = 0; } public void EmitClipRouteScissorProbe( bool enabled, bool applied, Vector4 ndcAabb) { if (!enabled) return; RenderGlScissorSnapshot snapshot = _gl.CaptureScissor(); string signature = FormattableString.Invariant( $"applied={(applied ? 1 : 0)} scis={(snapshot.Enabled ? 1 : 0)} box=({snapshot.Box.X},{snapshot.Box.Y},{snapshot.Box.Width},{snapshot.Box.Height}) ndc=({ndcAabb.X:F3},{ndcAabb.Y:F3},{ndcAabb.Z:F3},{ndcAabb.W:F3})"); _scissorSequence++; if (signature == _lastScissorSignature) return; _lastScissorSignature = signature; _log.WriteLine($"[clip-route-scis] n={_scissorSequence} {signature}"); } public void EmitClipRouteProbe( bool enabled, ClipFrame clipFrame, ClipFrameAssembly clipAssembly, ClipViewSlice slice, int sliceIndex) { if (!enabled) return; var text = new StringBuilder(256); text.Append(FormattableString.Invariant( $"slice={sliceIndex}/{clipAssembly.OutsideViewSlices.Length} slot={slice.Slot}")); text.Append(FormattableString.Invariant( $" ndc=({slice.NdcAabb.X:F3},{slice.NdcAabb.Y:F3},{slice.NdcAabb.Z:F3},{slice.NdcAabb.W:F3})")); text.Append(FormattableString.Invariant($" planes={slice.Planes.Length}[")); for (int i = 0; i < slice.Planes.Length; i++) { Vector4 plane = slice.Planes[i]; if (i > 0) text.Append(' '); text.Append(FormattableString.Invariant( $"({plane.X:F3},{plane.Y:F3},{plane.Z:F3},{plane.W:F3})")); } text.Append("] cells={"); _clipRouteCellKeys.Clear(); foreach (uint key in clipAssembly.CellIdToSlot.Keys) _clipRouteCellKeys.Add(key); _clipRouteCellKeys.Sort(); for (int i = 0; i < _clipRouteCellKeys.Count; i++) { if (i > 0) text.Append(','); text.Append(FormattableString.Invariant( $"0x{_clipRouteCellKeys[i]:X8}:{clipAssembly.CellIdToSlot[_clipRouteCellKeys[i]]}")); } text.Append('}'); ReadOnlySpan regionBytes = clipFrame.RegionBytesForTest; int offset = slice.Slot * ClipFrame.CellClipStrideBytes; if (offset >= 0 && offset + ClipFrame.CellClipStrideBytes <= regionBytes.Length) { uint count = BitConverter.ToUInt32(regionBytes.Slice(offset, 4)); text.Append(FormattableString.Invariant($" ssbo[{slice.Slot}]: n={count}")); int planeCount = (int)Math.Min(count, (uint)ClipFrame.MaxPlanes); for (int i = 0; i < planeCount; i++) { int planeOffset = offset + ClipFrame.CellClipPlanesOffset + i * 16; float x = BitConverter.ToSingle(regionBytes.Slice(planeOffset, 4)); float y = BitConverter.ToSingle(regionBytes.Slice(planeOffset + 4, 4)); float z = BitConverter.ToSingle(regionBytes.Slice(planeOffset + 8, 4)); float w = BitConverter.ToSingle(regionBytes.Slice(planeOffset + 12, 4)); text.Append(FormattableString.Invariant($" ({x:F3},{y:F3},{z:F3},{w:F3})")); } } else { text.Append(FormattableString.Invariant( $" ssbo[{slice.Slot}]: OUT-OF-RANGE len={regionBytes.Length}")); } ReadOnlySpan terrainBytes = clipFrame.TerrainBytesForTest; int terrainCount = BitConverter.ToInt32(terrainBytes[..4]); float p0 = BitConverter.ToSingle(terrainBytes.Slice(16, 4)); float p1 = BitConverter.ToSingle(terrainBytes.Slice(20, 4)); float p2 = BitConverter.ToSingle(terrainBytes.Slice(24, 4)); float p3 = BitConverter.ToSingle(terrainBytes.Slice(28, 4)); text.Append(FormattableString.Invariant( $" ubo: n={terrainCount} p0=({p0:F3},{p1:F3},{p2:F3},{p3:F3})")); string signature = text.ToString(); _clipRouteSequence++; if (signature == _lastClipRouteSignature) return; _lastClipRouteSignature = signature; _log.WriteLine($"[clip-route] n={_clipRouteSequence} {signature}"); } public void EmitSeamMask( bool enabled, IReadOnlySet targetCells, uint cellId, int portalIndex, bool forceFarZ, ReadOnlySpan vertices) { if (!enabled || !targetCells.Contains(cellId)) return; float minimumZ = float.MaxValue; float maximumZ = float.MinValue; foreach (Vector3 vertex in vertices) { minimumZ = Math.Min(minimumZ, vertex.Z); maximumZ = Math.Max(maximumZ, vertex.Z); } _log.WriteLine(FormattableString.Invariant( $"[seam-mask] t={Environment.TickCount64} cell=0x{cellId:X8} portal={portalIndex} far={forceFarZ} n={vertices.Length} z=[{minimumZ:F3},{maximumZ:F3}]")); } public void EmitPViewInput( bool enabled, IReadOnlySet visibleCells, int outsideViewCount, Matrix4x4 viewProjection, bool outdoorRoot, Vector3 eye, Vector3 player, Vector3 rawPlayer, float yaw, float? terrainHeight) { if (!enabled) return; string terrain = terrainHeight is { } height ? FormattableString.Invariant( $"terrZ={height:F3} eyeAbove={eye.Z - height:F3}") : "terrZ=n/a eyeAbove=n/a"; char root = outdoorRoot ? 'Y' : 'n'; Matrix4x4 vp = viewProjection; _log.WriteLine(FormattableString.Invariant( $"[pv-input] outRoot={root} visible={visibleCells.Count} outsideViews={outsideViewCount} eye=({eye.X:F6},{eye.Y:F6},{eye.Z:F6}) player=({player.X:F6},{player.Y:F6},{player.Z:F6}) rawPlayer=({rawPlayer.X:F6},{rawPlayer.Y:F6},{rawPlayer.Z:F6}) yaw={yaw:F8} {terrain} vp=[{vp.M11:F6} {vp.M13:F6} {vp.M22:F6} {vp.M31:F6} {vp.M33:F6} {vp.M41:F6} {vp.M42:F6} {vp.M43:F6}]")); } public void EmitRetailPViewDiagnostics( bool visibilityEnabled, bool flapEnabled, RetailPViewFrameResult result, LoadedCell clipRoot, uint viewerCellId, uint playerCellId, Vector3 cameraPosition, Vector3 playerPosition, CameraCellResolution cameraCellResolution) { if (visibilityEnabled) { AcDream.Core.Rendering.RenderingDiagnostics.EmitVis( clipRoot.CellId, result.VisibleCells.OrderBy(static id => id).ToArray(), result.ClipAssembly.OutsideViewSlices.Length, result.ClipAssembly.OutsidePlaneCount, result.ClipAssembly.PerCellPlaneCounts, result.ClipAssembly.ScissorFallbacks); } if (flapEnabled) { bool eyeInRoot = CellVisibility.PointInCell(cameraPosition, clipRoot); bool playerInRoot = CellVisibility.PointInCell(playerPosition, clipRoot); _log.WriteLine( $"[flap-cam] root=0x{clipRoot.CellId:X8} " + $"viewerCell=0x{viewerCellId:X8} playerCell=0x{playerCellId:X8} " + $"res={cameraCellResolution} " + $"eyeInRoot={(eyeInRoot ? "Y" : "n")} " + $"playerInRoot={(playerInRoot ? "Y" : "n")} " + $"eye=({cameraPosition.X:F2},{cameraPosition.Y:F2},{cameraPosition.Z:F2}) " + $"player=({playerPosition.X:F2},{playerPosition.Y:F2},{playerPosition.Z:F2}) " + $"terrain={result.ClipAssembly.TerrainMode} " + $"outVisible={result.ClipAssembly.OutdoorVisible}"); } } public void EmitRenderSignatureIfChanged( bool enabled, string branch, LoadedCell? clipRoot, LoadedCell? viewerRoot, LoadedCell? playerRoot, uint viewerCellId, uint playerCellId, bool playerIndoorGate, bool cameraInsideCell, bool renderSkyGate, bool drawSkyThisFrame, bool terrainDrawn, TerrainClipMode terrainClipMode, bool skyDrawn, bool depthClear, bool outdoorSceneryDrawn, int liveDynamicDrawnCount, string sceneParticles, IReadOnlySet? visibleCells, ClipFrameAssembly? clipAssembly, IReadOnlySet? drawableCells, InteriorEntityPartition.Result? partition, Vector3 cameraPosition, Vector3 playerPosition) { if (!enabled) return; _renderSignatureFrame++; bool eyeInRoot = clipRoot is not null && CellVisibility.PointInCell(cameraPosition, clipRoot); bool playerInRoot = clipRoot is not null && CellVisibility.PointInCell(playerPosition, clipRoot); var text = new StringBuilder(512); text.Append("branch=").Append(branch); text.Append(" root=0x").Append((clipRoot?.CellId ?? 0u).ToString("X8")); text.Append(" viewerRoot=0x").Append((viewerRoot?.CellId ?? 0u).ToString("X8")); text.Append(" playerRoot=0x").Append((playerRoot?.CellId ?? 0u).ToString("X8")); text.Append(" viewerCell=0x").Append(viewerCellId.ToString("X8")); text.Append(" playerCell=0x").Append(playerCellId.ToString("X8")); text.Append(" gate=").Append(playerIndoorGate ? "in" : "out"); text.Append(" camIn=").Append(cameraInsideCell ? 'Y' : 'n'); text.Append(" eyeInRoot=").Append(eyeInRoot ? 'Y' : 'n'); text.Append(" playerInRoot=").Append(playerInRoot ? 'Y' : 'n'); text.Append(" eye=").Append(FormatVector(cameraPosition)); text.Append(" player=").Append(FormatVector(playerPosition)); text.Append(" terrain=").Append(terrainClipMode); text.Append('/').Append(terrainDrawn ? "draw" : "skip"); text.Append(" skyGate=").Append(renderSkyGate ? 'Y' : 'n'); text.Append(" sky=").Append(skyDrawn ? 'Y' : 'n'); text.Append(" skyFrame=").Append(drawSkyThisFrame ? 'Y' : 'n'); text.Append(" zclear=").Append(depthClear ? 'Y' : 'n'); text.Append(" sceneParticles=").Append(sceneParticles); if (clipAssembly is not null) { text.Append(" outSlices=").Append(clipAssembly.OutsideViewSlices.Length); text.Append(" outPolys=").Append(clipAssembly.OutsideViewSlices.Length); text.Append(" outMode=").Append(clipAssembly.TerrainMode); } else { text.Append(" outSlices=0 outPolys=0 outMode=none"); } text.Append(" ids=").Append(FormatIds(visibleCells, false)); text.Append(" draw=").Append(FormatIds(drawableCells, false)); text.Append(" miss=").Append(FormatMissingDrawableCells(visibleCells, drawableCells)); text.Append(" obj=").Append(FormatPartitionCounts(partition)); text.Append(" outdoorDoor=").Append(outdoorSceneryDrawn ? 'Y' : 'n'); text.Append(" liveDynDraw=").Append(liveDynamicDrawnCount); text.Append(" outRoot=").Append(clipRoot is { IsOutdoorNode: true } ? 'Y' : 'n'); if (partition is not null) { int totalShells = 0; int shellsWithMeshes = 0; foreach (var entity in partition.OutdoorStatic) { if (!entity.IsBuildingShell) continue; totalShells++; if (entity.MeshRefs.Count > 0) shellsWithMeshes++; } text.Append(" bshell=").Append(totalShells).Append('/').Append(shellsWithMeshes); } string signature = text.ToString(); if (signature == _lastRenderSignature) { _renderSignatureStableFrames++; return; } _log.WriteLine( $"[render-sig] frame={_renderSignatureFrame} " + $"stable={_renderSignatureStableFrames} {signature}"); _lastRenderSignature = signature; _renderSignatureStableFrames = 0; } internal static string FormatGlState(RenderGlStateSnapshot state) => $"depth={(state.DepthTest ? 1 : 0)} " + $"dmask={(state.DepthWrite ? 1 : 0)} " + $"dfunc=0x{state.DepthFunction:X} " + $"blend={(state.Blend ? 1 : 0)} " + $"bsrc=0x{state.BlendSource:X} bdst=0x{state.BlendDestination:X} " + $"cull={(state.CullFace ? 1 : 0)} cmode=0x{state.CullMode:X} " + $"fface=0x{state.FrontFace:X} " + $"scis={(state.Scissor ? 1 : 0)} " + $"sbox=({state.ScissorBox.X},{state.ScissorBox.Y}," + $"{state.ScissorBox.Width},{state.ScissorBox.Height}) " + $"vp=({state.Viewport.X},{state.Viewport.Y}," + $"{state.Viewport.Width},{state.Viewport.Height}) " + $"fbo={state.DrawFramebuffer} " + $"a2c={(state.AlphaToCoverage ? 1 : 0)} " + $"stencil={(state.Stencil ? 1 : 0)} " + $"clip=0x{state.ClipBits:X2} err=0x{state.Error:X}"; private static string FormatVector(Vector3 value) { static float Quantize(float component) => MathF.Round(component * 20f) / 20f; return $"({Quantize(value.X):F2},{Quantize(value.Y):F2},{Quantize(value.Z):F2})"; } private static string FormatIds(IEnumerable? ids, bool preserveOrder) { if (ids is null) return "[]"; var values = new List(ids); if (!preserveOrder) values.Sort(); var text = new StringBuilder(96).Append('['); const int MaximumIds = 12; for (int index = 0; index < values.Count && index < MaximumIds; index++) { if (index > 0) text.Append(','); text.Append("0x").Append(values[index].ToString("X8")); } if (values.Count > MaximumIds) text.Append(",..."); return text.Append(']').ToString(); } private static string FormatMissingDrawableCells( IReadOnlySet? visibleCells, IReadOnlySet? drawableCells) { if (visibleCells is null || drawableCells is null) return "[]"; var text = new StringBuilder(96).Append('['); int written = 0; const int MaximumCells = 8; foreach (uint id in visibleCells.OrderBy(static id => id)) { if (drawableCells.Contains(id)) continue; if (written > 0) text.Append(','); text.Append("0x").Append(id.ToString("X8")); if (++written >= MaximumCells) { text.Append(",..."); break; } } return text.Append(']').ToString(); } private static string FormatPartitionCounts(InteriorEntityPartition.Result? partition) { if (partition is null) return "cell=[] out=0 live=0"; var keys = new List(partition.ByCell.Keys); keys.Sort(); var text = new StringBuilder(128).Append("cell=["); const int MaximumCells = 10; for (int index = 0; index < keys.Count && index < MaximumCells; index++) { uint id = keys[index]; if (index > 0) text.Append(','); text.Append("0x").Append(id.ToString("X8")) .Append(':').Append(partition.ByCell[id].Count); } if (keys.Count > MaximumCells) text.Append(",..."); return text.Append("] out=").Append(partition.OutdoorStatic.Count) .Append(" live=").Append(partition.Dynamics.Count) .ToString(); } }