593 lines
22 KiB
C#
593 lines
22 KiB
C#
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();
|
|
}
|
|
}
|