refactor(streaming): complete landblock presentation cutover
Remove the legacy GameWindow apply path and make the concrete render, physics, and static publishers the only production owner graph. Serialize full-window retirement with shared-origin teleport and session boundaries so old coordinate-frame resources cannot survive into a new world or login.
This commit is contained in:
parent
801d8a189c
commit
c79d0a49da
12 changed files with 1373 additions and 402 deletions
|
|
@ -291,8 +291,8 @@ public sealed class CellVisibility
|
|||
|
||||
/// <summary>
|
||||
/// Phase A8 (2026-05-28): enumerates the loaded cells that belong to a
|
||||
/// landblock prefix. Used by <c>GameWindow.ApplyLoadedTerrainLocked</c> when
|
||||
/// building the per-landblock <c>BuildingRegistry</c> — the per-frame
|
||||
/// landblock prefix. Used by <c>LandblockRenderPublisher</c> when building
|
||||
/// the per-landblock <c>BuildingRegistry</c> — the per-frame
|
||||
/// <c>drainedCells</c> dict misses cells loaded on prior frames, so the
|
||||
/// stamping loop in <see cref="Wb.BuildingLoader.Build"/> needs access to
|
||||
/// every cell currently in the landblock to ensure <c>BuildingId</c> is set.
|
||||
|
|
|
|||
|
|
@ -93,46 +93,15 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
private int _terrainCpuSampleCursor;
|
||||
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.
|
||||
// [FRAME-DIAG]: retain only the render-thread entity upload distribution.
|
||||
// Landblock publication timing/counts live with the focused publishers and
|
||||
// are read from their typed snapshots when diagnostics flush.
|
||||
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;
|
||||
System.Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG"),
|
||||
"1",
|
||||
System.StringComparison.Ordinal);
|
||||
private readonly long[] _entityUploadSamples = new long[256];
|
||||
private int _entityUploadSampleCursor;
|
||||
|
||||
// MP0 (2026-07-05): permanent frame profiler — one FrameBoundary call
|
||||
// per OnRender + three stage scopes. All logic lives in
|
||||
|
|
@ -150,10 +119,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
// Phase A.1: streaming fields replacing the one-shot _entities list.
|
||||
private AcDream.App.Streaming.LandblockStreamer? _streamer;
|
||||
private AcDream.App.Streaming.GpuWorldState _worldState = new();
|
||||
private AcDream.App.Streaming.LandblockRenderPublisher? _landblockRenderPublisher;
|
||||
private AcDream.App.Streaming.LandblockPhysicsPublisher? _landblockPhysicsPublisher;
|
||||
private AcDream.App.Streaming.LandblockStaticPresentationPublisher?
|
||||
_landblockStaticPresentationPublisher;
|
||||
private AcDream.App.Streaming.LandblockPresentationPipeline?
|
||||
_landblockPresentationPipeline;
|
||||
private AcDream.App.Rendering.EquippedChildRenderController? _equippedChildRenderer;
|
||||
private AcDream.App.Streaming.StreamingController? _streamingController;
|
||||
private AcDream.App.Streaming.WorldRevealCoordinator? _worldReveal;
|
||||
|
|
@ -1630,9 +1597,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
int centerX = (int)((centerLandblockId >> 24) & 0xFFu);
|
||||
int centerY = (int)((centerLandblockId >> 16) & 0xFFu);
|
||||
|
||||
// Build blending context from the terrain atlas. Stored as fields so
|
||||
// ApplyLoadedTerrain (render-thread callback invoked per streamed lb)
|
||||
// can call LandblockMesh.Build without re-deriving these every time.
|
||||
// Build blending context from the terrain atlas. The worker-side
|
||||
// LandblockBuildFactory captures this immutable table for each build.
|
||||
var terrainTypeToLayerBytes = new Dictionary<uint, byte>(terrainAtlas.TerrainTypeToLayer.Count);
|
||||
foreach (var kvp in terrainAtlas.TerrainTypeToLayer)
|
||||
terrainTypeToLayerBytes[kvp.Key] = (byte)kvp.Value;
|
||||
|
|
@ -2410,7 +2376,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
_envCellRenderer = new AcDream.App.Rendering.Wb.EnvCellRenderer(
|
||||
_gl, _wbMeshAdapter!.MeshManager!, _envCellFrustum);
|
||||
_envCellRenderer.Initialize(_meshShader!);
|
||||
_landblockRenderPublisher = new AcDream.App.Streaming.LandblockRenderPublisher(
|
||||
var landblockRenderPublisher = new AcDream.App.Streaming.LandblockRenderPublisher(
|
||||
publishTerrain: (landblockId, meshData, origin) =>
|
||||
_terrain!.AddLandblockWithMesh(landblockId, meshData, origin),
|
||||
removeTerrain: landblockId => _terrain!.RemoveLandblock(landblockId),
|
||||
|
|
@ -2422,16 +2388,34 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
build,
|
||||
_wbMeshAdapter.MeshManager!),
|
||||
removeEnvCells: _envCellRenderer.RemoveLandblock);
|
||||
_landblockPhysicsPublisher =
|
||||
var landblockPhysicsPublisher =
|
||||
new AcDream.App.Streaming.LandblockPhysicsPublisher(
|
||||
_physicsEngine,
|
||||
_heightTable!);
|
||||
_landblockStaticPresentationPublisher =
|
||||
var landblockStaticPresentationPublisher =
|
||||
new AcDream.App.Streaming.LandblockStaticPresentationPublisher(
|
||||
_lightingSink!,
|
||||
_translucencyFades,
|
||||
_worldGameState,
|
||||
_worldEvents);
|
||||
var landblockRetirementOwner =
|
||||
new AcDream.App.Streaming.LandblockPresentationRetirementOwner(
|
||||
landblockRenderPublisher,
|
||||
landblockPhysicsPublisher,
|
||||
landblockStaticPresentationPublisher,
|
||||
_lightingSink!,
|
||||
_translucencyFades);
|
||||
_landblockPresentationPipeline =
|
||||
new AcDream.App.Streaming.LandblockPresentationPipeline(
|
||||
landblockRenderPublisher,
|
||||
landblockPhysicsPublisher,
|
||||
landblockStaticPresentationPublisher,
|
||||
_worldState,
|
||||
landblockRetirementOwner,
|
||||
onLandblockLoaded: loadedLandblockId =>
|
||||
_liveEntityHydration!.OnLandblockLoaded(loadedLandblockId),
|
||||
ensureEnvCellMeshes:
|
||||
landblockRenderPublisher.PrepareAfterRenderPins);
|
||||
|
||||
_clipFrame ??= ClipFrame.NoClip();
|
||||
_retailPViewRenderer = new AcDream.App.Rendering.RetailPViewRenderer(
|
||||
|
|
@ -2523,24 +2507,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
});
|
||||
_streamer.Start();
|
||||
|
||||
var landblockRetirementOwner =
|
||||
new AcDream.App.Streaming.LandblockPresentationRetirementOwner(
|
||||
_landblockRenderPublisher!,
|
||||
_landblockPhysicsPublisher!,
|
||||
_landblockStaticPresentationPublisher!,
|
||||
_lightingSink!,
|
||||
_translucencyFades);
|
||||
var landblockPresentationPipeline =
|
||||
new AcDream.App.Streaming.LandblockPresentationPipeline(
|
||||
_landblockRenderPublisher!,
|
||||
_landblockPhysicsPublisher!,
|
||||
_landblockStaticPresentationPublisher!,
|
||||
_worldState,
|
||||
landblockRetirementOwner,
|
||||
onLandblockLoaded: loadedLandblockId =>
|
||||
_liveEntityHydration!.OnLandblockLoaded(loadedLandblockId),
|
||||
ensureEnvCellMeshes:
|
||||
_landblockRenderPublisher!.PrepareAfterRenderPins);
|
||||
_streamingController = new AcDream.App.Streaming.StreamingController(
|
||||
enqueueLoad: (id, kind, generation) => _streamer.EnqueueLoad(
|
||||
new AcDream.App.Streaming.LandblockBuildRequest(
|
||||
|
|
@ -2559,7 +2525,11 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
// Retained-object recovery runs after each
|
||||
// (dungeon-exit expand or Far→Near promote). ACE won't re-send the
|
||||
// objects it still considers known.
|
||||
presentationPipeline: landblockPresentationPipeline);
|
||||
presentationPipeline: _landblockPresentationPipeline!);
|
||||
_streamingOriginRecenter =
|
||||
new AcDream.App.Streaming.StreamingOriginRecenterCoordinator(
|
||||
_streamingController,
|
||||
_liveWorldOrigin);
|
||||
// A.5 T22.5: apply max-completions from resolved quality.
|
||||
_streamingController.MaxCompletionsPerFrame = _resolvedQuality.MaxCompletionsPerFrame;
|
||||
_worldReveal = new AcDream.App.Streaming.WorldRevealCoordinator(
|
||||
|
|
@ -3297,6 +3267,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
private readonly TeleportViewPlaneController _teleportViewPlane = new();
|
||||
private readonly AcDream.App.Streaming.TeleportTransitCoordinator<
|
||||
AcDream.Core.Net.WorldSession.EntityPositionUpdate> _teleportTransit = new();
|
||||
private AcDream.App.Streaming.StreamingOriginRecenterCoordinator?
|
||||
_streamingOriginRecenter;
|
||||
private System.Numerics.Vector3 _pendingTeleportPos;
|
||||
private uint _pendingTeleportCell;
|
||||
private float _teleportHoldSeconds; // wall-clock seconds waiting for residency (safety-net timeout)
|
||||
|
|
@ -3365,22 +3337,21 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
System.Numerics.Vector3 newWorldPos;
|
||||
if (streamingCenterChanges)
|
||||
{
|
||||
// #145: drop the stale STREAMING center landblock from the physics engine
|
||||
// BEFORE recentering. The destination loads at world-offset (0,0) (the new
|
||||
// center), but the prior center was ALSO loaded at offset (0,0) and its
|
||||
// offset is never re-based — so the two overlap, and the Z-agnostic outdoor
|
||||
// cell-snap resolves the player into the stale center. On a replacement this
|
||||
// can be the abandoned first destination, not the unplaced player's source.
|
||||
_landblockPhysicsPublisher!.RemoveLandblock(streamingOriginLandblockId);
|
||||
|
||||
_liveWorldOrigin.Recenter(lbX, lbY);
|
||||
// #145: retire the complete old streaming window while its shared
|
||||
// origin is still current. Destination streaming remains blocked
|
||||
// until every retained presentation ticket converges. This prevents
|
||||
// old terrain and collision, which were baked in the prior frame,
|
||||
// from overlapping the destination after the origin changes.
|
||||
var recenter = _streamingOriginRecenter
|
||||
?? throw new InvalidOperationException(
|
||||
"A teleport changed streaming center before the recenter coordinator was wired.");
|
||||
recenter.Begin(
|
||||
lbX,
|
||||
lbY,
|
||||
IsSealedDungeonCell(p.LandblockId));
|
||||
newWorldPos = new System.Numerics.Vector3(
|
||||
p.PositionX, p.PositionY, p.PositionZ);
|
||||
|
||||
if (_streamingController is not null && IsSealedDungeonCell(p.LandblockId))
|
||||
_streamingController.PreCollapseToDungeon(lbX, lbY);
|
||||
else
|
||||
_streamingController?.ForceReloadWindow();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -3418,6 +3389,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
/// </summary>
|
||||
private void ResetTeleportTransitState(bool clearSession = false)
|
||||
{
|
||||
bool originRecenterConverged =
|
||||
_streamingOriginRecenter?.Reset(sessionEnding: clearSession) ?? true;
|
||||
if (clearSession)
|
||||
{
|
||||
_teleportTransit.ClearSession();
|
||||
|
|
@ -3440,6 +3413,12 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
_streamingController.PriorityLandblockId = 0u;
|
||||
_streamingController.PriorityRadius = 0;
|
||||
}
|
||||
|
||||
if (clearSession && !originRecenterConverged)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The ending session's streaming-origin retirement has not converged.");
|
||||
}
|
||||
}
|
||||
|
||||
// #145: the LANDBLOCK-relative (cell-local) position used to SEED the player
|
||||
|
|
@ -3801,183 +3780,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
$"live: AdminEnvirons sound cue = {name} " +
|
||||
$"(0x{environChangeType:X2}) — audio binding pending");
|
||||
}
|
||||
/// <summary>
|
||||
/// Phase A.1 / A.5 T12: render-thread callback from StreamingController.Tick
|
||||
/// whenever a new landblock's terrain + entities are ready for GPU upload.
|
||||
/// Phase A.5 T12: the worker pre-builds <paramref name="meshData"/> off the
|
||||
/// render thread via <see cref="AcDream.Core.Terrain.LandblockMesh.Build"/>;
|
||||
/// this callback no longer pays that CPU cost.
|
||||
/// Must only be called from the render thread.
|
||||
/// </summary>
|
||||
private void ApplyLoadedTerrain(AcDream.App.Streaming.LandblockBuild build,
|
||||
AcDream.Core.Terrain.LandblockMeshData meshData)
|
||||
{
|
||||
var lb = build.Landblock;
|
||||
if (_terrain is null || _dats is null) return;
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport("APPLY", lb.LandblockId);
|
||||
|
||||
// datLock fix (2026-06-23): ApplyLoadedTerrainLocked now makes ZERO
|
||||
// DatCollection calls — every dat it needs was pre-read by the streaming
|
||||
// worker into lb.PhysicsDats. Its remaining mutations are update-thread-
|
||||
// only (physics engine, ShadowObjects, renderer, _worldState) or already
|
||||
// concurrent-safe (PhysicsDataCache is ConcurrentDictionary). _datLock's
|
||||
// only cross-thread job was serializing DatCollection, so with no Get call
|
||||
// here the lock is unnecessary — removing it eliminates the measured
|
||||
// 24ms-median / 88ms-p95 lockwait stall (the FPS 30↔200 swing). The method
|
||||
// keeps its historical "Locked" name; the worker still serializes its OWN
|
||||
// dat reads on _datLock — only the apply stops contending for it.
|
||||
// [FRAME-DIAG]: lockwait now measures ~0 — the before/after proof.
|
||||
long fdT0 = _frameDiag ? System.Diagnostics.Stopwatch.GetTimestamp() : 0L;
|
||||
if (_frameDiag)
|
||||
_applyLockWaitAccumTicks += System.Diagnostics.Stopwatch.GetTimestamp() - fdT0;
|
||||
try
|
||||
{
|
||||
ApplyLoadedTerrainLocked(build, meshData);
|
||||
}
|
||||
catch (AcDream.App.Streaming.StreamingMutationException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
// This legacy body spans multiple concrete owners and cannot yet
|
||||
// identify an exact failed suffix. Conservatively report that a
|
||||
// prefix may have committed so the transaction coordinator never
|
||||
// replays terrain/cells/collision/lights/plugin events. Slice-5
|
||||
// checkpoints C-E replace this wrapper with per-owner receipts.
|
||||
throw new AcDream.App.Streaming.StreamingMutationException(
|
||||
$"Landblock 0x{lb.LandblockId:X8} presentation failed after an unknown commit point.",
|
||||
mutationCommitted: true,
|
||||
error);
|
||||
}
|
||||
if (_frameDiag)
|
||||
{
|
||||
_applyAccumTicks += System.Diagnostics.Stopwatch.GetTimestamp() - fdT0;
|
||||
_appliesThisUpdate++;
|
||||
}
|
||||
}
|
||||
|
||||
private void PublishLandblockStaticLightingBeforeCollision(
|
||||
AcDream.Core.World.WorldEntity entity,
|
||||
AcDream.Core.World.PhysicsDatBundle datBundle)
|
||||
{
|
||||
if (_lightingSink is null || _dats is null)
|
||||
return;
|
||||
|
||||
uint sourceId = entity.SourceGfxObjOrSetupId;
|
||||
if ((sourceId & 0xFF000000u) != 0x02000000u)
|
||||
return;
|
||||
if (!datBundle.Setups.TryGetValue(sourceId, out var setup)
|
||||
|| setup.Lights.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// A landblock can reapply without an unload (#168 ForceReloadWindow
|
||||
// and Far-to-Near promotion). Static IDs are deterministic, so replace
|
||||
// the prior owner state before registering the Setup lights.
|
||||
uint effectOwnerId = entity.Id;
|
||||
_lightingSink.UnregisterOwner(effectOwnerId);
|
||||
_translucencyFades.ClearEntity(effectOwnerId);
|
||||
var lights = AcDream.Core.Lighting.LightInfoLoader.Load(
|
||||
setup,
|
||||
ownerId: effectOwnerId,
|
||||
entityPosition: entity.Position,
|
||||
entityRotation: entity.Rotation,
|
||||
cellId: entity.ParentCellId ?? 0u);
|
||||
foreach (var light in lights)
|
||||
_lightingSink.RegisterOwnedLight(light);
|
||||
}
|
||||
|
||||
private void ApplyLoadedTerrainLocked(
|
||||
AcDream.App.Streaming.LandblockBuild build,
|
||||
AcDream.Core.Terrain.LandblockMeshData meshData)
|
||||
{
|
||||
AcDream.Core.World.LoadedLandblock landblock = build.Landblock;
|
||||
if (_landblockRenderPublisher is null || _landblockPhysicsPublisher is null)
|
||||
return;
|
||||
|
||||
AcDream.Core.World.PhysicsDatBundle datBundle =
|
||||
landblock.PhysicsDats ?? AcDream.Core.World.PhysicsDatBundle.Empty;
|
||||
AcDream.App.Streaming.LandblockRenderPublisherDiagnostics renderBefore =
|
||||
_landblockRenderPublisher.Diagnostics;
|
||||
AcDream.App.Streaming.LandblockRenderPublication renderPublication =
|
||||
_landblockRenderPublisher.BeginPublication(build, meshData);
|
||||
System.Numerics.Vector3 origin = renderPublication.Origin;
|
||||
|
||||
AcDream.App.Streaming.LandblockRenderPublisherDiagnostics renderPrefix =
|
||||
_landblockRenderPublisher.Diagnostics;
|
||||
if (_frameDiag)
|
||||
{
|
||||
long terrainTicks =
|
||||
renderPrefix.TerrainPublishTicks - renderBefore.TerrainPublishTicks;
|
||||
_applyUploadAccumTicks += terrainTicks;
|
||||
_applyCellAccumTicks +=
|
||||
renderPrefix.BeginPublishTicks
|
||||
- renderBefore.BeginPublishTicks
|
||||
- terrainTicks;
|
||||
}
|
||||
|
||||
AcDream.App.Streaming.LandblockPhysicsPublisherDiagnostics physicsBefore =
|
||||
_landblockPhysicsPublisher.Diagnostics;
|
||||
AcDream.App.Streaming.LandblockPhysicsPublication physicsPublication =
|
||||
_landblockPhysicsPublisher.BeginPublication(renderPublication);
|
||||
|
||||
// Retail initializes buildings/visible cells before ordinary static
|
||||
// collision and scripts. The two receipts retain one captured origin
|
||||
// while each focused owner commits its exact stage.
|
||||
_landblockRenderPublisher.CompletePublication(renderPublication);
|
||||
_landblockPhysicsPublisher.CompletePublication(
|
||||
physicsPublication,
|
||||
entity => PublishLandblockStaticLightingBeforeCollision(
|
||||
entity,
|
||||
datBundle));
|
||||
|
||||
if (_frameDiag)
|
||||
{
|
||||
AcDream.App.Streaming.LandblockPhysicsPublisherDiagnostics physicsAfter =
|
||||
_landblockPhysicsPublisher.Diagnostics;
|
||||
AcDream.App.Streaming.LandblockRenderPublisherDiagnostics renderAfter =
|
||||
_landblockRenderPublisher.Diagnostics;
|
||||
long gfxCacheTicks =
|
||||
physicsAfter.GfxCacheTicks - physicsBefore.GfxCacheTicks;
|
||||
_applyCellAccumTicks +=
|
||||
physicsAfter.BasePublishTicks - physicsBefore.BasePublishTicks;
|
||||
_applyCellAccumTicks +=
|
||||
renderAfter.CompletePublishTicks
|
||||
- renderPrefix.CompletePublishTicks;
|
||||
_applyBspAccumTicks += gfxCacheTicks;
|
||||
_applyShadowAccumTicks +=
|
||||
physicsAfter.CompletePublishTicks
|
||||
- physicsBefore.CompletePublishTicks
|
||||
- gfxCacheTicks;
|
||||
}
|
||||
|
||||
long pluginStarted =
|
||||
_frameDiag ? System.Diagnostics.Stopwatch.GetTimestamp() : 0L;
|
||||
foreach (AcDream.Core.World.WorldEntity entity in landblock.Entities)
|
||||
{
|
||||
// Live objects publish through LiveEntityRuntime exactly once per
|
||||
// accepted incarnation. This callback owns DAT statics only.
|
||||
if (entity.ServerGuid != 0)
|
||||
continue;
|
||||
|
||||
var snapshot =
|
||||
new AcDream.Plugin.Abstractions.WorldEntitySnapshot(
|
||||
Id: entity.Id,
|
||||
SourceId: entity.SourceGfxObjOrSetupId,
|
||||
Position: entity.Position,
|
||||
Rotation: entity.Rotation);
|
||||
_worldGameState.Add(snapshot);
|
||||
_worldEvents.FireEntitySpawned(snapshot);
|
||||
}
|
||||
if (_frameDiag)
|
||||
{
|
||||
_applyShadowAccumTicks +=
|
||||
System.Diagnostics.Stopwatch.GetTimestamp() - pluginStarted;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUpdate(double dt)
|
||||
{
|
||||
using var _updStage = _frameProfiler.BeginStage(AcDream.App.Diagnostics.FrameStage.Update);
|
||||
|
|
@ -4009,31 +3811,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
_physicsScriptGameTime += frameSeconds;
|
||||
_scriptRunner?.PublishTime(_physicsScriptGameTime);
|
||||
|
||||
// [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
|
||||
// landblocks are loaded into GpuWorldState before live-session
|
||||
// CreateObject events drain. The earlier order (live tick first,
|
||||
|
|
@ -4059,6 +3836,12 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
// The world-geometry RENDER gate (IsLiveModeWaitingForLogin at the
|
||||
// draw site) is unchanged — the pre-entry screen still shows sky
|
||||
// only.
|
||||
// Recenter cancellation/session-reset requests must keep converging
|
||||
// even when ordinary streaming is temporarily gated off. This call is
|
||||
// a no-op outside a pending teleport or session-boundary origin
|
||||
// retirement transaction.
|
||||
_streamingOriginRecenter?.Advance();
|
||||
|
||||
bool liveInWorld = _liveSessionController?.IsInWorld == true;
|
||||
// #192: liveInWorld alone used to be sufficient here — but InWorld fires
|
||||
// right after the login handshake, before the player's own spawn
|
||||
|
|
@ -4078,10 +3861,11 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
&& _playerController.State == AcDream.App.Input.PlayerState.PortalSpace)
|
||||
{
|
||||
// Teleport hold (#135): the local player position is frozen at the
|
||||
// PRE-teleport spot, expressed in the OLD center frame, but
|
||||
// _liveCenterX/_liveCenterY were already recentered onto the
|
||||
// destination landblock (OnLivePositionUpdated). Follow the
|
||||
// destination directly — the stale position-derived offset
|
||||
// PRE-teleport spot, expressed in the OLD center frame. The
|
||||
// origin coordinator commits _liveCenterX/Y to the destination
|
||||
// only after old-window presentation retirement converges; the
|
||||
// streaming controller remains gated until that edge. Follow
|
||||
// the active shared origin directly — the stale position-derived offset
|
||||
// (_liveCenterX + floor(frozenPos/192)) could land ≥2 landblocks off
|
||||
// the dungeon and trip ExitDungeonExpand, re-streaming the very
|
||||
// neighbor window the pre-collapse just suppressed. Correct for an
|
||||
|
|
@ -4266,8 +4050,11 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
if (_teleportTransit.IsActive)
|
||||
{
|
||||
bool haveDest = _pendingTeleportCell != 0u;
|
||||
bool ready = haveDest && TeleportWorldReady(_pendingTeleportCell);
|
||||
if (haveDest && !ready)
|
||||
bool originReady = _streamingOriginRecenter?.IsPending != true;
|
||||
bool ready = haveDest
|
||||
&& originReady
|
||||
&& TeleportWorldReady(_pendingTeleportCell);
|
||||
if (haveDest && originReady && !ready)
|
||||
{
|
||||
_teleportHoldSeconds += frameDelta;
|
||||
if (_teleportHoldSeconds >= TeleportMaxHoldSeconds)
|
||||
|
|
@ -5059,7 +4846,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
// routes interior-root look-ins to its landscape-stage sub-pass
|
||||
// (DrawBuildingLookIns); the root's own building self-excludes
|
||||
// via the seed eye-side test.
|
||||
foreach (var registry in _landblockRenderPublisher?.BuildingRegistries
|
||||
foreach (var registry in _landblockPresentationPipeline?.BuildingRegistries
|
||||
?? Array.Empty<AcDream.App.Rendering.Wb.BuildingRegistry>())
|
||||
{
|
||||
foreach (var b in registry.All())
|
||||
|
|
@ -8712,43 +8499,38 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
$"loaded={_terrain?.LoadedSlots ?? 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;
|
||||
// Publication metrics are cumulative typed snapshots from the focused
|
||||
// owners. The only GameWindow-local distribution left is the render-
|
||||
// thread entity upload drain.
|
||||
AcDream.App.Streaming.LandblockPresentationDiagnostics presentation =
|
||||
_landblockPresentationPipeline?.Diagnostics ?? default;
|
||||
AcDream.App.Streaming.LandblockRenderPublisherDiagnostics render =
|
||||
presentation.Render;
|
||||
AcDream.App.Streaming.LandblockPhysicsPublisherDiagnostics physics =
|
||||
presentation.Physics;
|
||||
AcDream.App.Streaming.LandblockStaticPresentationDiagnostics statics =
|
||||
presentation.Statics;
|
||||
double ticksToMicros =
|
||||
1_000_000.0 / System.Diagnostics.Stopwatch.Frequency;
|
||||
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) " +
|
||||
$"[FRAME-DIAG] publish={render.BeginCount}/{render.CompleteCount} " +
|
||||
$"render_total_us=[terrain={render.TerrainPublishTicks * ticksToMicros:F1} " +
|
||||
$"begin={render.BeginPublishTicks * ticksToMicros:F1} " +
|
||||
$"complete={render.CompletePublishTicks * ticksToMicros:F1}] " +
|
||||
$"physics_total_us=[base={physics.BasePublishTicks * ticksToMicros:F1} " +
|
||||
$"gfx={physics.GfxCacheTicks * ticksToMicros:F1} " +
|
||||
$"complete={physics.CompletePublishTicks * ticksToMicros:F1}] " +
|
||||
$"static={statics.BeginCount}/{statics.CompleteCount}" +
|
||||
$"(active={statics.ActiveEntityCount}) " +
|
||||
$"entUpl_us={entUplMedUs:F1}m/{entUplP95Us:F1}p95 " +
|
||||
$"applies_max/upd={_frameDiagMaxAppliesPerUpdate} " +
|
||||
$"deferred={_streamingController?.DeferredApplyBacklog ?? 0} " +
|
||||
$"forceReload={_streamingController?.ForceReloadCount ?? 0}" +
|
||||
$"(drop={_streamingController?.LastForceReloadDropCount ?? 0}) " +
|
||||
$"fullRetire={_streamingController?.FullWindowRetirementCount ?? 0}" +
|
||||
$"(landblocks={_streamingController?.LastFullWindowRetirementLandblockCount ?? 0}) " +
|
||||
$"esg={_entitiesByServerGuid.Count} spawn={LastSpawns.Count} " +
|
||||
$"resident={_worldState.Entities.Count} walked={walked}");
|
||||
_frameDiagMaxAppliesPerUpdate = 0; // reset the per-window burst tracker
|
||||
|
||||
_terrainLastDiagTick = now;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue