diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index 52514ea..c6b58fd 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -154,7 +154,9 @@ a named invariant that the native runtime cannot protect yet. topology changes do not resize the GPU buffer while the trail fills. `MIMIR_SPECTRUM_SOURCE_LANES` sets the lane capacity; surplus sources are truncated and reported in the runtime UI. Lowerings use that fixed capacity - as `ColumnStride`, and `MIMIR_SPECTRUM_TUBE_SUBDIVISIONS` controls the + as `ColumnStride`. Physical history slots are addressed as a ring with + `RollingOffset`, so logical age no longer requires reshuffling every column + before shader sampling. `MIMIR_SPECTRUM_TUBE_SUBDIVISIONS` controls the requested Catmull-Rom subdivision count so Fensalir's overflow report has a direct cost lever. - `MimirFensalirFieldLowering` now emits `AquariumFieldResourceDeclaration` diff --git a/notes/current-system-map.md b/notes/current-system-map.md index 35d957f..0cec0a5 100644 --- a/notes/current-system-map.md +++ b/notes/current-system-map.md @@ -391,8 +391,10 @@ its own age trail at `z = age * 0.1`. The resource advertises fixed history and source-lane capacities, so content updates and source topology changes do not resize the GPU buffer while the rolling trail fills. `MIMIR_SPECTRUM_SOURCE_LANES` sets the fixed lane budget; surplus sources are truncated and reported in the -runtime UI. `MIMIR_SPECTRUM_TUBE_SUBDIVISIONS` is the runtime cost lever for -lowering Catmull-Rom subdivision count when Fensalir reports TubeField +runtime UI. Physical history slots are now addressed as a ring with +`RollingOffset`, so logical age no longer requires moving every column before +shader sampling. `MIMIR_SPECTRUM_TUBE_SUBDIVISIONS` is the runtime cost lever +for lowering Catmull-Rom subdivision count when Fensalir reports TubeField truncation. ## Known Risks diff --git a/src/Mimir.BufferSmoke/Program.cs b/src/Mimir.BufferSmoke/Program.cs index 0687441..b80f0c4 100644 --- a/src/Mimir.BufferSmoke/Program.cs +++ b/src/Mimir.BufferSmoke/Program.cs @@ -678,6 +678,7 @@ static int RunMimirSpectrumUploadSmoke() const int expectedHistoryCount = 3; const int expectedHistoryCapacity = 40; const int expectedSubdivisions = 2; + const int expectedRollingOffset = (expectedHistoryCapacity - (expectedHistoryCount - 1)) * expectedSourceLaneCapacity; var plannedTubePackets = plan.Packets .Where(packet => packet.Backend == AquariumFieldBackendKind.TubeField && @@ -714,6 +715,7 @@ static int RunMimirSpectrumUploadSmoke() item.ColumnCount == expectedHistoryCount && item.ColumnStride == expectedSourceLaneCapacity && item.RollingModulo == resource.Height && + item.RollingOffset == expectedRollingOffset && item.CatmullRomSubdivisions == expectedSubdivisions && Math.Abs(item.ColumnStep.Z - 0.1f) < 0.0001f && string.Equals(item.RampResourceKey, ramp.ResourceKey, StringComparison.Ordinal) && diff --git a/src/Mimir.Runtime/MimirRuntime.cs b/src/Mimir.Runtime/MimirRuntime.cs index a6db33d..fa09633 100644 --- a/src/Mimir.Runtime/MimirRuntime.cs +++ b/src/Mimir.Runtime/MimirRuntime.cs @@ -278,9 +278,12 @@ public sealed class MimirRuntime : IAquariumRuntime var sourceCount = sourceIds.Length; var activeColumnCount = checked(sourceCount * historyCount); var resourceColumnCapacity = checked(spectrumSourceLaneCapacity * SpectrumHistoryWindowCount); + var newestHistorySlot = PositiveModulo(-historyFrames[0].Sequence, SpectrumHistoryWindowCount); + var rollingOffset = checked(newestHistorySlot * spectrumSourceLaneCapacity); var samples = new float[width * resourceColumnCapacity]; for (var historyIndex = 0; historyIndex < historyFrames.Length; historyIndex++) { + var historySlot = PositiveModulo(-historyFrames[historyIndex].Sequence, SpectrumHistoryWindowCount); foreach (var spectrum in historyFrames[historyIndex].Spectra) { if (!sourceIndexById.TryGetValue(spectrum.SourceId, out var sourceIndex)) @@ -288,7 +291,7 @@ public sealed class MimirRuntime : IAquariumRuntime continue; } - var physicalColumn = historyIndex * spectrumSourceLaneCapacity + sourceIndex; + var physicalColumn = historySlot * spectrumSourceLaneCapacity + sourceIndex; WriteNormalizedSpectrum(spectrum, samples.AsSpan(physicalColumn * width, width)); } } @@ -378,7 +381,7 @@ public sealed class MimirRuntime : IAquariumRuntime ColumnCount: historyCount, ColumnStride: spectrumSourceLaneCapacity, RollingModulo: resourceColumnCapacity, - RollingOffset: 0, + RollingOffset: rollingOffset, Origin: new Vector3(-5.0f, sourceIndex * SpectrumChannelSeparation, 0.0f), AxisStep: new Vector3(axisStepX, 0.0f, 0.0f), ColumnStep: new Vector3(0.0f, 0.0f, SpectrumWindowDepthSeparation), @@ -451,6 +454,12 @@ public sealed class MimirRuntime : IAquariumRuntime } } + private static int PositiveModulo(long value, int modulo) + { + var remainder = value % modulo; + return (int)(remainder < 0 ? remainder + modulo : remainder); + } + private static Vector3 SpectrumCameraPosition( int spectrumCount, int windowCount, diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 0c67c13..14147d2 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -166,3 +166,4 @@ {"ts": "2026-05-27T11:38:16+01:00", "type": "implementation", "status": "ok", "note": "Mimir spectrum resource declarations now use fixed history capacity instead of growing with the currently filled trail. The runtime smoke proves 48 bands x 4 source lanes x 40 history capacity = 7680 uploaded floats while only the active history count is emitted through each source lowering. This avoids frame-to-frame GPU buffer resizing while preserving the rolling age trail."} {"ts": "2026-05-27T11:41:00+01:00", "type": "implementation", "status": "ok", "note": "Mimir now exposes MIMIR_SPECTRUM_TUBE_SUBDIVISIONS as the first runtime cost lever for TubeField overflow. The spectrum upload smoke sets it to 2 and verifies every emitted source-lane TubeSpline lowering uses that Catmull-Rom subdivision count, so Fensalir's requested/dispatched/truncated segment report can be answered without changing code."} {"ts": "2026-05-27T11:47:48+01:00", "type": "implementation", "status": "ok", "note": "Mimir spectrum upload now has fixed source-lane capacity as well as fixed history capacity. MIMIR_SPECTRUM_SOURCE_LANES sets the lane budget, TubeSpline lowerings use that budget as ColumnStride, surplus sources are truncated and reported in the runtime UI, and --mimir-spectrum-upload-smoke verifies 48 bands x 8 lanes x 40 history capacity = 15360 uploaded floats while emitting only four active TubeField claims/lowerings."} +{"ts": "2026-05-27T11:54:44+01:00", "type": "implementation", "status": "ok", "note": "Mimir spectrum history now uses true ring addressing. Physical history slots are keyed by sequence modulo the fixed history capacity, TubeSpline lowerings carry RollingOffset for newest logical age, and --mimir-spectrum-upload-smoke verifies the expected offset with the fixed 8-lane resource. This makes partial column uploads possible without changing shader sampling semantics."} diff --git a/state/map.yaml b/state/map.yaml index 82a50b8..4ee42b2 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -370,9 +370,11 @@ accepted_design: the lane capacity; surplus sources are truncated and reported in the runtime UI. Lowerings use that capacity as `ColumnStride` through the shared Float32 resource so each active lane renders a true age trail at - `z = age * 0.1`. `MIMIR_SPECTRUM_TUBE_SUBDIVISIONS` controls the - Mimir-requested Catmull-Rom subdivisions so Fensalir's TubeField - truncation report has a direct cost lever. + `z = age * 0.1`. Physical history slots are addressed as a ring with + `RollingOffset`, so logical age no longer requires moving every column in + the upload matrix before shader sampling. `MIMIR_SPECTRUM_TUBE_SUBDIVISIONS` + controls the Mimir-requested Catmull-Rom subdivisions so Fensalir's + TubeField truncation report has a direct cost lever. - `config/perfect-machine.example.json` declares live owners, sensors, reservoir views, and runtime policy. - `src/Mimir.Runtime/Synchronization` now includes the Perfect Machine module