From 133c22ed2f2e76a60c0132d6cd78526a69ab698d Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 11 Apr 2026 23:23:11 +0200 Subject: [PATCH] =?UTF-8?q?fix(app):=20Phase=20A.1=20=E2=80=94=20restore?= =?UTF-8?q?=20MaxCompletionsPerFrame=3D4=20(uncap=20caused=20OOM)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fix in f792931 set MaxCompletionsPerFrame to int.MaxValue on the theory that synchronous loading made the cap pointless. That ignored the GPU upload cost: applying 25 landblocks in one Tick allocates ~25 terrain VBOs + hundreds of entity GfxObj sub-mesh VBOs + all unique texture uploads in a single frame, which observably crashes with OutOfMemoryException on the first frame after login. The pending-spawn list (also added in f792931) is what actually fixes the spawn-drop bug — it makes the cap safe by parking late-arriving spawns until their landblock loads. With both fixes: - Cap=4 spreads the 25-landblock first-frame load over ~7 frames (~116ms at 60fps, below human perception) - Spawns for the 21 not-yet-loaded landblocks land in pending and back-fill as each one arrives over the next 6 frames - No data lost, no OOM 219 tests still green. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Streaming/StreamingController.cs | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/AcDream.App/Streaming/StreamingController.cs b/src/AcDream.App/Streaming/StreamingController.cs index 35362a6..6bdd957 100644 --- a/src/AcDream.App/Streaming/StreamingController.cs +++ b/src/AcDream.App/Streaming/StreamingController.cs @@ -26,26 +26,22 @@ public sealed class StreamingController public int Radius { get; set; } /// - /// Cap on completions drained per call. Defaults to - /// effectively unlimited because the current LandblockStreamer - /// is synchronous — every EnqueueLoad writes to the outbox on - /// the same thread, so by the time we drain there's no backlog - /// to spread, and the cap only serves to *delay* applying landblocks - /// the user is already trying to look at. + /// Cap on completions drained per call. The cap is + /// the GPU upload budget for one frame: terrain mesh + per-entity GfxObj + /// sub-mesh uploads + texture uploads for one landblock take a few ms; + /// applying 25 of them in a single frame produces a memory spike + /// (observed: out-of-memory crash on the 5×5 first-frame load). /// /// - /// The original async design used a small cap (4) to limit per-frame - /// GPU upload spikes. That reasoning becomes relevant again if/when - /// the streamer moves back to async loading; lower this knob then. - /// Crucially, dropping completions to a lower frame is what was - /// silently breaking live spawns: the post-login spawn flood would - /// arrive on a frame where only 4 of the 25 visible-window landblocks - /// had been applied, the spawns for the other 21 hit - /// AppendLiveEntity with no matching loaded slot, and got - /// dropped (now: parked in the pending bucket). + /// 4 is the original async-streamer value; it spreads a 5×5 first-frame + /// load over ~7 frames (~116ms at 60fps), which is below the human + /// perception threshold. Spawn races that previously dropped entities + /// while landblocks were in flight are now handled by + /// 's pending-spawn list, so spreading + /// completions doesn't lose any data. /// /// - public int MaxCompletionsPerFrame { get; set; } = int.MaxValue; + public int MaxCompletionsPerFrame { get; set; } = 4; public StreamingController( Action enqueueLoad,