From 58c8de264ee838ce2c776677520e4d206dfaf874 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 8 Aug 2026 10:29:39 +0200 Subject: [PATCH] =?UTF-8?q?fix(render):=20#350=20=E2=80=94=20the=20shadow?= =?UTF-8?q?=20ledger's=20lifetime=20counters=20are=2064-bit;=20the=202h42m?= =?UTF-8?q?=20overflow=20is=20closed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RenderDeltaApplyResult's nine counters accumulate for the lifetime of a world generation (the cumulative sum is never reset in place) yet were int where every sibling lifetime counter in the same class was already ulong/long. A 2h42m single-generation session (login to crash with zero portals) overflowed one through ordinary per-tick churn from SynchronizeActiveSources' two call sites per frame. Introduced 0eb66485 (2026-07-24); first reached by the vendor buy gate because parking at a shop for hours produced the project's first multi-hour unbroken generation. The vendor materializer was exonerated by routing analysis (shop-item Ingest/Remove reaches inventory deltas only, never the render journal). Widened to long; the per-tick builder stays int and widens implicitly; the arithmetic stays checked. All 68 render-shadow tests green. Co-Authored-By: Claude Opus 5 --- docs/ISSUES.md | 26 +++++++++++++++++++ .../Rendering/Scene/RenderSceneContracts.cs | 25 +++++++++++------- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/docs/ISSUES.md b/docs/ISSUES.md index e76754b5..ab510ba1 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -24,6 +24,32 @@ What does NOT go here: - Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending. - Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed. +## #350 — Render-shadow ledger overflow after 2h42m: lifetime int counters in a never-reset accumulator + +**Status:** FIXED IN TREE 2026-08-08 (pending clean-room + landing). +**Evidence:** `vendor-buy-gate.log` ~1606 — checked OverflowException in +`RenderSceneShadowRuntime.Add` from `UpdateFrameOrchestrator.Tick`, exit 82, +2h42m into a SINGLE world generation (login 20:28 -> crash 23:10, one +`[world-reveal] generation=1`, no portal). + +**Mechanism (investigated, vendor coupling REFUTED):** +`_cumulativeApply` sums nine int fields once per tick for the lifetime of a +world generation and is never reset in place (production has zero Clear() +call sites; only generation replacement constructs a fresh runtime). +`SynchronizeActiveSources` feeds per-entity churn from TWO call sites per +frame; at uncapped frame rates a long session's ordinary churn crosses +int.MaxValue. Introduced `0eb66485` (2026-07-24) with the class's OTHER +lifetime counters already ulong/long — only these nine were undersized. +The vendor materializer was exonerated by routing analysis: shop-item +Ingest/Remove reaches inventory deltas only, never the render journal; +the correlation was that buy-gate testing produced the first multi-hour +single-generation soak. + +**Fix:** widen `RenderDeltaApplyResult`'s nine fields to long (the +per-tick builder stays int and widens implicitly); arithmetic stays +checked. No clamp, no reset-behavior change — the counters are +legitimately unbounded lifetime telemetry that was simply undersized. + ## #348 — Render-loop death by Win32 cursor-handle exhaustion: Silk recreates the native cursor on every alternation **Status:** FIX IN TREE (2026-08-08) pending the vendor-gate relaunch. diff --git a/src/AcDream.App/Rendering/Scene/RenderSceneContracts.cs b/src/AcDream.App/Rendering/Scene/RenderSceneContracts.cs index 057d1fe7..e3450ce2 100644 --- a/src/AcDream.App/Rendering/Scene/RenderSceneContracts.cs +++ b/src/AcDream.App/Rendering/Scene/RenderSceneContracts.cs @@ -401,18 +401,23 @@ internal readonly record struct RenderSceneIndexCounts( }; } +// #350: these counters accumulate for the LIFETIME of a world generation +// (RenderSceneShadowRuntime._cumulativeApply is never reset in place), so +// they are 64-bit like the class's other lifetime counters — a 2h42m +// single-generation session overflowed the original int through ordinary +// per-tick churn. The arithmetic stays checked. internal readonly record struct RenderDeltaApplyResult( - int Applied, - int Registered, - int Updated, - int Replaced, - int Unregistered, - int RejectedGeneration, - int RejectedOutOfOrderSequence, - int RejectedStaleIncarnation, - int RejectedMissing) + long Applied, + long Registered, + long Updated, + long Replaced, + long Unregistered, + long RejectedGeneration, + long RejectedOutOfOrderSequence, + long RejectedStaleIncarnation, + long RejectedMissing) { - public int Rejected => + public long Rejected => RejectedGeneration + RejectedOutOfOrderSequence + RejectedStaleIncarnation