refactor(render): extract frame presentation diagnostics
This commit is contained in:
parent
7e4cfb37c3
commit
733126a272
20 changed files with 3767 additions and 1021 deletions
|
|
@ -76,10 +76,10 @@ internal sealed class FrameScreenshotController
|
|||
&& status.State == CaptureState.Complete;
|
||||
|
||||
/// <summary>Captures at most one queued image on the current GL thread.</summary>
|
||||
public void CapturePending()
|
||||
public bool CapturePending()
|
||||
{
|
||||
if (_pending.Count == 0)
|
||||
return;
|
||||
return false;
|
||||
|
||||
string name = _pending.Dequeue();
|
||||
try
|
||||
|
|
@ -104,12 +104,14 @@ internal sealed class FrameScreenshotController
|
|||
|
||||
_status[name] = new CaptureStatus(CaptureState.Complete);
|
||||
_log($"[world-gate] screenshot-complete name={name} path={path} size={width}x{height}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
string message = $"screenshot '{name}' failed: {exception.Message}";
|
||||
_status[name] = new CaptureStatus(CaptureState.Failed, message);
|
||||
_log($"[world-gate] screenshot-failed name={name} error={exception.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
79
src/AcDream.App/Rendering/DebugVmRenderFactsPublisher.cs
Normal file
79
src/AcDream.App/Rendering/DebugVmRenderFactsPublisher.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>Same-frame render facts consumed by the developer DebugVM.</summary>
|
||||
internal readonly record struct DebugVmRenderFacts(
|
||||
int VisibleLandblocks,
|
||||
int TotalLandblocks,
|
||||
float NearestObjectDistance,
|
||||
string NearestObjectLabel,
|
||||
bool Colliding)
|
||||
{
|
||||
public static DebugVmRenderFacts Initial => new(
|
||||
VisibleLandblocks: 0,
|
||||
TotalLandblocks: 0,
|
||||
NearestObjectDistance: float.PositiveInfinity,
|
||||
NearestObjectLabel: "-",
|
||||
Colliding: false);
|
||||
}
|
||||
|
||||
internal interface IDebugVmRenderFactsSource
|
||||
{
|
||||
DebugVmRenderFacts DebugVmFacts { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns render-derived DebugVM facts without retaining the physics registry,
|
||||
/// camera, player controller, world state, or any other canonical runtime owner.
|
||||
/// The pass-local diagnostics owner publishes through this focused query surface.
|
||||
/// </summary>
|
||||
internal sealed class DebugVmRenderFactsPublisher : IDebugVmRenderFactsSource
|
||||
{
|
||||
internal const float PlayerCollisionRadius = 0.48f;
|
||||
internal const float ContactThreshold = 0.05f;
|
||||
|
||||
public DebugVmRenderFacts DebugVmFacts { get; private set; } =
|
||||
DebugVmRenderFacts.Initial;
|
||||
|
||||
/// <summary>
|
||||
/// Publishes the facts immediately before private presentation. A disabled
|
||||
/// consumer leaves the prior snapshot untouched and does not enumerate the
|
||||
/// borrowed shadow-object sequence.
|
||||
/// </summary>
|
||||
public void PublishDebugVmFacts(
|
||||
bool consumerActive,
|
||||
int visibleLandblocks,
|
||||
int totalLandblocks,
|
||||
Vector3 nearestOrigin,
|
||||
IEnumerable<ShadowEntry>? shadowObjects)
|
||||
{
|
||||
if (!consumerActive)
|
||||
return;
|
||||
ArgumentNullException.ThrowIfNull(shadowObjects);
|
||||
|
||||
float nearestDistance = float.PositiveInfinity;
|
||||
string nearestLabel = "-";
|
||||
foreach (ShadowEntry shadow in shadowObjects)
|
||||
{
|
||||
float dx = shadow.Position.X - nearestOrigin.X;
|
||||
float dy = shadow.Position.Y - nearestOrigin.Y;
|
||||
float distance = MathF.Sqrt(dx * dx + dy * dy)
|
||||
- shadow.Radius
|
||||
- PlayerCollisionRadius;
|
||||
if (distance < nearestDistance)
|
||||
{
|
||||
nearestDistance = distance;
|
||||
nearestLabel = $"0x{shadow.EntityId:X8} {shadow.CollisionType}";
|
||||
}
|
||||
}
|
||||
|
||||
DebugVmFacts = new DebugVmRenderFacts(
|
||||
visibleLandblocks,
|
||||
totalLandblocks,
|
||||
nearestDistance < 0f ? 0f : nearestDistance,
|
||||
nearestLabel,
|
||||
nearestDistance < ContactThreshold);
|
||||
}
|
||||
}
|
||||
387
src/AcDream.App/Rendering/DevToolsFramePresenter.cs
Normal file
387
src/AcDream.App/Rendering/DevToolsFramePresenter.cs
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.UI.Abstractions;
|
||||
using AcDream.UI.Abstractions.Panels.Chat;
|
||||
using AcDream.UI.Abstractions.Panels.Debug;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
using AcDream.UI.Abstractions.Panels.Vitals;
|
||||
using AcDream.UI.ImGui;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal enum DevToolsPanelLayoutCondition
|
||||
{
|
||||
FirstUseEver,
|
||||
Always,
|
||||
}
|
||||
|
||||
internal enum DevToolsPanelKind
|
||||
{
|
||||
Vitals,
|
||||
Chat,
|
||||
Debug,
|
||||
Settings,
|
||||
}
|
||||
|
||||
internal interface IDevToolsFrameBackend
|
||||
{
|
||||
void BeginFrame(float deltaSeconds);
|
||||
|
||||
bool BeginMainMenuBar();
|
||||
|
||||
void EndMainMenuBar();
|
||||
|
||||
bool BeginMenu(string label);
|
||||
|
||||
void EndMenu();
|
||||
|
||||
bool MenuItem(string label, string? shortcut = null, bool selected = false);
|
||||
|
||||
void Separator();
|
||||
|
||||
void RenderPanels(PanelContext context);
|
||||
|
||||
void RenderDrawData();
|
||||
|
||||
void SetWindowLayout(
|
||||
string title,
|
||||
Vector2 position,
|
||||
Vector2 size,
|
||||
DevToolsPanelLayoutCondition condition);
|
||||
}
|
||||
|
||||
internal interface IDevToolsCameraMenuOperations
|
||||
{
|
||||
bool IsFlyMode { get; }
|
||||
|
||||
bool CollideCamera { get; set; }
|
||||
|
||||
void ToggleFlyOrChase();
|
||||
}
|
||||
|
||||
internal interface IDevToolsCommandBusSource
|
||||
{
|
||||
ICommandBus Current { get; }
|
||||
}
|
||||
|
||||
internal interface IDevToolsPanelSet
|
||||
{
|
||||
bool Contains(DevToolsPanelKind kind);
|
||||
|
||||
string? GetTitle(DevToolsPanelKind kind);
|
||||
|
||||
bool IsVisible(DevToolsPanelKind kind);
|
||||
|
||||
void Toggle(DevToolsPanelKind kind);
|
||||
|
||||
void FocusChatInput();
|
||||
|
||||
void AddDebugToast(string message);
|
||||
}
|
||||
|
||||
/// <summary>Concrete ImGui backend for the developer presentation owner.</summary>
|
||||
internal sealed class ImGuiDevToolsFrameBackend : IDevToolsFrameBackend
|
||||
{
|
||||
private readonly ImGuiBootstrapper _bootstrap;
|
||||
private readonly ImGuiPanelHost _panels;
|
||||
|
||||
public ImGuiDevToolsFrameBackend(
|
||||
ImGuiBootstrapper bootstrap,
|
||||
ImGuiPanelHost panels)
|
||||
{
|
||||
_bootstrap = bootstrap ?? throw new ArgumentNullException(nameof(bootstrap));
|
||||
_panels = panels ?? throw new ArgumentNullException(nameof(panels));
|
||||
}
|
||||
|
||||
public void BeginFrame(float deltaSeconds) => _bootstrap.BeginFrame(deltaSeconds);
|
||||
|
||||
public bool BeginMainMenuBar() => ImGuiNET.ImGui.BeginMainMenuBar();
|
||||
|
||||
public void EndMainMenuBar() => ImGuiNET.ImGui.EndMainMenuBar();
|
||||
|
||||
public bool BeginMenu(string label) => ImGuiNET.ImGui.BeginMenu(label);
|
||||
|
||||
public void EndMenu() => ImGuiNET.ImGui.EndMenu();
|
||||
|
||||
public bool MenuItem(string label, string? shortcut = null, bool selected = false) =>
|
||||
ImGuiNET.ImGui.MenuItem(label, shortcut ?? string.Empty, selected);
|
||||
|
||||
public void Separator() => ImGuiNET.ImGui.Separator();
|
||||
|
||||
public void RenderPanels(PanelContext context) => _panels.RenderAll(context);
|
||||
|
||||
public void RenderDrawData() => _bootstrap.Render();
|
||||
|
||||
public void SetWindowLayout(
|
||||
string title,
|
||||
Vector2 position,
|
||||
Vector2 size,
|
||||
DevToolsPanelLayoutCondition condition)
|
||||
{
|
||||
ImGuiCond imguiCondition = condition switch
|
||||
{
|
||||
DevToolsPanelLayoutCondition.FirstUseEver => ImGuiCond.FirstUseEver,
|
||||
DevToolsPanelLayoutCondition.Always => ImGuiCond.Always,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(condition)),
|
||||
};
|
||||
ImGuiNET.ImGui.SetWindowPos(title, position, imguiCondition);
|
||||
ImGuiNET.ImGui.SetWindowSize(title, size, imguiCondition);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Mutable late-composition seam for player-mode menu operations.</summary>
|
||||
internal sealed class DevToolsCameraMenuOperations : IDevToolsCameraMenuOperations
|
||||
{
|
||||
private readonly CameraController _camera;
|
||||
private PlayerModeController? _playerMode;
|
||||
|
||||
public DevToolsCameraMenuOperations(CameraController camera)
|
||||
{
|
||||
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
|
||||
}
|
||||
|
||||
public bool IsFlyMode => _camera.IsFlyMode;
|
||||
|
||||
public bool CollideCamera
|
||||
{
|
||||
get => AcDream.Core.Rendering.CameraDiagnostics.CollideCamera;
|
||||
set => AcDream.Core.Rendering.CameraDiagnostics.CollideCamera = value;
|
||||
}
|
||||
|
||||
public void Bind(PlayerModeController playerMode)
|
||||
{
|
||||
_playerMode = playerMode ?? throw new ArgumentNullException(nameof(playerMode));
|
||||
}
|
||||
|
||||
public void ToggleFlyOrChase() => _playerMode?.ToggleFlyOrChase();
|
||||
}
|
||||
|
||||
/// <summary>Resolves the reconnect-safe command bus at draw time.</summary>
|
||||
internal sealed class DevToolsCommandBusSource : IDevToolsCommandBusSource
|
||||
{
|
||||
private LiveSessionController? _session;
|
||||
|
||||
public ICommandBus Current =>
|
||||
_session?.Commands ?? NullCommandBus.Instance;
|
||||
|
||||
public void Bind(LiveSessionController session)
|
||||
{
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Typed panel operations used by menu and input presentation.</summary>
|
||||
internal sealed class DevToolsPanelSet : IDevToolsPanelSet
|
||||
{
|
||||
private readonly VitalsPanel _vitals;
|
||||
private readonly ChatPanel _chat;
|
||||
private readonly DebugPanel _debug;
|
||||
private readonly DebugVM _debugViewModel;
|
||||
private readonly SettingsPanel? _settings;
|
||||
|
||||
public DevToolsPanelSet(
|
||||
VitalsPanel vitals,
|
||||
ChatPanel chat,
|
||||
DebugPanel debug,
|
||||
DebugVM debugViewModel,
|
||||
SettingsPanel? settings)
|
||||
{
|
||||
_vitals = vitals ?? throw new ArgumentNullException(nameof(vitals));
|
||||
_chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
||||
_debug = debug ?? throw new ArgumentNullException(nameof(debug));
|
||||
_debugViewModel = debugViewModel
|
||||
?? throw new ArgumentNullException(nameof(debugViewModel));
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public bool Contains(DevToolsPanelKind kind) =>
|
||||
kind != DevToolsPanelKind.Settings || _settings is not null;
|
||||
|
||||
public string? GetTitle(DevToolsPanelKind kind) => GetPanel(kind)?.Title;
|
||||
|
||||
public bool IsVisible(DevToolsPanelKind kind) => GetPanel(kind)?.IsVisible == true;
|
||||
|
||||
public void Toggle(DevToolsPanelKind kind)
|
||||
{
|
||||
if (GetPanel(kind) is { } panel)
|
||||
panel.IsVisible = !panel.IsVisible;
|
||||
}
|
||||
|
||||
public void FocusChatInput() => _chat.FocusInput();
|
||||
|
||||
public void AddDebugToast(string message) => _debugViewModel.AddToast(message);
|
||||
|
||||
private IPanel? GetPanel(DevToolsPanelKind kind) => kind switch
|
||||
{
|
||||
DevToolsPanelKind.Vitals => _vitals,
|
||||
DevToolsPanelKind.Chat => _chat,
|
||||
DevToolsPanelKind.Debug => _debug,
|
||||
DevToolsPanelKind.Settings => _settings,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind)),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns the optional ImGui developer frame, menu policy, panel actions, and
|
||||
/// reusable default layout. Retained gameplay UI remains a separate earlier
|
||||
/// presentation phase.
|
||||
/// </summary>
|
||||
internal sealed class DevToolsFramePresenter
|
||||
{
|
||||
private readonly IDevToolsFrameBackend _backend;
|
||||
private readonly IDevToolsCameraMenuOperations _camera;
|
||||
private readonly IDevToolsCommandBusSource _commands;
|
||||
private readonly IDevToolsPanelSet _panels;
|
||||
private readonly AcDream.App.Diagnostics.FrameProfiler _profiler;
|
||||
|
||||
public DevToolsFramePresenter(
|
||||
IDevToolsFrameBackend backend,
|
||||
IDevToolsCameraMenuOperations camera,
|
||||
IDevToolsCommandBusSource commands,
|
||||
AcDream.App.Diagnostics.FrameProfiler profiler,
|
||||
IDevToolsPanelSet panels)
|
||||
{
|
||||
_backend = backend ?? throw new ArgumentNullException(nameof(backend));
|
||||
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
|
||||
_commands = commands ?? throw new ArgumentNullException(nameof(commands));
|
||||
_profiler = profiler ?? throw new ArgumentNullException(nameof(profiler));
|
||||
_panels = panels ?? throw new ArgumentNullException(nameof(panels));
|
||||
}
|
||||
|
||||
public void BeginFrame(float deltaSeconds) => _backend.BeginFrame(deltaSeconds);
|
||||
|
||||
public void Render(double deltaSeconds, int viewportWidth, int viewportHeight)
|
||||
{
|
||||
var context = new PanelContext((float)deltaSeconds, _commands.Current);
|
||||
DrawMenu(viewportWidth, viewportHeight);
|
||||
_backend.RenderPanels(context);
|
||||
using var stage = _profiler.BeginStage(AcDream.App.Diagnostics.FrameStage.ImGui);
|
||||
_backend.RenderDrawData();
|
||||
}
|
||||
|
||||
public void ToggleDebugPanel()
|
||||
{
|
||||
_panels.Toggle(DevToolsPanelKind.Debug);
|
||||
_panels.AddDebugToast(
|
||||
$"Debug panel {(_panels.IsVisible(DevToolsPanelKind.Debug) ? "ON" : "OFF")}");
|
||||
}
|
||||
|
||||
public void FocusChatInput() => _panels.FocusChatInput();
|
||||
|
||||
public void ToggleSettingsPanel()
|
||||
{
|
||||
_panels.Toggle(DevToolsPanelKind.Settings);
|
||||
}
|
||||
|
||||
public void ResetLayout(
|
||||
int viewportWidth,
|
||||
int viewportHeight,
|
||||
DevToolsPanelLayoutCondition condition)
|
||||
{
|
||||
float width = Math.Max(viewportWidth, 480);
|
||||
float height = Math.Max(viewportHeight, 320);
|
||||
|
||||
SetLayout(
|
||||
DevToolsPanelKind.Vitals,
|
||||
new Vector2(10f, 30f),
|
||||
new Vector2(220f, 110f),
|
||||
condition);
|
||||
SetLayout(
|
||||
DevToolsPanelKind.Chat,
|
||||
new Vector2(10f, height - 320f),
|
||||
new Vector2(450f, 300f),
|
||||
condition);
|
||||
SetLayout(
|
||||
DevToolsPanelKind.Debug,
|
||||
new Vector2(width - 380f, 30f),
|
||||
new Vector2(370f, 520f),
|
||||
condition);
|
||||
if (_panels.Contains(DevToolsPanelKind.Settings))
|
||||
{
|
||||
SetLayout(
|
||||
DevToolsPanelKind.Settings,
|
||||
new Vector2((width - 700f) * 0.5f, (height - 500f) * 0.5f),
|
||||
new Vector2(700f, 500f),
|
||||
condition);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawMenu(int viewportWidth, int viewportHeight)
|
||||
{
|
||||
if (!_backend.BeginMainMenuBar())
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (_backend.BeginMenu("View"))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_panels.Contains(DevToolsPanelKind.Settings)
|
||||
&& _backend.MenuItem("Settings", "F11"))
|
||||
{
|
||||
_panels.Toggle(DevToolsPanelKind.Settings);
|
||||
}
|
||||
if (_backend.MenuItem("Vitals"))
|
||||
_panels.Toggle(DevToolsPanelKind.Vitals);
|
||||
if (_backend.MenuItem("Chat"))
|
||||
_panels.Toggle(DevToolsPanelKind.Chat);
|
||||
if (_backend.MenuItem("Debug", "Ctrl+F1"))
|
||||
_panels.Toggle(DevToolsPanelKind.Debug);
|
||||
_backend.Separator();
|
||||
if (_backend.MenuItem("Reset window layout"))
|
||||
ResetLayout(
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
DevToolsPanelLayoutCondition.Always);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_backend.EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
if (_backend.BeginMenu("Camera"))
|
||||
{
|
||||
try
|
||||
{
|
||||
string flyLabel = _camera.IsFlyMode
|
||||
? "Exit Free-Fly Mode"
|
||||
: "Enter Free-Fly Mode";
|
||||
if (_backend.MenuItem(flyLabel, "Ctrl+Shift+F"))
|
||||
_camera.ToggleFlyOrChase();
|
||||
|
||||
bool collide = _camera.CollideCamera;
|
||||
if (_backend.MenuItem(
|
||||
"Collide Camera (spring arm)",
|
||||
selected: collide))
|
||||
{
|
||||
_camera.CollideCamera = !collide;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_backend.EndMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_backend.EndMainMenuBar();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLayout(
|
||||
DevToolsPanelKind panel,
|
||||
Vector2 position,
|
||||
Vector2 size,
|
||||
DevToolsPanelLayoutCondition condition)
|
||||
{
|
||||
string? title = _panels.GetTitle(panel);
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
_backend.SetWindowLayout(title, position, size, condition);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
309
src/AcDream.App/Rendering/PaperdollFramePresenter.cs
Normal file
309
src/AcDream.App/Rendering/PaperdollFramePresenter.cs
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal interface IPaperdollDollRenderer
|
||||
{
|
||||
void SetDoll(WorldEntity? doll);
|
||||
|
||||
uint Render(int width, int height);
|
||||
}
|
||||
|
||||
internal interface IPaperdollFrameView
|
||||
{
|
||||
bool TryGetVisibleSize(out int width, out int height);
|
||||
|
||||
void SetTextureHandle(uint textureHandle);
|
||||
}
|
||||
|
||||
internal interface IPaperdollInventoryVisibility
|
||||
{
|
||||
bool IsVisible { get; }
|
||||
}
|
||||
|
||||
internal interface IPaperdollDollFactory
|
||||
{
|
||||
bool TryBuild(out WorldEntity? doll);
|
||||
}
|
||||
|
||||
internal interface IPaperdollEntityLookup
|
||||
{
|
||||
bool TryGet(uint serverGuid, out WorldEntity player);
|
||||
}
|
||||
|
||||
internal interface IPaperdollPoseApplicator
|
||||
{
|
||||
void Apply(WorldEntity doll, uint setupId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns paperdoll dirty/rebuild state and the private FBO presentation edge.
|
||||
/// The GL renderer remains a borrowed resource disposed by the existing window
|
||||
/// shutdown transaction.
|
||||
/// </summary>
|
||||
internal sealed class PaperdollFramePresenter
|
||||
{
|
||||
private readonly IPaperdollDollRenderer _renderer;
|
||||
private readonly IPaperdollFrameView _view;
|
||||
private readonly IPaperdollDollFactory _factory;
|
||||
private bool _dirty = true;
|
||||
|
||||
public PaperdollFramePresenter(
|
||||
IPaperdollDollRenderer renderer,
|
||||
IPaperdollFrameView view,
|
||||
IPaperdollDollFactory factory)
|
||||
{
|
||||
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
|
||||
_view = view ?? throw new ArgumentNullException(nameof(view));
|
||||
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
internal bool IsDirty => _dirty;
|
||||
|
||||
public void MarkDirty() => _dirty = true;
|
||||
|
||||
public void Render()
|
||||
{
|
||||
if (!_view.TryGetVisibleSize(out int width, out int height))
|
||||
return;
|
||||
|
||||
if (_dirty)
|
||||
{
|
||||
if (_factory.TryBuild(out WorldEntity? doll))
|
||||
{
|
||||
_renderer.SetDoll(doll);
|
||||
_dirty = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Retail's private viewport remains present while the live
|
||||
// player projection is still materializing. Keep the dirty
|
||||
// edge armed and clear any doll from the prior session.
|
||||
_renderer.SetDoll(null);
|
||||
}
|
||||
}
|
||||
|
||||
_view.SetTextureHandle(_renderer.Render(width, height));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Retained-UI visibility and texture publication for the doll view.</summary>
|
||||
internal sealed class RetailPaperdollFrameView : IPaperdollFrameView
|
||||
{
|
||||
private readonly UiViewport _viewport;
|
||||
private readonly IPaperdollInventoryVisibility _inventory;
|
||||
|
||||
public RetailPaperdollFrameView(
|
||||
UiViewport viewport,
|
||||
IPaperdollInventoryVisibility inventory)
|
||||
{
|
||||
_viewport = viewport ?? throw new ArgumentNullException(nameof(viewport));
|
||||
_inventory = inventory ?? throw new ArgumentNullException(nameof(inventory));
|
||||
}
|
||||
|
||||
public bool TryGetVisibleSize(out int width, out int height)
|
||||
{
|
||||
width = 0;
|
||||
height = 0;
|
||||
if (!_viewport.Visible || !_inventory.IsVisible)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
width = (int)_viewport.Width;
|
||||
height = (int)_viewport.Height;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetTextureHandle(uint textureHandle) =>
|
||||
_viewport.TextureHandle = textureHandle;
|
||||
}
|
||||
|
||||
/// <summary>Narrow visibility adapter for the paperdoll's inventory host.</summary>
|
||||
internal sealed class PaperdollInventoryVisibility : IPaperdollInventoryVisibility
|
||||
{
|
||||
private readonly UiElement _inventoryFrame;
|
||||
|
||||
public PaperdollInventoryVisibility(UiElement inventoryFrame)
|
||||
{
|
||||
_inventoryFrame = inventoryFrame
|
||||
?? throw new ArgumentNullException(nameof(inventoryFrame));
|
||||
}
|
||||
|
||||
public bool IsVisible => _inventoryFrame.Visible;
|
||||
}
|
||||
|
||||
/// <summary>Canonical live-entity lookup used by the paperdoll factory.</summary>
|
||||
internal sealed class LivePaperdollEntityLookup : IPaperdollEntityLookup
|
||||
{
|
||||
private readonly LiveEntityRuntime _liveEntities;
|
||||
|
||||
public LivePaperdollEntityLookup(LiveEntityRuntime liveEntities)
|
||||
{
|
||||
_liveEntities = liveEntities
|
||||
?? throw new ArgumentNullException(nameof(liveEntities));
|
||||
}
|
||||
|
||||
public bool TryGet(uint serverGuid, out WorldEntity player) =>
|
||||
_liveEntities.TryGetWorldEntity(serverGuid, out player);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the static retail paperdoll clone from the canonical live player
|
||||
/// projection and applies the DAT-defined held pose.
|
||||
/// </summary>
|
||||
internal sealed class RetailPaperdollDollFactory : IPaperdollDollFactory
|
||||
{
|
||||
private readonly IPaperdollEntityLookup _entities;
|
||||
private readonly ILocalPlayerIdentitySource _identity;
|
||||
private readonly IPaperdollPoseApplicator _pose;
|
||||
|
||||
public RetailPaperdollDollFactory(
|
||||
IPaperdollEntityLookup entities,
|
||||
ILocalPlayerIdentitySource identity,
|
||||
IPaperdollPoseApplicator pose)
|
||||
{
|
||||
_entities = entities ?? throw new ArgumentNullException(nameof(entities));
|
||||
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
||||
_pose = pose ?? throw new ArgumentNullException(nameof(pose));
|
||||
}
|
||||
|
||||
public bool TryBuild(out WorldEntity? doll)
|
||||
{
|
||||
doll = null;
|
||||
if (!_entities.TryGet(_identity.ServerGuid, out WorldEntity player)
|
||||
|| player.MeshRefs.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint? basePalette = null;
|
||||
List<(uint, byte, byte)>? subPalettes = null;
|
||||
if (player.PaletteOverride is { } palette)
|
||||
{
|
||||
basePalette = palette.BasePaletteId;
|
||||
subPalettes = new List<(uint, byte, byte)>();
|
||||
foreach (var range in palette.SubPalettes)
|
||||
{
|
||||
subPalettes.Add((
|
||||
range.SubPaletteId,
|
||||
range.Offset,
|
||||
range.Length));
|
||||
}
|
||||
}
|
||||
|
||||
List<(byte, uint)>? partOverrides = null;
|
||||
if (player.PartOverrides.Count > 0)
|
||||
{
|
||||
partOverrides = new List<(byte, uint)>(player.PartOverrides.Count);
|
||||
foreach (var part in player.PartOverrides)
|
||||
partOverrides.Add((part.PartIndex, part.GfxObjId));
|
||||
}
|
||||
|
||||
doll = DollEntityBuilder.Build(
|
||||
player.SourceGfxObjOrSetupId,
|
||||
new List<MeshRef>(player.MeshRefs),
|
||||
basePalette,
|
||||
subPalettes,
|
||||
partOverrides);
|
||||
_pose.Apply(doll, player.SourceGfxObjOrSetupId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Applies retail's DAT-defined settled paperdoll stance.</summary>
|
||||
internal sealed class RetailPaperdollPoseApplicator : IPaperdollPoseApplicator
|
||||
{
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly IAnimationLoader _animations;
|
||||
private readonly object _datLock;
|
||||
|
||||
public RetailPaperdollPoseApplicator(
|
||||
IDatReaderWriter dats,
|
||||
IAnimationLoader animations,
|
||||
object datLock)
|
||||
{
|
||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||
_animations = animations ?? throw new ArgumentNullException(nameof(animations));
|
||||
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>gmPaperDollUI</c> resolves its held pose with
|
||||
/// <c>DBCache::GetDIDFromEnumStatic(0x10000005, 7)</c>. The master map
|
||||
/// therefore resolves key 7 to a sub-map, then key 0x10000005 to the
|
||||
/// Animation DID.
|
||||
/// </summary>
|
||||
private uint ResolvePoseDid()
|
||||
{
|
||||
uint masterDid = (uint)_dats.Portal.Db.Header.MasterMapId;
|
||||
if (masterDid == 0
|
||||
|| !_dats.Portal.TryGet<DatReaderWriter.DBObjs.EnumIDMap>(
|
||||
masterDid,
|
||||
out var master)
|
||||
|| !master.ClientEnumToID.TryGetValue(7u, out uint subDid)
|
||||
|| !_dats.Portal.TryGet<DatReaderWriter.DBObjs.EnumIDMap>(
|
||||
subDid,
|
||||
out var sub))
|
||||
{
|
||||
return 0u;
|
||||
}
|
||||
|
||||
return sub.ClientEnumToID.TryGetValue(0x10000005u, out uint did)
|
||||
? did
|
||||
: 0u;
|
||||
}
|
||||
|
||||
public void Apply(WorldEntity doll, uint setupId)
|
||||
{
|
||||
DatReaderWriter.DBObjs.Animation? animation;
|
||||
DatReaderWriter.DBObjs.Setup? setup;
|
||||
lock (_datLock)
|
||||
{
|
||||
uint poseDid = ResolvePoseDid();
|
||||
if ((poseDid >> 24) != 0x03u)
|
||||
return;
|
||||
|
||||
animation = _animations.LoadAnimation(poseDid);
|
||||
setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(setupId);
|
||||
}
|
||||
if (animation is null || setup is null || animation.PartFrames.Count == 0)
|
||||
return;
|
||||
|
||||
// RedressCreature @ 0x004A3C22 installs the pose with zero frame rate
|
||||
// and holds the settled final frame.
|
||||
var frame = animation.PartFrames[^1];
|
||||
var reposed = new List<MeshRef>(doll.MeshRefs.Count);
|
||||
for (int index = 0; index < doll.MeshRefs.Count; index++)
|
||||
{
|
||||
Vector3 scale = index < setup.DefaultScale.Count
|
||||
? setup.DefaultScale[index]
|
||||
: Vector3.One;
|
||||
Vector3 origin = Vector3.Zero;
|
||||
Quaternion orientation = Quaternion.Identity;
|
||||
if (index < frame.Frames.Count)
|
||||
{
|
||||
origin = frame.Frames[index].Origin;
|
||||
orientation = frame.Frames[index].Orientation;
|
||||
}
|
||||
|
||||
Matrix4x4 transform = Matrix4x4.CreateScale(scale)
|
||||
* Matrix4x4.CreateFromQuaternion(orientation)
|
||||
* Matrix4x4.CreateTranslation(origin);
|
||||
MeshRef source = doll.MeshRefs[index];
|
||||
reposed.Add(new MeshRef(source.GfxObjId, transform)
|
||||
{
|
||||
SurfaceOverrides = source.SurfaceOverrides,
|
||||
});
|
||||
}
|
||||
|
||||
doll.MeshRefs = reposed;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,22 +12,24 @@ namespace AcDream.App.Rendering;
|
|||
/// <summary>
|
||||
/// Render-to-texture renderer for the paperdoll 3-D doll (the C# analog of retail
|
||||
/// <c>CreatureMode::Render</c>). Draws ONE re-dressed player clone (a <see cref="WorldEntity"/>,
|
||||
/// built by <see cref="DollEntityBuilder"/> + wired by GameWindow) into a private off-screen
|
||||
/// built by <see cref="DollEntityBuilder"/> + wired by <see cref="PaperdollFramePresenter"/>) into a private off-screen
|
||||
/// framebuffer with a fixed <see cref="DollCamera"/> + one distant light, then hands the color
|
||||
/// texture to the <see cref="UiViewport"/> widget which blits it as a normal 2-D sprite.
|
||||
///
|
||||
/// <para>The whole 3-D pass is sealed in a <see cref="GLStateScope"/> so it can't disturb the
|
||||
/// surrounding world / UI GL state ([[feedback_render_self_contained_gl_state]]). It runs in the
|
||||
/// per-frame pre-UI hook (GameWindow), gated on inventory-open ∧ doll-view — NOT from the widget's
|
||||
/// per-frame pre-UI hook, gated on inventory-open ∧ doll-view — NOT from the widget's
|
||||
/// 2-D OnDraw.</para>
|
||||
///
|
||||
/// <para>Reuses the existing <see cref="WbDrawDispatcher"/> + the player's already-resolved,
|
||||
/// already-GPU-resident MeshRefs (the doll clones them), so no separate mesh/texture upload is
|
||||
/// needed. With <c>frustum: null</c> the doll's synthetic landblock is always "visible" and the
|
||||
/// entity is walked from <c>entry.Entities</c> and drawn from its current MeshRefs — a STATIC pose
|
||||
/// for now; idle animation (TickAnimations rebuilding the doll's MeshRefs) is a later slice.</para>
|
||||
/// entity is walked from <c>entry.Entities</c> and drawn from its current MeshRefs — a static held pose.</para>
|
||||
/// </summary>
|
||||
public sealed unsafe class PaperdollViewportRenderer : IUiViewportRenderer, IDisposable
|
||||
public sealed unsafe class PaperdollViewportRenderer :
|
||||
IUiViewportRenderer,
|
||||
IPaperdollDollRenderer,
|
||||
IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly WbDrawDispatcher _dispatcher;
|
||||
|
|
@ -42,8 +44,7 @@ public sealed unsafe class PaperdollViewportRenderer : IUiViewportRenderer, IDis
|
|||
private int _fbW;
|
||||
private int _fbH;
|
||||
|
||||
// The doll WorldEntity (held by reference: when GameWindow's TickAnimations later rebuilds its
|
||||
// MeshRefs each frame, this renderer sees the updated pose automatically). null = nothing to draw.
|
||||
// The static held-pose doll. null = nothing to draw.
|
||||
private WorldEntity? _doll;
|
||||
|
||||
// Synthetic landblock for the doll's one-entry Draw tuple. With frustum:null + this as
|
||||
|
|
@ -70,7 +71,7 @@ public sealed unsafe class PaperdollViewportRenderer : IUiViewportRenderer, IDis
|
|||
DollEntityBuilder.DollRenderId);
|
||||
}
|
||||
|
||||
/// <summary>Set (or clear) the doll entity. GameWindow builds/re-dresses it from the live player.</summary>
|
||||
/// <summary>Set (or clear) the doll entity built from the live player projection.</summary>
|
||||
public void SetDoll(WorldEntity? doll)
|
||||
{
|
||||
if (ReferenceEquals(_doll, doll))
|
||||
|
|
|
|||
168
src/AcDream.App/Rendering/RenderFrameDiagnosticSources.cs
Normal file
168
src/AcDream.App/Rendering/RenderFrameDiagnosticSources.cs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Vfx;
|
||||
using AcDream.Core.World;
|
||||
using Silk.NET.Windowing;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal sealed class RuntimeRenderFrameTitleFactsSource : IRenderFrameTitleFactsSource
|
||||
{
|
||||
private readonly GpuWorldState _world;
|
||||
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _animations;
|
||||
private readonly WorldTimeService _time;
|
||||
|
||||
public RuntimeRenderFrameTitleFactsSource(
|
||||
GpuWorldState world,
|
||||
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animations,
|
||||
WorldTimeService time)
|
||||
{
|
||||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||
_animations = animations ?? throw new ArgumentNullException(nameof(animations));
|
||||
_time = time ?? throw new ArgumentNullException(nameof(time));
|
||||
}
|
||||
|
||||
public RenderFrameTitleFacts Capture() => new(
|
||||
_world.Entities.Count,
|
||||
_animations.Count,
|
||||
DerethDateTime.ToCalendar(_time.NowTicks),
|
||||
_time.DayFraction);
|
||||
}
|
||||
|
||||
internal sealed class SilkRenderFrameTitleSink : IRenderFrameTitleSink
|
||||
{
|
||||
private readonly IWindow _window;
|
||||
|
||||
public SilkRenderFrameTitleSink(IWindow window) =>
|
||||
_window = window ?? throw new ArgumentNullException(nameof(window));
|
||||
|
||||
public void SetTitle(string title) => _window.Title = title;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read-only sampler for the explicit, low-frequency GPU-stream diagnostic.
|
||||
/// It borrows canonical owners and returns one immutable value snapshot.
|
||||
/// </summary>
|
||||
internal sealed class RuntimeRenderFrameResourceDiagnosticsSource :
|
||||
IRenderFrameResourceDiagnosticsSource
|
||||
{
|
||||
private readonly ParticleSystem? _particles;
|
||||
private readonly ParticleHookSink? _particleBindings;
|
||||
private readonly WbDrawDispatcher? _worldDispatcher;
|
||||
private readonly Wb.EnvCellRenderer? _environmentCells;
|
||||
private readonly ParticleRenderer? _particleRenderer;
|
||||
private readonly TextRenderer? _uiTextRenderer;
|
||||
private readonly PortalDepthMaskRenderer? _portalDepthMask;
|
||||
private readonly ClipFrame? _clipFrame;
|
||||
private readonly TerrainModernRenderer? _terrain;
|
||||
private readonly SceneLightingUboBinding? _lighting;
|
||||
private readonly WbMeshAdapter? _meshes;
|
||||
private readonly TextureCache? _textures;
|
||||
|
||||
public RuntimeRenderFrameResourceDiagnosticsSource(
|
||||
ParticleSystem? particles,
|
||||
ParticleHookSink? particleBindings,
|
||||
WbDrawDispatcher? worldDispatcher,
|
||||
Wb.EnvCellRenderer? environmentCells,
|
||||
ParticleRenderer? particleRenderer,
|
||||
TextRenderer? uiTextRenderer,
|
||||
PortalDepthMaskRenderer? portalDepthMask,
|
||||
ClipFrame? clipFrame,
|
||||
TerrainModernRenderer? terrain,
|
||||
SceneLightingUboBinding? lighting,
|
||||
WbMeshAdapter? meshes,
|
||||
TextureCache? textures)
|
||||
{
|
||||
_particles = particles;
|
||||
_particleBindings = particleBindings;
|
||||
_worldDispatcher = worldDispatcher;
|
||||
_environmentCells = environmentCells;
|
||||
_particleRenderer = particleRenderer;
|
||||
_uiTextRenderer = uiTextRenderer;
|
||||
_portalDepthMask = portalDepthMask;
|
||||
_clipFrame = clipFrame;
|
||||
_terrain = terrain;
|
||||
_lighting = lighting;
|
||||
_meshes = meshes;
|
||||
_textures = textures;
|
||||
}
|
||||
|
||||
public RenderFrameResourceDiagnosticsSnapshot Capture()
|
||||
{
|
||||
(int particleSets, long particleBytes) =
|
||||
_particleRenderer?.DynamicBufferDiagnostics ?? default;
|
||||
var meshManager = _meshes?.MeshManager;
|
||||
var mesh = meshManager?.Diagnostics ?? default;
|
||||
var globalMesh = meshManager?.GlobalBuffer;
|
||||
var cpuMesh = _meshes?.CpuMeshCacheDiagnostics ?? default;
|
||||
GCMemoryInfo gc = GC.GetGCMemoryInfo();
|
||||
|
||||
return new RenderFrameResourceDiagnosticsSnapshot(
|
||||
new VfxStreamResourceDiagnostics(
|
||||
_particles?.ActiveEmitterCount ?? 0,
|
||||
_particles?.ActiveParticleCount ?? 0,
|
||||
_particleBindings?.ActiveBindingCount ?? 0),
|
||||
new DynamicBufferResourceDiagnostics(
|
||||
_worldDispatcher?.DynamicBufferSetCount ?? 0,
|
||||
_environmentCells?.DynamicBufferSetCount ?? 0,
|
||||
particleSets,
|
||||
particleBytes,
|
||||
_uiTextRenderer?.DynamicBufferCapacityBytes ?? 0,
|
||||
_portalDepthMask?.DynamicBufferCapacityBytes ?? 0,
|
||||
_clipFrame?.DynamicBufferSetCount ?? 0,
|
||||
_terrain?.DynamicIndirectBufferCount ?? 0,
|
||||
_lighting?.DynamicBufferCount ?? 0),
|
||||
new MeshStreamResourceDiagnostics(
|
||||
mesh.RenderData,
|
||||
mesh.AtlasArrays,
|
||||
mesh.UnusedLru,
|
||||
mesh.EstimatedBytes,
|
||||
globalMesh?.UploadCount ?? 0,
|
||||
globalMesh?.UploadedBytes ?? 0,
|
||||
_meshes?.LastUploadCount ?? 0,
|
||||
_meshes?.LastUploadBytes ?? 0,
|
||||
_meshes?.LastArrayAllocationBytes ?? 0,
|
||||
_meshes?.LastPlannedMipmapBytes ?? 0,
|
||||
_meshes?.LastNewArrayCount ?? 0,
|
||||
_meshes?.LastBufferUploadBytes ?? 0,
|
||||
_meshes?.LastBufferAllocationBytes ?? 0,
|
||||
_meshes?.LastBufferCopyBytes ?? 0,
|
||||
_meshes?.LastNewBufferCount ?? 0,
|
||||
_meshes?.StagedUploadBacklog ?? 0,
|
||||
_meshes?.StagedUploadBytes ?? 0,
|
||||
_meshes?.StagingAtHighWater ?? false,
|
||||
_meshes?.LastStaleDiscardCount ?? 0,
|
||||
cpuMesh.Count,
|
||||
cpuMesh.Bytes,
|
||||
_meshes?.LastMipmapArrayCount ?? 0,
|
||||
_meshes?.LastMipmapBytes ?? 0,
|
||||
globalMesh?.CapacityBytes ?? 0,
|
||||
globalMesh?.PhysicalCapacityBytes ?? 0,
|
||||
globalMesh?.IsMigrationInProgress ?? false),
|
||||
new TextureStreamResourceDiagnostics(
|
||||
_textures?.OwnedBindlessTextureCount ?? 0,
|
||||
_textures?.TextureOwnerCount ?? 0,
|
||||
_textures?.ActiveParticleTextureCount ?? 0,
|
||||
_textures?.ParticleTextureOwnerCount ?? 0,
|
||||
_textures?.CachedParticleTextureCount ?? 0,
|
||||
_textures?.CachedUnownedParticleTextureCount ?? 0,
|
||||
_textures?.CachedUnownedParticleTextureBytes ?? 0,
|
||||
_textures?.CachedCompositeTextureCount ?? 0,
|
||||
_textures?.CachedUnownedCompositeCount ?? 0,
|
||||
_textures?.CachedUnownedCompositeBytes ?? 0,
|
||||
_textures?.CompositeAtlasCount ?? 0,
|
||||
_textures?.CompositeAtlasBytes ?? 0,
|
||||
_textures?.CompositeFrameUploadCount ?? 0,
|
||||
_textures?.CompositeFrameUploadBytes ?? 0,
|
||||
_worldDispatcher?.LastCompositeWarmupPendingCount ?? 0),
|
||||
new ProcessResourceDiagnostics(
|
||||
GC.GetTotalMemory(false),
|
||||
gc.TotalCommittedBytes,
|
||||
GpuMemoryTracker.AllocatedBytes,
|
||||
GpuMemoryTracker.BufferCount,
|
||||
GpuMemoryTracker.TextureCount));
|
||||
}
|
||||
}
|
||||
269
src/AcDream.App/Rendering/RenderFrameDiagnosticsController.cs
Normal file
269
src/AcDream.App/Rendering/RenderFrameDiagnosticsController.cs
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>Current values published by the successful render-frame cadence.</summary>
|
||||
internal readonly record struct RenderFrameDiagnosticsSnapshot(
|
||||
double Fps,
|
||||
double FrameMilliseconds,
|
||||
int VisibleLandblocks,
|
||||
int TotalLandblocks,
|
||||
int EntityCount,
|
||||
int AnimatedEntityCount)
|
||||
{
|
||||
public static RenderFrameDiagnosticsSnapshot Initial => new(
|
||||
Fps: 60.0,
|
||||
FrameMilliseconds: 16.7,
|
||||
VisibleLandblocks: 0,
|
||||
TotalLandblocks: 0,
|
||||
EntityCount: 0,
|
||||
AnimatedEntityCount: 0);
|
||||
}
|
||||
|
||||
/// <summary>Non-render facts sampled only when the title cadence publishes.</summary>
|
||||
internal readonly record struct RenderFrameTitleFacts(
|
||||
int EntityCount,
|
||||
int AnimatedEntityCount,
|
||||
DerethDateTime.Calendar Calendar,
|
||||
double DayFraction);
|
||||
|
||||
internal interface IRenderFrameTitleFactsSource
|
||||
{
|
||||
RenderFrameTitleFacts Capture();
|
||||
}
|
||||
|
||||
/// <summary>Narrow host seam; diagnostics can publish chrome without retaining a window.</summary>
|
||||
internal interface IRenderFrameTitleSink
|
||||
{
|
||||
void SetTitle(string title);
|
||||
}
|
||||
|
||||
internal interface IRenderFrameDiagnosticLog
|
||||
{
|
||||
void WriteLine(string message);
|
||||
}
|
||||
|
||||
internal sealed class ConsoleRenderFrameDiagnosticLog : IRenderFrameDiagnosticLog
|
||||
{
|
||||
public void WriteLine(string message) => Console.WriteLine(message);
|
||||
}
|
||||
|
||||
internal interface IRenderFrameResourceDiagnosticsSource
|
||||
{
|
||||
RenderFrameResourceDiagnosticsSnapshot Capture();
|
||||
}
|
||||
|
||||
internal readonly record struct VfxStreamResourceDiagnostics(
|
||||
int ActiveEmitters,
|
||||
int ActiveParticles,
|
||||
int ActiveBindings);
|
||||
|
||||
internal readonly record struct DynamicBufferResourceDiagnostics(
|
||||
int WorldBufferSets,
|
||||
int EnvCellBufferSets,
|
||||
int ParticleBufferSets,
|
||||
long ParticleBufferBytes,
|
||||
long UiTextBufferBytes,
|
||||
long PortalDepthMaskBufferBytes,
|
||||
int ClipBufferSets,
|
||||
int TerrainIndirectBuffers,
|
||||
int SceneLightingBuffers);
|
||||
|
||||
internal readonly record struct MeshStreamResourceDiagnostics(
|
||||
int RenderData,
|
||||
int AtlasArrays,
|
||||
int UnusedLru,
|
||||
long EstimatedBytes,
|
||||
long GlobalUploadCount,
|
||||
long GlobalUploadedBytes,
|
||||
int FrameUploadCount,
|
||||
long FrameUploadBytes,
|
||||
long FrameArrayAllocationBytes,
|
||||
long FramePlannedMipmapBytes,
|
||||
int FrameNewArrayCount,
|
||||
long FrameBufferUploadBytes,
|
||||
long FrameBufferAllocationBytes,
|
||||
long FrameBufferCopyBytes,
|
||||
int FrameNewBufferCount,
|
||||
int StagedUploadBacklog,
|
||||
long StagedUploadBytes,
|
||||
bool StagingAtHighWater,
|
||||
int FrameStaleDiscardCount,
|
||||
int CpuMeshCacheCount,
|
||||
long CpuMeshCacheBytes,
|
||||
int FrameMipmapArrayCount,
|
||||
long FrameMipmapBytes,
|
||||
long GlobalCapacityBytes,
|
||||
long GlobalPhysicalCapacityBytes,
|
||||
bool GlobalMigrationInProgress);
|
||||
|
||||
internal readonly record struct TextureStreamResourceDiagnostics(
|
||||
int OwnedBindlessTextures,
|
||||
int TextureOwners,
|
||||
int ActiveParticleTextures,
|
||||
int ParticleTextureOwners,
|
||||
int CachedParticleTextures,
|
||||
int CachedUnownedParticleTextures,
|
||||
long CachedUnownedParticleTextureBytes,
|
||||
int CachedCompositeTextures,
|
||||
int CachedUnownedComposites,
|
||||
long CachedUnownedCompositeBytes,
|
||||
int CompositeAtlases,
|
||||
long CompositeAtlasBytes,
|
||||
int FrameCompositeUploadCount,
|
||||
long FrameCompositeUploadBytes,
|
||||
int CompositeWarmupPending);
|
||||
|
||||
internal readonly record struct ProcessResourceDiagnostics(
|
||||
long ManagedBytes,
|
||||
long ManagedCommittedBytes,
|
||||
long TrackedGpuBytes,
|
||||
int TrackedGpuBuffers,
|
||||
int TrackedGpuTextures);
|
||||
|
||||
/// <summary>
|
||||
/// Immutable resource facts captured only when explicit UI-probe dumping is enabled.
|
||||
/// Grouping keeps the diagnostics controller independent from every canonical renderer,
|
||||
/// VFX, mesh, texture, and process owner used to produce the values.
|
||||
/// </summary>
|
||||
internal readonly record struct RenderFrameResourceDiagnosticsSnapshot(
|
||||
VfxStreamResourceDiagnostics Vfx,
|
||||
DynamicBufferResourceDiagnostics DynamicBuffers,
|
||||
MeshStreamResourceDiagnostics Mesh,
|
||||
TextureStreamResourceDiagnostics Textures,
|
||||
ProcessResourceDiagnostics Process);
|
||||
|
||||
/// <summary>
|
||||
/// Publishes post-presentation frame statistics. The caller invokes this only after
|
||||
/// screenshot capture, preserving the accepted screenshot-before-title/resource order.
|
||||
/// </summary>
|
||||
internal sealed class RenderFrameDiagnosticsController : IRenderFrameDiagnosticsPhase
|
||||
{
|
||||
internal const double PublicationIntervalSeconds = 0.5;
|
||||
|
||||
private readonly IRenderFrameTitleFactsSource _titleFacts;
|
||||
private readonly IRenderFrameTitleSink _titleSink;
|
||||
private readonly IRenderFrameResourceDiagnosticsSource? _resources;
|
||||
private readonly IRenderFrameDiagnosticLog _log;
|
||||
private readonly bool _publishResourceDiagnostics;
|
||||
|
||||
private double _elapsedSeconds;
|
||||
private int _frameCount;
|
||||
|
||||
public RenderFrameDiagnosticsSnapshot Snapshot { get; private set; } =
|
||||
RenderFrameDiagnosticsSnapshot.Initial;
|
||||
|
||||
public RenderFrameDiagnosticsController(
|
||||
IRenderFrameTitleFactsSource titleFacts,
|
||||
IRenderFrameTitleSink titleSink,
|
||||
IRenderFrameDiagnosticLog log,
|
||||
bool publishResourceDiagnostics,
|
||||
IRenderFrameResourceDiagnosticsSource? resources = null)
|
||||
{
|
||||
_titleFacts = titleFacts ?? throw new ArgumentNullException(nameof(titleFacts));
|
||||
_titleSink = titleSink ?? throw new ArgumentNullException(nameof(titleSink));
|
||||
_log = log ?? throw new ArgumentNullException(nameof(log));
|
||||
_publishResourceDiagnostics = publishResourceDiagnostics;
|
||||
_resources = publishResourceDiagnostics
|
||||
? resources ?? throw new ArgumentNullException(nameof(resources))
|
||||
: resources;
|
||||
}
|
||||
|
||||
public void Publish(RenderFrameInput input, RenderFrameOutcome outcome)
|
||||
{
|
||||
_elapsedSeconds += input.DeltaSeconds;
|
||||
_frameCount++;
|
||||
if (_elapsedSeconds < PublicationIntervalSeconds)
|
||||
return;
|
||||
|
||||
double averageFrameMilliseconds = _elapsedSeconds / _frameCount * 1000.0;
|
||||
double fps = _frameCount / _elapsedSeconds;
|
||||
RenderFrameTitleFacts facts = _titleFacts.Capture();
|
||||
|
||||
// Keep external publication ahead of the cached values and accumulator reset.
|
||||
// If either sink fails, the controller retains the accumulated window exactly as
|
||||
// the previous GameWindow body did and a later successful frame can retry it.
|
||||
_titleSink.SetTitle(FormatTitle(
|
||||
fps,
|
||||
averageFrameMilliseconds,
|
||||
outcome.World.VisibleLandblocks,
|
||||
outcome.World.TotalLandblocks,
|
||||
facts));
|
||||
|
||||
if (_publishResourceDiagnostics)
|
||||
{
|
||||
RenderFrameResourceDiagnosticsSnapshot resources = _resources!.Capture();
|
||||
_log.WriteLine(FormatGpuStream(resources));
|
||||
}
|
||||
|
||||
Snapshot = new RenderFrameDiagnosticsSnapshot(
|
||||
fps,
|
||||
averageFrameMilliseconds,
|
||||
outcome.World.VisibleLandblocks,
|
||||
outcome.World.TotalLandblocks,
|
||||
facts.EntityCount,
|
||||
facts.AnimatedEntityCount);
|
||||
_elapsedSeconds = 0;
|
||||
_frameCount = 0;
|
||||
}
|
||||
|
||||
internal static string FormatTitle(
|
||||
double fps,
|
||||
double averageFrameMilliseconds,
|
||||
int visibleLandblocks,
|
||||
int totalLandblocks,
|
||||
RenderFrameTitleFacts facts) =>
|
||||
$"acdream | {fps:F0} fps | {averageFrameMilliseconds:F1} ms | "
|
||||
+ $"lb {visibleLandblocks}/{totalLandblocks} | "
|
||||
+ $"ent {facts.EntityCount}/anim {facts.AnimatedEntityCount} | "
|
||||
+ $"PY{facts.Calendar.Year} {facts.Calendar.Month} {facts.Calendar.Day} "
|
||||
+ $"{facts.Calendar.Hour} (df={facts.DayFraction:F4})";
|
||||
|
||||
internal static string FormatGpuStream(RenderFrameResourceDiagnosticsSnapshot snapshot)
|
||||
{
|
||||
VfxStreamResourceDiagnostics vfx = snapshot.Vfx;
|
||||
DynamicBufferResourceDiagnostics buffers = snapshot.DynamicBuffers;
|
||||
MeshStreamResourceDiagnostics mesh = snapshot.Mesh;
|
||||
TextureStreamResourceDiagnostics textures = snapshot.Textures;
|
||||
ProcessResourceDiagnostics process = snapshot.Process;
|
||||
return
|
||||
$"[gpu-stream] emit={vfx.ActiveEmitters} "
|
||||
+ $"particles={vfx.ActiveParticles} "
|
||||
+ $"bindings={vfx.ActiveBindings} "
|
||||
+ $"wbSets={buffers.WorldBufferSets} "
|
||||
+ $"cellSets={buffers.EnvCellBufferSets} "
|
||||
+ $"particleSets={buffers.ParticleBufferSets}/{buffers.ParticleBufferBytes}B "
|
||||
+ $"uiBytes={buffers.UiTextBufferBytes} "
|
||||
+ $"portalBytes={buffers.PortalDepthMaskBufferBytes} "
|
||||
+ $"clipSets={buffers.ClipBufferSets} "
|
||||
+ $"terrainBuffers={buffers.TerrainIndirectBuffers} "
|
||||
+ $"lightBuffers={buffers.SceneLightingBuffers} "
|
||||
+ $"mesh={mesh.RenderData}/{mesh.AtlasArrays}/{mesh.UnusedLru} "
|
||||
+ $"meshEst={mesh.EstimatedBytes} "
|
||||
+ $"meshUpload={mesh.GlobalUploadCount}/{mesh.GlobalUploadedBytes} "
|
||||
+ $"uploadFrame={mesh.FrameUploadCount}/{mesh.FrameUploadBytes}/"
|
||||
+ $"{mesh.FrameArrayAllocationBytes}/{mesh.FramePlannedMipmapBytes}/"
|
||||
+ $"{mesh.FrameNewArrayCount} "
|
||||
+ $"bufferFrame={mesh.FrameBufferUploadBytes}/{mesh.FrameBufferAllocationBytes}/"
|
||||
+ $"{mesh.FrameBufferCopyBytes}/{mesh.FrameNewBufferCount} "
|
||||
+ $"staging={mesh.StagedUploadBacklog}/{mesh.StagedUploadBytes}/"
|
||||
+ $"{mesh.StagingAtHighWater}/{mesh.FrameStaleDiscardCount} "
|
||||
+ $"cpuMesh={mesh.CpuMeshCacheCount}/{mesh.CpuMeshCacheBytes} "
|
||||
+ $"mipmapFrame={mesh.FrameMipmapArrayCount}/{mesh.FrameMipmapBytes} "
|
||||
+ $"meshCap={mesh.GlobalCapacityBytes} "
|
||||
+ $"meshPhys={mesh.GlobalPhysicalCapacityBytes}/{mesh.GlobalMigrationInProgress} "
|
||||
+ $"gpuTrack={process.TrackedGpuBytes}/{process.TrackedGpuBuffers}/"
|
||||
+ $"{process.TrackedGpuTextures} "
|
||||
+ $"managed={process.ManagedBytes}/{process.ManagedCommittedBytes} "
|
||||
+ $"ownedTextures={textures.OwnedBindlessTextures}/{textures.TextureOwners} "
|
||||
+ $"particleTextures={textures.ActiveParticleTextures}/"
|
||||
+ $"{textures.ParticleTextureOwners}/{textures.CachedParticleTextures}/"
|
||||
+ $"{textures.CachedUnownedParticleTextures}/"
|
||||
+ $"{textures.CachedUnownedParticleTextureBytes} "
|
||||
+ $"compositeCache={textures.CachedCompositeTextures}/"
|
||||
+ $"{textures.CachedUnownedComposites}/{textures.CachedUnownedCompositeBytes} "
|
||||
+ $"compositeAtlas={textures.CompositeAtlases}/{textures.CompositeAtlasBytes} "
|
||||
+ $"compositeUpload={textures.FrameCompositeUploadCount}/"
|
||||
+ $"{textures.FrameCompositeUploadBytes}/{textures.CompositeWarmupPending}";
|
||||
}
|
||||
}
|
||||
54
src/AcDream.App/Rendering/RollingTimingSampleWindow.cs
Normal file
54
src/AcDream.App/Rendering/RollingTimingSampleWindow.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal readonly record struct RollingTimingPercentiles(
|
||||
long MedianHundredthsMicroseconds,
|
||||
long Percentile95HundredthsMicroseconds);
|
||||
|
||||
/// <summary>
|
||||
/// Fixed-capacity rolling timing distribution. Zero and negative values remain
|
||||
/// empty padding and do not dilute the diagnostic percentiles.
|
||||
/// </summary>
|
||||
internal sealed class RollingTimingSampleWindow
|
||||
{
|
||||
private readonly long[] _samples;
|
||||
private int _cursor;
|
||||
|
||||
public RollingTimingSampleWindow(int capacity)
|
||||
{
|
||||
if (capacity <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(capacity));
|
||||
_samples = new long[capacity];
|
||||
}
|
||||
|
||||
public int Capacity => _samples.Length;
|
||||
|
||||
public void PushHundredthsMicroseconds(long sample)
|
||||
{
|
||||
_samples[_cursor] = sample;
|
||||
_cursor = (_cursor + 1) % _samples.Length;
|
||||
}
|
||||
|
||||
public void PushStopwatchTicks(long ticks) =>
|
||||
PushHundredthsMicroseconds(
|
||||
(long)(ticks * 100_000_000.0 / System.Diagnostics.Stopwatch.Frequency));
|
||||
|
||||
public RollingTimingPercentiles Snapshot()
|
||||
{
|
||||
var copy = (long[])_samples.Clone();
|
||||
Array.Sort(copy);
|
||||
int populated = 0;
|
||||
foreach (long sample in copy)
|
||||
{
|
||||
if (sample > 0)
|
||||
populated++;
|
||||
}
|
||||
|
||||
if (populated == 0)
|
||||
return default;
|
||||
|
||||
long median = copy[copy.Length - 1 - (populated - 1) / 2];
|
||||
int p95Offset = (int)((populated - 1) * 0.05);
|
||||
long percentile95 = copy[copy.Length - 1 - p95Offset];
|
||||
return new RollingTimingPercentiles(median, percentile95);
|
||||
}
|
||||
}
|
||||
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