feat(streaming): worker pre-reads ApplyLoadedTerrain dats into the bundle
BuildPhysicsDatBundle mirrors the apply's six Get<T> sites (LandBlockInfo, EnvCell, Environment, building Setup, entity GfxObj, entity Setup) under the worker's existing _datLock and attaches them to LoadedLandblock. Far tier gets PhysicsDatBundle.Empty. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3a0e349c6e
commit
4a99b55a73
1 changed files with 233 additions and 2 deletions
|
|
@ -81,6 +81,47 @@ public sealed class GameWindow : IDisposable
|
||||||
private int _terrainCpuSampleCursor;
|
private int _terrainCpuSampleCursor;
|
||||||
private long _terrainLastDiagTick;
|
private long _terrainLastDiagTick;
|
||||||
|
|
||||||
|
// [FRAME-DIAG] (ACDREAM_WB_DIAG=1): per-frame cost split for the FPS deep-dive.
|
||||||
|
// The crux this measures: terrain APPLY runs in OnUpdate (under _datLock —
|
||||||
|
// dat reads + physics/ShadowObjects/BSP registration + the terrain GPU
|
||||||
|
// upload), while the DRAW + the title-bar ms run in OnRender, so a heavy
|
||||||
|
// apply stalls the loop turn yet is INVISIBLE to the render-only ms. We
|
||||||
|
// accumulate apply cost across each OnUpdate and flush it next to the
|
||||||
|
// [TERRAIN-DIAG] draw cost. Samples are µs×100 over a rolling 256-ring,
|
||||||
|
// reusing the [TERRAIN-DIAG] median/p95 helpers. Removable (mirrors the
|
||||||
|
// probes stripped in 92e95be). Gated once at construction — zero cost off.
|
||||||
|
private readonly bool _frameDiag = string.Equals(
|
||||||
|
System.Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG"), "1", System.StringComparison.Ordinal);
|
||||||
|
private long _applyAccumTicks; // apply CPU accumulated across this OnUpdate
|
||||||
|
private long _applyUploadAccumTicks; // terrain glBufferSubData sub-span this OnUpdate
|
||||||
|
private int _appliesThisUpdate; // # landblock LOADS applied this OnUpdate
|
||||||
|
private int _frameDiagMaxAppliesPerUpdate; // burst size over the 5s window
|
||||||
|
private readonly long[] _applyCpuSamples = new long[256]; // apply µs×100 / Update
|
||||||
|
private int _applyCpuSampleCursor;
|
||||||
|
private readonly long[] _applyUploadSamples = new long[256]; // terrain-upload µs×100 / Update
|
||||||
|
private int _applyUploadSampleCursor;
|
||||||
|
private readonly long[] _entityUploadSamples = new long[256]; // _wbMeshAdapter.Tick µs×100 / Render
|
||||||
|
private int _entityUploadSampleCursor;
|
||||||
|
// Apply CPU split into three sub-spans (to name WHICH part of the 40-110ms
|
||||||
|
// apply dominates): cell-build (EnvCell dat reads + physics surfaces +
|
||||||
|
// buildings), gfxobj-BSP (per-entity physics BSP cache), ShadowObjects+lights
|
||||||
|
// registration. Accumulated per OnUpdate via checkpoints in ApplyLoadedTerrainLocked.
|
||||||
|
private long _applyCellAccumTicks;
|
||||||
|
private long _applyBspAccumTicks;
|
||||||
|
private long _applyShadowAccumTicks;
|
||||||
|
private readonly long[] _applyCellSamples = new long[256];
|
||||||
|
private int _applyCellSampleCursor;
|
||||||
|
private readonly long[] _applyBspSamples = new long[256];
|
||||||
|
private int _applyBspSampleCursor;
|
||||||
|
private readonly long[] _applyShadowSamples = new long[256];
|
||||||
|
private int _applyShadowSampleCursor;
|
||||||
|
// Direct measure of the _datLock acquisition wait (the streaming worker holds
|
||||||
|
// the lock for its full per-landblock build, stalling this apply). Confirms the
|
||||||
|
// apply≫sub-span gap is lock contention, not in-lock work.
|
||||||
|
private long _applyLockWaitAccumTicks;
|
||||||
|
private readonly long[] _applyLockWaitSamples = new long[256];
|
||||||
|
private int _applyLockWaitSampleCursor;
|
||||||
|
|
||||||
// Phase A.1: streaming fields replacing the one-shot _entities list.
|
// Phase A.1: streaming fields replacing the one-shot _entities list.
|
||||||
private AcDream.App.Streaming.LandblockStreamer? _streamer;
|
private AcDream.App.Streaming.LandblockStreamer? _streamer;
|
||||||
private AcDream.App.Streaming.GpuWorldState _worldState = new();
|
private AcDream.App.Streaming.GpuWorldState _worldState = new();
|
||||||
|
|
@ -5896,7 +5937,8 @@ public sealed class GameWindow : IDisposable
|
||||||
return new AcDream.Core.World.LoadedLandblock(
|
return new AcDream.Core.World.LoadedLandblock(
|
||||||
landblockId,
|
landblockId,
|
||||||
heightmapOnly,
|
heightmapOnly,
|
||||||
System.Array.Empty<AcDream.Core.World.WorldEntity>());
|
System.Array.Empty<AcDream.Core.World.WorldEntity>(),
|
||||||
|
AcDream.Core.World.PhysicsDatBundle.Empty); // far tier: no cells/buildings/entities
|
||||||
}
|
}
|
||||||
|
|
||||||
var baseLoaded = AcDream.Core.World.LandblockLoader.Load(_dats, landblockId);
|
var baseLoaded = AcDream.Core.World.LandblockLoader.Load(_dats, landblockId);
|
||||||
|
|
@ -5975,10 +6017,91 @@ public sealed class GameWindow : IDisposable
|
||||||
merged.AddRange(BuildSceneryEntitiesForStreaming(baseLoaded, lbX, lbY));
|
merged.AddRange(BuildSceneryEntitiesForStreaming(baseLoaded, lbX, lbY));
|
||||||
merged.AddRange(BuildInteriorEntitiesForStreaming(landblockId, lbX, lbY));
|
merged.AddRange(BuildInteriorEntitiesForStreaming(landblockId, lbX, lbY));
|
||||||
|
|
||||||
|
// datLock fix: pre-read (under the worker's _datLock) every dat the apply
|
||||||
|
// consumes, so ApplyLoadedTerrainLocked can run lock-free. Built AFTER
|
||||||
|
// `merged` so entity GfxObj/Setup ids are known.
|
||||||
|
var physicsDats = BuildPhysicsDatBundle(landblockId, merged);
|
||||||
return new AcDream.Core.World.LoadedLandblock(
|
return new AcDream.Core.World.LoadedLandblock(
|
||||||
baseLoaded.LandblockId,
|
baseLoaded.LandblockId,
|
||||||
baseLoaded.Heightmap,
|
baseLoaded.Heightmap,
|
||||||
merged);
|
merged,
|
||||||
|
physicsDats);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pre-reads (under the worker's <c>_datLock</c>) every dat object
|
||||||
|
/// <see cref="ApplyLoadedTerrainLocked"/> consumes, so the apply makes zero
|
||||||
|
/// <c>DatCollection</c> calls and the update thread never blocks on the
|
||||||
|
/// worker's lock. MIRRORS the apply's six read sites — gather and apply must
|
||||||
|
/// enumerate the same ids. Reads only; no build / registration.
|
||||||
|
/// </summary>
|
||||||
|
private AcDream.Core.World.PhysicsDatBundle BuildPhysicsDatBundle(
|
||||||
|
uint landblockId,
|
||||||
|
System.Collections.Generic.IReadOnlyList<AcDream.Core.World.WorldEntity> entities)
|
||||||
|
{
|
||||||
|
var envCells = new System.Collections.Generic.Dictionary<uint, DatReaderWriter.DBObjs.EnvCell>();
|
||||||
|
var environments = new System.Collections.Generic.Dictionary<uint, DatReaderWriter.DBObjs.Environment>();
|
||||||
|
var setups = new System.Collections.Generic.Dictionary<uint, DatReaderWriter.DBObjs.Setup>();
|
||||||
|
var gfxObjs = new System.Collections.Generic.Dictionary<uint, DatReaderWriter.DBObjs.GfxObj>();
|
||||||
|
|
||||||
|
// (1) LandBlockInfo
|
||||||
|
var lbInfo = _dats!.Get<DatReaderWriter.DBObjs.LandBlockInfo>(
|
||||||
|
(landblockId & 0xFFFF0000u) | 0xFFFEu);
|
||||||
|
|
||||||
|
if (lbInfo is not null)
|
||||||
|
{
|
||||||
|
// (2)+(3) EnvCell + Environment, per cell
|
||||||
|
if (lbInfo.NumCells > 0)
|
||||||
|
{
|
||||||
|
uint firstCellId = (landblockId & 0xFFFF0000u) | 0x0100u;
|
||||||
|
for (uint offset = 0; offset < lbInfo.NumCells; offset++)
|
||||||
|
{
|
||||||
|
uint envCellId = firstCellId + offset;
|
||||||
|
var envCell = _dats.Get<DatReaderWriter.DBObjs.EnvCell>(envCellId);
|
||||||
|
if (envCell is null) continue;
|
||||||
|
envCells[envCellId] = envCell;
|
||||||
|
if (envCell.EnvironmentId == 0) continue;
|
||||||
|
uint envId = 0x0D000000u | envCell.EnvironmentId;
|
||||||
|
if (!environments.ContainsKey(envId))
|
||||||
|
{
|
||||||
|
var environment = _dats.Get<DatReaderWriter.DBObjs.Environment>(envId);
|
||||||
|
if (environment is not null) environments[envId] = environment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// (4) building shell Setup, per building
|
||||||
|
foreach (var building in lbInfo.Buildings)
|
||||||
|
{
|
||||||
|
uint modelId = building.ModelId;
|
||||||
|
if ((modelId & 0xFF000000u) == 0x02000000u && !setups.ContainsKey(modelId))
|
||||||
|
{
|
||||||
|
var bldSetup = _dats.Get<DatReaderWriter.DBObjs.Setup>(modelId);
|
||||||
|
if (bldSetup is not null) setups[modelId] = bldSetup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// (5)+(6) entity GfxObj (BSP cache) + entity Setup (ShadowObjects parts/lights)
|
||||||
|
foreach (var entity in entities)
|
||||||
|
{
|
||||||
|
uint src = entity.SourceGfxObjOrSetupId;
|
||||||
|
if ((src & 0xFF000000u) == 0x02000000u && !setups.ContainsKey(src))
|
||||||
|
{
|
||||||
|
var s = _dats.Get<DatReaderWriter.DBObjs.Setup>(src);
|
||||||
|
if (s is not null) setups[src] = s;
|
||||||
|
}
|
||||||
|
foreach (var meshRef in entity.MeshRefs)
|
||||||
|
{
|
||||||
|
uint gid = meshRef.GfxObjId;
|
||||||
|
if ((gid & 0xFF000000u) != 0x01000000u) continue;
|
||||||
|
if (gfxObjs.ContainsKey(gid)) continue;
|
||||||
|
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(gid);
|
||||||
|
if (gfx is not null) gfxObjs[gid] = gfx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AcDream.Core.World.PhysicsDatBundle(lbInfo, envCells, environments, setups, gfxObjs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -6493,10 +6616,22 @@ public sealed class GameWindow : IDisposable
|
||||||
// Hold the lock across the entity hydration below (GfxObj sub-mesh
|
// Hold the lock across the entity hydration below (GfxObj sub-mesh
|
||||||
// builds). The terrain mesh is pre-built by the worker (T12) and passed
|
// builds). The terrain mesh is pre-built by the worker (T12) and passed
|
||||||
// in via meshData, so LandblockMesh.Build no longer runs under this lock.
|
// in via meshData, so LandblockMesh.Build no longer runs under this lock.
|
||||||
|
// [FRAME-DIAG]: time the full locked apply (dat reads + physics/registry +
|
||||||
|
// the terrain GPU upload) — this is the OnUpdate cost the title-bar ms misses.
|
||||||
|
long fdT0 = _frameDiag ? System.Diagnostics.Stopwatch.GetTimestamp() : 0L;
|
||||||
lock (_datLock)
|
lock (_datLock)
|
||||||
{
|
{
|
||||||
|
// [FRAME-DIAG]: time blocked acquiring _datLock — the streaming worker
|
||||||
|
// holds it for the full per-landblock build (tens of ms), stalling us.
|
||||||
|
if (_frameDiag)
|
||||||
|
_applyLockWaitAccumTicks += System.Diagnostics.Stopwatch.GetTimestamp() - fdT0;
|
||||||
ApplyLoadedTerrainLocked(lb, meshData);
|
ApplyLoadedTerrainLocked(lb, meshData);
|
||||||
}
|
}
|
||||||
|
if (_frameDiag)
|
||||||
|
{
|
||||||
|
_applyAccumTicks += System.Diagnostics.Stopwatch.GetTimestamp() - fdT0;
|
||||||
|
_appliesThisUpdate++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -6667,7 +6802,16 @@ public sealed class GameWindow : IDisposable
|
||||||
// Phase A.5 T15/T16: route through AddLandblockWithMesh — the named
|
// Phase A.5 T15/T16: route through AddLandblockWithMesh — the named
|
||||||
// two-tier entry point. Delegates to AddLandblock internally; both
|
// two-tier entry point. Delegates to AddLandblock internally; both
|
||||||
// paths share one GPU upload path.
|
// paths share one GPU upload path.
|
||||||
|
// [FRAME-DIAG]: bracket the terrain glBufferSubData sub-span so we can tell
|
||||||
|
// the (tiny ~17KB) GPU upload apart from the dat-read/registration tail.
|
||||||
|
long fdU0 = _frameDiag ? System.Diagnostics.Stopwatch.GetTimestamp() : 0L;
|
||||||
_terrain.AddLandblockWithMesh(lb.LandblockId, meshData, origin);
|
_terrain.AddLandblockWithMesh(lb.LandblockId, meshData, origin);
|
||||||
|
if (_frameDiag)
|
||||||
|
_applyUploadAccumTicks += System.Diagnostics.Stopwatch.GetTimestamp() - fdU0;
|
||||||
|
|
||||||
|
// [FRAME-DIAG]: split the post-upload apply CPU into three sub-spans via
|
||||||
|
// method-scope checkpoints — cell-build → gfxobj-BSP → ShadowObjects+lights.
|
||||||
|
long fdCheck = _frameDiag ? System.Diagnostics.Stopwatch.GetTimestamp() : 0L;
|
||||||
|
|
||||||
// Step 4: drain pending LoadedCells from the worker thread.
|
// Step 4: drain pending LoadedCells from the worker thread.
|
||||||
// Also collect into a local dict for the BuildingLoader stamping pass below.
|
// Also collect into a local dict for the BuildingLoader stamping pass below.
|
||||||
|
|
@ -6916,6 +7060,8 @@ public sealed class GameWindow : IDisposable
|
||||||
// (and _pendingCellMeshes drain) are retired with InstancedMeshRenderer.
|
// (and _pendingCellMeshes drain) are retired with InstancedMeshRenderer.
|
||||||
// Cache GfxObj physics data (BSP trees) for the physics engine — this
|
// Cache GfxObj physics data (BSP trees) for the physics engine — this
|
||||||
// loop is physics-only, not renderer-side.
|
// loop is physics-only, not renderer-side.
|
||||||
|
// [FRAME-DIAG] checkpoint: end cell-build, begin gfxobj-BSP.
|
||||||
|
if (_frameDiag) { long t = System.Diagnostics.Stopwatch.GetTimestamp(); _applyCellAccumTicks += t - fdCheck; fdCheck = t; }
|
||||||
foreach (var entity in lb.Entities)
|
foreach (var entity in lb.Entities)
|
||||||
{
|
{
|
||||||
foreach (var meshRef in entity.MeshRefs)
|
foreach (var meshRef in entity.MeshRefs)
|
||||||
|
|
@ -6930,6 +7076,8 @@ public sealed class GameWindow : IDisposable
|
||||||
// The data is no longer consumed (WB handles EnvCell geometry through
|
// The data is no longer consumed (WB handles EnvCell geometry through
|
||||||
// its own pipeline), but the worker thread still populates this dict.
|
// its own pipeline), but the worker thread still populates this dict.
|
||||||
_pendingCellMeshes.Clear();
|
_pendingCellMeshes.Clear();
|
||||||
|
// [FRAME-DIAG] checkpoint: end gfxobj-BSP, begin ShadowObjects+lights.
|
||||||
|
if (_frameDiag) { long t = System.Diagnostics.Stopwatch.GetTimestamp(); _applyBspAccumTicks += t - fdCheck; fdCheck = t; }
|
||||||
|
|
||||||
// Task 7: register static entities into the ShadowObjectRegistry so the
|
// Task 7: register static entities into the ShadowObjectRegistry so the
|
||||||
// Transition system can find and collide against them during movement.
|
// Transition system can find and collide against them during movement.
|
||||||
|
|
@ -7459,10 +7607,37 @@ public sealed class GameWindow : IDisposable
|
||||||
_worldGameState.Add(snapshot);
|
_worldGameState.Add(snapshot);
|
||||||
_worldEvents.FireEntitySpawned(snapshot);
|
_worldEvents.FireEntitySpawned(snapshot);
|
||||||
}
|
}
|
||||||
|
// [FRAME-DIAG] checkpoint: end ShadowObjects+lights registration (apply tail).
|
||||||
|
if (_frameDiag) _applyShadowAccumTicks += System.Diagnostics.Stopwatch.GetTimestamp() - fdCheck;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnUpdate(double dt)
|
private void OnUpdate(double dt)
|
||||||
{
|
{
|
||||||
|
// [FRAME-DIAG]: applies run later this frame inside _streamingController.Tick;
|
||||||
|
// flush the PREVIOUS OnUpdate's accumulated apply cost here (a clean per-frame
|
||||||
|
// boundary, independent of where in OnUpdate the applies landed) and reset.
|
||||||
|
if (_frameDiag)
|
||||||
|
{
|
||||||
|
if (_appliesThisUpdate > 0)
|
||||||
|
{
|
||||||
|
FrameDiagPush(_applyCpuSamples, ref _applyCpuSampleCursor, _applyAccumTicks);
|
||||||
|
FrameDiagPush(_applyUploadSamples, ref _applyUploadSampleCursor, _applyUploadAccumTicks);
|
||||||
|
FrameDiagPush(_applyCellSamples, ref _applyCellSampleCursor, _applyCellAccumTicks);
|
||||||
|
FrameDiagPush(_applyBspSamples, ref _applyBspSampleCursor, _applyBspAccumTicks);
|
||||||
|
FrameDiagPush(_applyShadowSamples, ref _applyShadowSampleCursor, _applyShadowAccumTicks);
|
||||||
|
FrameDiagPush(_applyLockWaitSamples, ref _applyLockWaitSampleCursor, _applyLockWaitAccumTicks);
|
||||||
|
if (_appliesThisUpdate > _frameDiagMaxAppliesPerUpdate)
|
||||||
|
_frameDiagMaxAppliesPerUpdate = _appliesThisUpdate;
|
||||||
|
}
|
||||||
|
_applyAccumTicks = 0;
|
||||||
|
_applyUploadAccumTicks = 0;
|
||||||
|
_applyCellAccumTicks = 0;
|
||||||
|
_applyBspAccumTicks = 0;
|
||||||
|
_applyShadowAccumTicks = 0;
|
||||||
|
_applyLockWaitAccumTicks = 0;
|
||||||
|
_appliesThisUpdate = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Phase A.1: advance the streaming controller FIRST so the initial
|
// Phase A.1: advance the streaming controller FIRST so the initial
|
||||||
// landblocks are loaded into GpuWorldState before live-session
|
// landblocks are loaded into GpuWorldState before live-session
|
||||||
// CreateObject events drain. The earlier order (live tick first,
|
// CreateObject events drain. The earlier order (live tick first,
|
||||||
|
|
@ -8224,7 +8399,13 @@ public sealed class GameWindow : IDisposable
|
||||||
// GL thread queue). Must happen before any draw work so that
|
// GL thread queue). Must happen before any draw work so that
|
||||||
// resources uploaded this frame are available immediately.
|
// resources uploaded this frame are available immediately.
|
||||||
// No-op when ACDREAM_USE_WB_FOUNDATION is off (_wbMeshAdapter is null).
|
// No-op when ACDREAM_USE_WB_FOUNDATION is off (_wbMeshAdapter is null).
|
||||||
|
// [FRAME-DIAG]: this OnRender drain is where staged GfxObj/entity meshes hit
|
||||||
|
// the GPU (uncapped) — the H2 entity-upload suspect, separate from the apply.
|
||||||
|
long fdE0 = _frameDiag ? System.Diagnostics.Stopwatch.GetTimestamp() : 0L;
|
||||||
_wbMeshAdapter?.Tick();
|
_wbMeshAdapter?.Tick();
|
||||||
|
if (_frameDiag)
|
||||||
|
FrameDiagPush(_entityUploadSamples, ref _entityUploadSampleCursor,
|
||||||
|
System.Diagnostics.Stopwatch.GetTimestamp() - fdE0);
|
||||||
|
|
||||||
// Phase D.2a — begin ImGui frame. Paired with the Render() call
|
// Phase D.2a — begin ImGui frame. Paired with the Render() call
|
||||||
// after the scene draws (below). ImGuiController.Update()
|
// after the scene draws (below). ImGuiController.Update()
|
||||||
|
|
@ -13136,6 +13317,45 @@ public sealed class GameWindow : IDisposable
|
||||||
$"visible={_terrain?.VisibleSlots ?? 0} " +
|
$"visible={_terrain?.VisibleSlots ?? 0} " +
|
||||||
$"loaded={_terrain?.LoadedSlots ?? 0} " +
|
$"loaded={_terrain?.LoadedSlots ?? 0} " +
|
||||||
$"capacity={_terrain?.CapacitySlots ?? 0}");
|
$"capacity={_terrain?.CapacitySlots ?? 0}");
|
||||||
|
|
||||||
|
// [FRAME-DIAG]: the FPS-deep-dive per-frame cost split + leak/churn counters.
|
||||||
|
// apply_us = terrain-apply CPU in OnUpdate (dat reads + physics/ShadowObjects
|
||||||
|
// registration + terrain upload) — INVISIBLE to the title-bar ms (H1).
|
||||||
|
// upl_us = the terrain glBufferSubData sub-span of that apply (expected tiny).
|
||||||
|
// entUpl_us= the OnRender _wbMeshAdapter.Tick entity-mesh GPU drain (H2).
|
||||||
|
// For entity-DRAW CPU/GPU see the [WB-DIAG] line; walked = resident-N proxy (H3).
|
||||||
|
double applyMedUs = TerrainDiagMedianMicros(_applyCpuSamples) / 100.0;
|
||||||
|
double applyP95Us = TerrainDiagPercentile95Micros(_applyCpuSamples) / 100.0;
|
||||||
|
double uplMedUs = TerrainDiagMedianMicros(_applyUploadSamples) / 100.0;
|
||||||
|
double uplP95Us = TerrainDiagPercentile95Micros(_applyUploadSamples) / 100.0;
|
||||||
|
double entUplMedUs = TerrainDiagMedianMicros(_entityUploadSamples) / 100.0;
|
||||||
|
double entUplP95Us = TerrainDiagPercentile95Micros(_entityUploadSamples) / 100.0;
|
||||||
|
// apply CPU split: cell-build / gfxobj-BSP / ShadowObjects+lights — names
|
||||||
|
// WHICH part of the apply dominates (decides the fix shape).
|
||||||
|
double cellMedUs = TerrainDiagMedianMicros(_applyCellSamples) / 100.0;
|
||||||
|
double cellP95Us = TerrainDiagPercentile95Micros(_applyCellSamples) / 100.0;
|
||||||
|
double bspMedUs = TerrainDiagMedianMicros(_applyBspSamples) / 100.0;
|
||||||
|
double bspP95Us = TerrainDiagPercentile95Micros(_applyBspSamples) / 100.0;
|
||||||
|
double shadMedUs = TerrainDiagMedianMicros(_applyShadowSamples) / 100.0;
|
||||||
|
double shadP95Us = TerrainDiagPercentile95Micros(_applyShadowSamples) / 100.0;
|
||||||
|
double lockMedUs = TerrainDiagMedianMicros(_applyLockWaitSamples) / 100.0;
|
||||||
|
double lockP95Us = TerrainDiagPercentile95Micros(_applyLockWaitSamples) / 100.0;
|
||||||
|
int walked = _wbDrawDispatcher?.LastDrawStats.EntitiesWalked ?? 0;
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[FRAME-DIAG] apply_us={applyMedUs:F1}m/{applyP95Us:F1}p95 " +
|
||||||
|
$"lockwait={lockMedUs:F1}m/{lockP95Us:F1}p95 " +
|
||||||
|
$"[cell={cellMedUs:F1}m/{cellP95Us:F1}p95 bsp={bspMedUs:F1}m/{bspP95Us:F1}p95 " +
|
||||||
|
$"shadow={shadMedUs:F1}m/{shadP95Us:F1}p95] " +
|
||||||
|
$"(upl={uplMedUs:F1}m/{uplP95Us:F1}p95) " +
|
||||||
|
$"entUpl_us={entUplMedUs:F1}m/{entUplP95Us:F1}p95 " +
|
||||||
|
$"applies_max/upd={_frameDiagMaxAppliesPerUpdate} " +
|
||||||
|
$"deferred={_streamingController?.DeferredApplyBacklog ?? 0} " +
|
||||||
|
$"forceReload={_streamingController?.ForceReloadCount ?? 0}" +
|
||||||
|
$"(drop={_streamingController?.LastForceReloadDropCount ?? 0}) " +
|
||||||
|
$"esg={_entitiesByServerGuid.Count} spawn={_lastSpawnByGuid.Count} " +
|
||||||
|
$"resident={_worldState.Entities.Count} walked={walked}");
|
||||||
|
_frameDiagMaxAppliesPerUpdate = 0; // reset the per-window burst tracker
|
||||||
|
|
||||||
_terrainLastDiagTick = now;
|
_terrainLastDiagTick = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -13167,6 +13387,17 @@ public sealed class GameWindow : IDisposable
|
||||||
return copy[copy.Length - 1 - offset];
|
return copy[copy.Length - 1 - offset];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>[FRAME-DIAG] helper: convert a <see cref="System.Diagnostics.Stopwatch"/>
|
||||||
|
/// timestamp-tick delta to the microseconds×100 fixed-point used by the rolling
|
||||||
|
/// sample rings and store it (zeros are ignored by the median/p95 helpers, so an
|
||||||
|
/// idle frame's 0-sample doesn't dilute the non-zero cost distribution).</summary>
|
||||||
|
private static void FrameDiagPush(long[] samples, ref int cursor, long ticks)
|
||||||
|
{
|
||||||
|
// µs×100 = ticks × 1e8 / Stopwatch.Frequency.
|
||||||
|
samples[cursor] = (long)(ticks * 100_000_000.0 / System.Diagnostics.Stopwatch.Frequency);
|
||||||
|
cursor = (cursor + 1) % samples.Length;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>A.5 T22: parse a float environment variable, returning
|
/// <summary>A.5 T22: parse a float environment variable, returning
|
||||||
/// <paramref name="defaultValue"/> when the variable is absent or unparseable.</summary>
|
/// <paramref name="defaultValue"/> when the variable is absent or unparseable.</summary>
|
||||||
private static float ParseEnvFloat(string name, float defaultValue)
|
private static float ParseEnvFloat(string name, float defaultValue)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue