refactor(render): extract frame presentation diagnostics
This commit is contained in:
parent
7e4cfb37c3
commit
733126a272
20 changed files with 3767 additions and 1021 deletions
593
src/AcDream.App/Rendering/WorldRenderDiagnostics.cs
Normal file
593
src/AcDream.App/Rendering/WorldRenderDiagnostics.cs
Normal file
|
|
@ -0,0 +1,593 @@
|
|||
using System.Numerics;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using AcDream.Core.Vfx;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
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 LoadedSlots,
|
||||
int CapacitySlots);
|
||||
|
||||
internal interface IRenderGlStateReader
|
||||
{
|
||||
RenderGlStateSnapshot CaptureState();
|
||||
|
||||
RenderGlScissorSnapshot CaptureScissor();
|
||||
}
|
||||
|
||||
/// <summary>Render-thread GL state reader used only by explicitly enabled probes.</summary>
|
||||
internal sealed class SilkRenderGlStateReader : IRenderGlStateReader
|
||||
{
|
||||
private readonly GL _gl;
|
||||
|
||||
public SilkRenderGlStateReader(GL gl) =>
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
|
||||
public RenderGlStateSnapshot CaptureState()
|
||||
{
|
||||
Span<int> scissor = stackalloc int[4];
|
||||
Span<int> viewport = stackalloc int[4];
|
||||
_gl.GetInteger(GetPName.ScissorBox, scissor);
|
||||
_gl.GetInteger(GetPName.Viewport, viewport);
|
||||
|
||||
int clipBits = 0;
|
||||
for (int index = 0; index < ClipFrame.MaxPlanes; index++)
|
||||
{
|
||||
if (_gl.IsEnabled(EnableCap.ClipDistance0 + index))
|
||||
clipBits |= 1 << index;
|
||||
}
|
||||
|
||||
// Preserve the old tripwire boundary: consume the error that existed
|
||||
// after the scissor/viewport/clip reads, before any of the state reads
|
||||
// below can produce a probe-owned error.
|
||||
int error = (int)_gl.GetError();
|
||||
|
||||
return new RenderGlStateSnapshot(
|
||||
_gl.IsEnabled(EnableCap.DepthTest),
|
||||
_gl.GetBoolean(GetPName.DepthWritemask),
|
||||
_gl.GetInteger(GetPName.DepthFunc),
|
||||
_gl.IsEnabled(EnableCap.Blend),
|
||||
_gl.GetInteger(GetPName.BlendSrcRgb),
|
||||
_gl.GetInteger(GetPName.BlendDstRgb),
|
||||
_gl.IsEnabled(EnableCap.CullFace),
|
||||
_gl.GetInteger(GetPName.CullFaceMode),
|
||||
_gl.GetInteger(GetPName.FrontFace),
|
||||
_gl.IsEnabled(EnableCap.ScissorTest),
|
||||
new IntRenderRectangle(scissor[0], scissor[1], scissor[2], scissor[3]),
|
||||
new IntRenderRectangle(viewport[0], viewport[1], viewport[2], viewport[3]),
|
||||
_gl.GetInteger(GetPName.DrawFramebufferBinding),
|
||||
_gl.IsEnabled(EnableCap.SampleAlphaToCoverage),
|
||||
_gl.IsEnabled(EnableCap.StencilTest),
|
||||
clipBits,
|
||||
error);
|
||||
}
|
||||
|
||||
public RenderGlScissorSnapshot CaptureScissor()
|
||||
{
|
||||
Span<int> box = stackalloc int[4];
|
||||
_gl.GetInteger(GetPName.ScissorBox, box);
|
||||
return new RenderGlScissorSnapshot(
|
||||
_gl.IsEnabled(EnableCap.ScissorTest),
|
||||
new IntRenderRectangle(box[0], box[1], box[2], box[3]));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal sealed class WorldRenderDiagnostics
|
||||
{
|
||||
private readonly IRenderGlStateReader _gl;
|
||||
private readonly IRenderFrameDiagnosticLog _log;
|
||||
private readonly HashSet<uint> _lastViewerFloodCells = [];
|
||||
private readonly HashSet<uint> _outStageUnmatched = [];
|
||||
private readonly HashSet<uint> _outStageMatched = [];
|
||||
private readonly Stopwatch _terrainStopwatch = new();
|
||||
private readonly RollingTimingSampleWindow _terrainSamples = new(256);
|
||||
private string? _lastRenderSignature;
|
||||
private int _renderSignatureFrame;
|
||||
private int _renderSignatureStableFrames;
|
||||
private string? _lastViewerSignature;
|
||||
private string? _lastGlStateSignature;
|
||||
private long _glStateFrame;
|
||||
private long _glStateStableFrames;
|
||||
private string? _lastScissorSignature;
|
||||
private long _scissorSequence;
|
||||
private string? _lastOutStageSignature;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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.VisibleSlots}/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;
|
||||
}
|
||||
|
||||
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 EmitPViewInput(
|
||||
bool enabled,
|
||||
PortalVisibilityFrame portalFrame,
|
||||
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} flood={portalFrame.OrderedVisibleCells.Count} 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 EmitOutStageParticles(
|
||||
bool enabled,
|
||||
ParticleSystem? particles,
|
||||
IReadOnlySet<uint> ownerIds)
|
||||
{
|
||||
if (!enabled || particles is null)
|
||||
return;
|
||||
|
||||
int matched = 0;
|
||||
int attached = 0;
|
||||
int unattached = 0;
|
||||
_outStageUnmatched.Clear();
|
||||
_outStageMatched.Clear();
|
||||
foreach (var (emitter, _) in particles.EnumerateLive())
|
||||
{
|
||||
if (emitter.AttachedObjectId == 0)
|
||||
{
|
||||
unattached++;
|
||||
continue;
|
||||
}
|
||||
|
||||
attached++;
|
||||
if (ownerIds.Contains(emitter.AttachedObjectId))
|
||||
{
|
||||
matched++;
|
||||
if (_outStageMatched.Count < 48)
|
||||
_outStageMatched.Add(emitter.AttachedObjectId);
|
||||
}
|
||||
else if (_outStageUnmatched.Count < 12)
|
||||
{
|
||||
_outStageUnmatched.Add(emitter.AttachedObjectId);
|
||||
}
|
||||
}
|
||||
|
||||
var unmatched = new StringBuilder(96);
|
||||
foreach (uint id in _outStageUnmatched)
|
||||
unmatched.Append(FormattableString.Invariant($" 0x{id:X8}"));
|
||||
var matchedIds = new StringBuilder(192);
|
||||
foreach (uint id in _outStageMatched)
|
||||
matchedIds.Append(FormattableString.Invariant($" 0x{id:X8}"));
|
||||
string signature = FormattableString.Invariant(
|
||||
$"ids={ownerIds.Count} attachedEmitters={attached} matched={matched} unattached={unattached} matchedIds=[{matchedIds}] unmatchedIds=[{unmatched}]");
|
||||
if (signature == _lastOutStageSignature)
|
||||
return;
|
||||
|
||||
_lastOutStageSignature = signature;
|
||||
_log.WriteLine("[outstage-pt] " + signature);
|
||||
}
|
||||
|
||||
public void EmitRetailPViewDiagnostics(
|
||||
bool viewerEnabled,
|
||||
bool visibilityEnabled,
|
||||
bool flapEnabled,
|
||||
RetailPViewFrameResult result,
|
||||
LoadedCell clipRoot,
|
||||
uint viewerCellId,
|
||||
uint playerCellId,
|
||||
Vector3 cameraPosition,
|
||||
Vector3 playerPosition,
|
||||
Matrix4x4 cameraView,
|
||||
CameraCellResolution cameraCellResolution)
|
||||
{
|
||||
if (viewerEnabled)
|
||||
{
|
||||
string signature = FormattableString.Invariant(
|
||||
$"root=0x{clipRoot.CellId:X8}{(clipRoot.IsOutdoorNode ? "(OUT)" : string.Empty)} flood={result.PortalFrame.OrderedVisibleCells.Count} outPolys={result.PortalFrame.OutsideView.Polygons.Count} pCell=0x{playerCellId:X8}");
|
||||
if (signature != _lastViewerSignature)
|
||||
{
|
||||
_lastViewerSignature = signature;
|
||||
_log.WriteLine(FormattableString.Invariant(
|
||||
$"[viewer] {signature} eye=({cameraPosition.X:F3},{cameraPosition.Y:F3},{cameraPosition.Z:F3}) fwd=({-cameraView.M13:F4},{-cameraView.M23:F4},{-cameraView.M33:F4}) viewerCell=0x{viewerCellId:X8}"));
|
||||
EmitViewerDiff(result.PortalFrame.OrderedVisibleCells);
|
||||
}
|
||||
}
|
||||
|
||||
if (visibilityEnabled)
|
||||
{
|
||||
AcDream.Core.Rendering.RenderingDiagnostics.EmitVis(
|
||||
clipRoot.CellId,
|
||||
result.PortalFrame.OrderedVisibleCells,
|
||||
result.PortalFrame.OutsideView.Polygons.Count,
|
||||
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,
|
||||
bool outdoorPortalDrawn,
|
||||
int outdoorRootObjectCount,
|
||||
int liveDynamicDrawnCount,
|
||||
string sceneParticles,
|
||||
PortalVisibilityFrame? portalFrame,
|
||||
ClipFrameAssembly? clipAssembly,
|
||||
IReadOnlySet<uint>? drawableCells,
|
||||
InteriorEntityPartition.Result? partition,
|
||||
PortalVisibilityFrame? exteriorPortalFrame,
|
||||
ClipFrameAssembly? exteriorClipAssembly,
|
||||
IReadOnlySet<uint>? exteriorDrawableCells,
|
||||
InteriorEntityPartition.Result? exteriorPartition,
|
||||
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(portalFrame?.OutsideView.Polygons.Count ?? 0);
|
||||
text.Append(" outMode=").Append(clipAssembly.TerrainMode);
|
||||
}
|
||||
else
|
||||
{
|
||||
text.Append(" outSlices=0 outPolys=0 outMode=none");
|
||||
}
|
||||
|
||||
text.Append(" ids=").Append(FormatIds(portalFrame?.OrderedVisibleCells, true));
|
||||
text.Append(" draw=").Append(FormatIds(drawableCells, false));
|
||||
text.Append(" miss=").Append(FormatMissingDrawableCells(portalFrame, drawableCells));
|
||||
text.Append(" obj=").Append(FormatPartitionCounts(partition));
|
||||
text.Append(" outdoorDoor=").Append(outdoorSceneryDrawn ? 'Y' : 'n');
|
||||
text.Append(" outdoorRootObjs=").Append(outdoorRootObjectCount);
|
||||
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);
|
||||
}
|
||||
|
||||
if (outdoorPortalDrawn || exteriorPortalFrame is not null
|
||||
|| exteriorClipAssembly is not null)
|
||||
{
|
||||
text.Append(" extPortal=").Append(outdoorPortalDrawn ? 'Y' : 'n');
|
||||
text.Append(" extSlices=").Append(exteriorClipAssembly?.OutsideViewSlices.Length ?? 0);
|
||||
text.Append(" extIds=").Append(FormatIds(
|
||||
exteriorPortalFrame?.OrderedVisibleCells,
|
||||
true));
|
||||
text.Append(" extDraw=").Append(FormatIds(exteriorDrawableCells, false));
|
||||
text.Append(" extMiss=").Append(FormatMissingDrawableCells(
|
||||
exteriorPortalFrame,
|
||||
exteriorDrawableCells));
|
||||
text.Append(" extObj=").Append(FormatPartitionCounts(exteriorPartition));
|
||||
}
|
||||
|
||||
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 void EmitViewerDiff(IReadOnlyList<uint> current)
|
||||
{
|
||||
var text = new StringBuilder(96);
|
||||
text.Append("[viewer-diff] added=[");
|
||||
bool first = true;
|
||||
foreach (uint cell in current)
|
||||
{
|
||||
if (_lastViewerFloodCells.Contains(cell))
|
||||
continue;
|
||||
if (!first)
|
||||
text.Append(',');
|
||||
text.Append("0x").Append(cell.ToString("X8"));
|
||||
first = false;
|
||||
}
|
||||
|
||||
text.Append("] removed=[");
|
||||
first = true;
|
||||
foreach (uint cell in _lastViewerFloodCells)
|
||||
{
|
||||
bool present = false;
|
||||
for (int index = 0; index < current.Count; index++)
|
||||
{
|
||||
if (current[index] == cell)
|
||||
{
|
||||
present = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (present)
|
||||
continue;
|
||||
if (!first)
|
||||
text.Append(',');
|
||||
text.Append("0x").Append(cell.ToString("X8"));
|
||||
first = false;
|
||||
}
|
||||
text.Append(']');
|
||||
_log.WriteLine(text.ToString());
|
||||
|
||||
_lastViewerFloodCells.Clear();
|
||||
foreach (uint cell in current)
|
||||
_lastViewerFloodCells.Add(cell);
|
||||
}
|
||||
|
||||
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<uint>? ids, bool preserveOrder)
|
||||
{
|
||||
if (ids is null)
|
||||
return "[]";
|
||||
|
||||
var values = new List<uint>(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(
|
||||
PortalVisibilityFrame? portalFrame,
|
||||
IReadOnlySet<uint>? drawableCells)
|
||||
{
|
||||
if (portalFrame is null || drawableCells is null)
|
||||
return "[]";
|
||||
|
||||
var text = new StringBuilder(96).Append('[');
|
||||
int written = 0;
|
||||
const int MaximumCells = 8;
|
||||
foreach (uint id in portalFrame.OrderedVisibleCells)
|
||||
{
|
||||
if (drawableCells.Contains(id))
|
||||
continue;
|
||||
if (written > 0)
|
||||
text.Append(',');
|
||||
text.Append("0x").Append(id.ToString("X8"));
|
||||
if (portalFrame.CellViews.TryGetValue(id, out var view))
|
||||
{
|
||||
text.Append(":p").Append(view.Polygons.Count);
|
||||
if (view.IsEmpty)
|
||||
text.Append(":empty");
|
||||
}
|
||||
else
|
||||
{
|
||||
text.Append(":noView");
|
||||
}
|
||||
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<uint>(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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue