feat(teleport A): StreamingController priority-apply for the teleport destination LB
Adds PriorityLandblockId (uint, default 0) + _deferredApply buffer. DrainAndApply now: (1) applies up to budget from the deferred buffer, (2) when PriorityLandblockId != 0, hunts the worker outbox in chunks applying the priority LB immediately on match and buffering any non-priority items drained past it for later frames, (3) falls back to normal drain when no priority is set or not found. Extracts ApplyResult(result) + ResultLandblockId(result) helpers so both the priority and normal paths share identical side-effects. No existing behaviour changes on the non-priority path. 21 streaming tests pass (19 existing + 2 new priority-apply tests). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b869128df3
commit
1f6baa6cfc
2 changed files with 228 additions and 28 deletions
|
|
@ -89,6 +89,28 @@ public sealed class StreamingController
|
|||
/// </summary>
|
||||
public int MaxCompletionsPerFrame { get; set; } = 4;
|
||||
|
||||
/// <summary>
|
||||
/// #138: the teleport destination landblock id. When non-zero,
|
||||
/// <see cref="DrainAndApply"/> applies this landblock's
|
||||
/// <see cref="LandblockStreamResult.Loaded"/> or
|
||||
/// <see cref="LandblockStreamResult.Promoted"/> completion immediately
|
||||
/// — even if it sits past the <see cref="MaxCompletionsPerFrame"/>
|
||||
/// position in the outbox — so the player can materialise at the
|
||||
/// destination without waiting for every earlier-queued landblock to
|
||||
/// drain first. Non-priority completions drained past it are buffered
|
||||
/// in <see cref="_deferredApply"/> and applied over subsequent frames,
|
||||
/// so no completions are lost and there is no GPU spike.
|
||||
///
|
||||
/// <para>Set by <c>GameWindow</c> when a teleport starts; reset to 0
|
||||
/// once the destination landblock has been applied (or when the
|
||||
/// teleport destination changes).</para>
|
||||
/// </summary>
|
||||
public uint PriorityLandblockId { get; set; }
|
||||
|
||||
// Completions that were drained past a priority item get buffered here
|
||||
// so they still apply over subsequent frames without loss.
|
||||
private readonly List<LandblockStreamResult> _deferredApply = new();
|
||||
|
||||
public StreamingController(
|
||||
Action<uint, LandblockStreamJobKind> enqueueLoad,
|
||||
Action<uint> enqueueUnload,
|
||||
|
|
@ -299,40 +321,133 @@ public sealed class StreamingController
|
|||
/// <summary>
|
||||
/// Drain up to N completions per frame so a big diff doesn't spike GPU
|
||||
/// upload time. Remaining completions wait for the next frame.
|
||||
///
|
||||
/// <para>
|
||||
/// When <see cref="PriorityLandblockId"/> is set (non-zero), the priority
|
||||
/// landblock is applied immediately even if it sits past position N in the
|
||||
/// outbox. Non-priority completions drained past it are buffered in
|
||||
/// <see cref="_deferredApply"/> and applied over subsequent frames
|
||||
/// (no loss, no GPU spike).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private void DrainAndApply()
|
||||
{
|
||||
var drained = _drainCompletions(MaxCompletionsPerFrame);
|
||||
foreach (var result in drained)
|
||||
int budget = MaxCompletionsPerFrame;
|
||||
|
||||
// --- Step 1: drain the deferred buffer first (items held from a prior
|
||||
// priority-hunt that overshot the cap). Apply up to `budget` of them.
|
||||
int deferredApplied = 0;
|
||||
while (deferredApplied < budget && _deferredApply.Count > 0)
|
||||
{
|
||||
switch (result)
|
||||
var item = _deferredApply[0];
|
||||
_deferredApply.RemoveAt(0);
|
||||
ApplyResult(item);
|
||||
deferredApplied++;
|
||||
}
|
||||
budget -= deferredApplied;
|
||||
|
||||
if (budget <= 0) return;
|
||||
|
||||
// --- Step 2: priority hunt (only when a destination is set).
|
||||
if (PriorityLandblockId != 0u)
|
||||
{
|
||||
// Drain in chunks of MaxCompletionsPerFrame (bounded at 64 total
|
||||
// iterations to avoid an unbounded loop if the outbox is huge).
|
||||
const int MaxHuntIterations = 64;
|
||||
int hunted = 0;
|
||||
bool found = false;
|
||||
|
||||
while (hunted < MaxHuntIterations && !found)
|
||||
{
|
||||
case LandblockStreamResult.Loaded loaded:
|
||||
_applyTerrain(loaded.Landblock, loaded.MeshData);
|
||||
_state.AddLandblock(loaded.Landblock);
|
||||
// #138: after the landblock is in _loaded (so AppendLiveEntity
|
||||
// hot-paths), restore any retained server objects ACE won't
|
||||
// re-send. Fired AFTER AddLandblock, never before.
|
||||
_onLandblockLoaded?.Invoke(loaded.Landblock.LandblockId);
|
||||
break;
|
||||
case LandblockStreamResult.Promoted promoted:
|
||||
_applyTerrain(promoted.Landblock, promoted.MeshData);
|
||||
_state.AddEntitiesToExistingLandblock(promoted.LandblockId, promoted.Entities);
|
||||
_onLandblockLoaded?.Invoke(promoted.LandblockId);
|
||||
break;
|
||||
case LandblockStreamResult.Unloaded unloaded:
|
||||
_state.RemoveLandblock(unloaded.LandblockId);
|
||||
_removeTerrain?.Invoke(unloaded.LandblockId);
|
||||
break;
|
||||
case LandblockStreamResult.Failed failed:
|
||||
Console.WriteLine(
|
||||
$"streaming: load failed for 0x{failed.LandblockId:X8}: {failed.Error}");
|
||||
break;
|
||||
case LandblockStreamResult.WorkerCrashed crashed:
|
||||
Console.WriteLine(
|
||||
$"streaming: worker CRASHED: {crashed.Error}");
|
||||
break;
|
||||
var chunk = _drainCompletions(MaxCompletionsPerFrame);
|
||||
if (chunk.Count == 0) break;
|
||||
|
||||
foreach (var result in chunk)
|
||||
{
|
||||
hunted++;
|
||||
if (ResultLandblockId(result) == PriorityLandblockId)
|
||||
{
|
||||
ApplyResult(result);
|
||||
budget--; // priority counts against the budget
|
||||
found = true;
|
||||
// Remaining items in this chunk go to deferred — they
|
||||
// were drained past the cap to find the priority; don't
|
||||
// drop them.
|
||||
}
|
||||
else
|
||||
{
|
||||
_deferredApply.Add(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found)
|
||||
{
|
||||
// Now apply up to remaining budget from deferred (mix from the
|
||||
// chunk we just buffered + any pre-existing deferred items).
|
||||
while (budget > 0 && _deferredApply.Count > 0)
|
||||
{
|
||||
var item = _deferredApply[0];
|
||||
_deferredApply.RemoveAt(0);
|
||||
ApplyResult(item);
|
||||
budget--;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Priority not found in the outbox this frame: fall through to
|
||||
// normal drain (the deferred buffer was already partially applied
|
||||
// above; budget was already decremented).
|
||||
}
|
||||
|
||||
// --- Step 3: normal path — drain up to remaining budget from the
|
||||
// worker's outbox (priority was 0, or not found this frame).
|
||||
var drained = _drainCompletions(budget);
|
||||
foreach (var result in drained)
|
||||
ApplyResult(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a single <see cref="LandblockStreamResult"/> with the full side-
|
||||
/// effects: terrain upload, GPU state, and the re-hydration callback.
|
||||
/// Extracted from the inline switch in the original <c>DrainAndApply</c>
|
||||
/// so both the priority-hunt path and the normal drain path share it.
|
||||
/// </summary>
|
||||
private void ApplyResult(LandblockStreamResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case LandblockStreamResult.Loaded loaded:
|
||||
_applyTerrain(loaded.Landblock, loaded.MeshData);
|
||||
_state.AddLandblock(loaded.Landblock);
|
||||
// #138: after the landblock is in _loaded (so AppendLiveEntity
|
||||
// hot-paths), restore any retained server objects ACE won't
|
||||
// re-send. Fired AFTER AddLandblock, never before.
|
||||
_onLandblockLoaded?.Invoke(loaded.Landblock.LandblockId);
|
||||
break;
|
||||
case LandblockStreamResult.Promoted promoted:
|
||||
_applyTerrain(promoted.Landblock, promoted.MeshData);
|
||||
_state.AddEntitiesToExistingLandblock(promoted.LandblockId, promoted.Entities);
|
||||
_onLandblockLoaded?.Invoke(promoted.LandblockId);
|
||||
break;
|
||||
case LandblockStreamResult.Unloaded unloaded:
|
||||
_state.RemoveLandblock(unloaded.LandblockId);
|
||||
_removeTerrain?.Invoke(unloaded.LandblockId);
|
||||
break;
|
||||
case LandblockStreamResult.Failed failed:
|
||||
Console.WriteLine(
|
||||
$"streaming: load failed for 0x{failed.LandblockId:X8}: {failed.Error}");
|
||||
break;
|
||||
case LandblockStreamResult.WorkerCrashed crashed:
|
||||
Console.WriteLine(
|
||||
$"streaming: worker CRASHED: {crashed.Error}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the landblock id associated with <paramref name="result"/>.
|
||||
/// For <see cref="LandblockStreamResult.WorkerCrashed"/> this is 0 by
|
||||
/// convention (not tied to a specific landblock).
|
||||
/// </summary>
|
||||
private static uint ResultLandblockId(LandblockStreamResult result) => result.LandblockId;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue