Commit 2 deleted the GL rendering backend's implementations; this step removes the package references and shader vocabulary they leave behind, so nothing in the App project still spells Silk.NET.OpenGL. Silk.NET.OpenGL and Silk.NET.OpenGL.Extensions.ARB are dropped from AcDream.App.csproj. Chorizite.Core stays — the audit is NOT clean: its Render.Enums (TextureFormat, BufferUsage) and Lib.BoundingBox types are used directly and extensively across the Wb texture/mesh pipeline, independent of the deleted GL IUniformBuffer implementers the package comment used to cite. The stale comment is corrected in place. IMeshPipelineDevice.Gl is removed along with the GL? gl parameter threaded through WbMeshAdapter's four constructors, WorldRenderComposition's CreateMeshAdapter, and VulkanMeshPipelineDevice's Gl => null implementation — nothing read any of them once the legacy per-mesh upload bodies were gone (confirmed by grep: the sole non-doc-comment hit was a test assertion). While in WbMeshAdapter.Dispose(), found and fixed a real bug along the way: its teardown still pattern-matched the deleted GL GpuFrameFlightController to decide whether to wait for submitted work, which VulkanFrameFlightController replaced at slice V6a without this site being updated — so the wait had been silently dead on every Vulkan run since then. Retargeted to VulkanFrameFlightController, which carries the same WaitForSubmittedWork(). The GL pixel-format vocabulary (Silk.NET.OpenGL.PixelFormat/PixelType) that WorldTextureArray/TextureFormatExtensions/TextureAtlasManager used for upload validation is replaced by AcDream.Content's existing Silk.NET-free UploadPixelFormat/UploadPixelType enums (added at MP1a to keep the bake tool GL-free); two new members (Rgb, Red, Float) extend that enum with their GL ABI constants to cover the full vocabulary WorldTextureArray needs, since MP1a's original set only covered what the extractor itself emits. ObjectMeshManager's App-boundary cast `(Silk.NET.OpenGL.PixelFormat?)batch.UploadPixelFormat` becomes a direct pass-through now that both sides share the type. GpuBindingModel.StorageTextureTable (the GL-only binding=9 emulation of the Vulkan texture table) is deleted and StorageBindingCount drops from 10 to 9; the descriptor-set-layout code that builds from that count (VulkanPipelineLayouts, VulkanFrameBindings) is untouched and just allocates one fewer always-dummy-seeded, always-unused binding. Several fully dead GL-only classes came along for the ride, confirmed by zero construction sites: SilkFramebufferViewportTarget (NullFramebufferViewportTarget is the sole production IFramebufferViewportTarget), SilkRenderGlStateReader (NullRenderGlStateReader.Instance is the sole IRenderGlStateReader), RuntimeRenderFrameClearPhase (VulkanRenderFrameClearPhase is the sole IRenderFrameClearPhase, expressing the same atmosphere-clear logic as a pass load-op instead), and GpuFrameTimer plus FrameProfiler's GL-owning FrameBoundary(GL) overload and BeginGpuFrame/EndGpuFrame bracket (RecordGpuSample is the only GPU-timing path any backend uses now — the ACDREAM_WB_DIAG nested-query exclusion these existed for no longer applies, since WbDrawDispatcher's own diagnostic GPU sampling already moved to the device's Vulkan timer pool). GpuFrameFlightController itself stays (never constructed with a real fence API in production, but its retirement-ledger/serial-ring logic is backend-neutral and still covered by its own unit tests) — only its GL-specific parts (the public GL constructor overload, SilkGpuFenceApi) are deleted, since removing the whole class would mean restructuring the frozen Slice-8 composition shape's GpuFrameFlightController? threading, which is out of this commit's scope. TextureParameters.cs and BufferUsageExtensions.cs (zero callers each) are deleted outright. common.glsl is deleted: nothing in the actual Vulkan .spv build reads it. tools/ShaderCompiler/Program.cs compiles each .vert/.frag pair directly and tools/ShaderCompiler/VulkanGlslPreamble.cs injects its own complete self-contained preamble per file; common.glsl's textual concatenation was exclusively Shader.cs's GL-only mechanism, deleted at Commit 2. The five shader files that named it in comments (mesh_modern.vert, particle.vert, particle.frag, sky.frag, terrain_modern.frag) are corrected to point at VulkanGlslPreamble.cs instead. mesh.vert/mesh.frag — the pre-N.5 legacy shader pair the mandatory modern path already made unreachable, with zero C# consumers and no compiled .spv — are deleted too. Regenerated via tools/compile-shaders.ps1: 9/9 remaining shader pairs compile (previously 9/10, with mesh the sole failure — the VulkanShaderManifestTests doc comment's "nine of ten are not Vulkan-expressible" was already stale before this commit). Test fallout: dead-subject test methods/files are deleted rather than patched (TextRendererFailureSafetyTests.cs, ClipFrameUploadTests.cs, GpuResourceRetirementTransactionTests.cs's GL queue tests, one WorldRenderDiagnosticsTests source-order test, one RenderFrameResourceControllerTests clear-phase-order test); tests whose subject moved or was renamed are updated in place rather than deleted (GpuContractTests, VulkanCapabilityGateTests, MeshPipelineDeviceSeamTests' pinned seven-member surface now reads six, ParticleBindlessInstanceTests' cross-dialect check now covers the one surviving dialect, WbMeshAdapterTests' misleadingly-named null-gl test — gpuDevice was always the parameter that actually threw). Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors, with the Silk.NET.OpenGL/.Extensions.ARB package references physically removed from the csproj (not just unreferenced in code). Tests: full-solution `dotnet test` green across every project. Zero remaining `using Silk.NET.OpenGL` anywhere in src/ or tests/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
762 lines
29 KiB
C#
762 lines
29 KiB
C#
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 LoadedSlots,
|
|
int CapacitySlots);
|
|
|
|
internal interface IRenderGlStateReader
|
|
{
|
|
RenderGlStateSnapshot CaptureState();
|
|
|
|
RenderGlScissorSnapshot CaptureScissor();
|
|
}
|
|
|
|
/// <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? _lastPostWorldGlStateSignature;
|
|
private long _postWorldGlStateFrame;
|
|
private long _postWorldGlStateStableFrames;
|
|
private string? _lastScissorSignature;
|
|
private long _scissorSequence;
|
|
private string? _lastOutStageSignature;
|
|
private string? _lastOutStageRoutingSignature;
|
|
private readonly Dictionary<uint, bool> _outStageOwnerVerdicts = [];
|
|
private readonly Dictionary<uint, int> _phantomObjectSignatures = [];
|
|
private string? _lastClipRouteSignature;
|
|
private long _clipRouteSequence;
|
|
private readonly List<uint> _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));
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Second sample of the same snapshot, taken at the END of the normal-world
|
|
/// phase instead of at the frame clear.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <see cref="EmitGlStateTripwireIfChanged"/> samples immediately after the
|
|
/// clear phase has run <c>RestoreFrameDefaults</c>, 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.
|
|
/// </remarks>
|
|
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<byte> 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<byte> 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 EmitOutStageOwner(
|
|
bool enabled,
|
|
IReadOnlySet<uint> watchedEntityIds,
|
|
WorldEntity entity,
|
|
Vector3 sphereCenter,
|
|
float sphereRadius,
|
|
int sliceIndex,
|
|
bool passed)
|
|
{
|
|
if (!enabled
|
|
|| !watchedEntityIds.Contains(entity.Id)
|
|
|| (_outStageOwnerVerdicts.TryGetValue(entity.Id, out bool previous)
|
|
&& previous == passed))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_outStageOwnerVerdicts[entity.Id] = passed;
|
|
_log.WriteLine(FormattableString.Invariant(
|
|
$"[outstage-own] id=0x{entity.Id:X8} src=0x{entity.SourceGfxObjOrSetupId:X8} pos=({entity.Position.X:F1},{entity.Position.Y:F1},{entity.Position.Z:F1}) c=({sphereCenter.X:F1},{sphereCenter.Y:F1},{sphereCenter.Z:F1}) r={sphereRadius:F1} slice={sliceIndex} {(passed ? "PASS" : "CULL")}"));
|
|
}
|
|
|
|
public void EmitOutStageRouting(
|
|
bool enabled,
|
|
int sliceIndex,
|
|
IReadOnlyList<WorldEntity> entities,
|
|
ViewconeCuller viewcone)
|
|
{
|
|
if (!enabled)
|
|
return;
|
|
|
|
var text = new StringBuilder(192);
|
|
text.Append("slice=").Append(sliceIndex)
|
|
.Append(" outStage=").Append(entities.Count).Append(" [");
|
|
for (int i = 0; i < entities.Count; i++)
|
|
{
|
|
WorldEntity entity = entities[i];
|
|
EntitySphere(entity, out Vector3 center, out float radius);
|
|
bool passed = viewcone.SphereVisibleInOutsideSlice(
|
|
sliceIndex,
|
|
center,
|
|
radius);
|
|
if (i > 0)
|
|
text.Append(' ');
|
|
text.Append(FormattableString.Invariant(
|
|
$"0x{(entity.ServerGuid != 0 ? entity.ServerGuid : entity.Id):X8}(s{entity.SourceGfxObjOrSetupId:X8}):{(passed ? "PASS" : "CULL")}:r={radius:F1}"));
|
|
}
|
|
text.Append(']');
|
|
string signature = text.ToString();
|
|
if (signature == _lastOutStageRoutingSignature)
|
|
return;
|
|
_lastOutStageRoutingSignature = signature;
|
|
_log.WriteLine("[outstage] " + signature);
|
|
}
|
|
|
|
public void EmitPhantomObjects(bool enabled, uint cellId, int survivorCount)
|
|
{
|
|
if (!enabled
|
|
|| (_phantomObjectSignatures.TryGetValue(cellId, out int previous)
|
|
&& previous == survivorCount))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_phantomObjectSignatures[cellId] = survivorCount;
|
|
_log.WriteLine(
|
|
$"[phantom-objs] cell=0x{cellId:X8} entities={survivorCount} (drawn unclipped, no viewcone)");
|
|
}
|
|
|
|
public void EmitSeamMask(
|
|
bool enabled,
|
|
IReadOnlySet<uint> targetCells,
|
|
uint cellId,
|
|
int portalIndex,
|
|
bool forceFarZ,
|
|
ReadOnlySpan<Vector3> 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,
|
|
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();
|
|
}
|
|
|
|
private static void EntitySphere(
|
|
WorldEntity entity,
|
|
out Vector3 center,
|
|
out float radius)
|
|
{
|
|
if (entity.AabbDirty)
|
|
entity.RefreshAabb();
|
|
center = (entity.AabbMin + entity.AabbMax) * 0.5f;
|
|
radius = (entity.AabbMax - entity.AabbMin).Length() * 0.5f;
|
|
}
|
|
}
|