diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index e3cdc8f..52514ea 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -149,12 +149,14 @@ a named invariant that the native runtime cannot protect yet. ramp PNG as a GPU-resident Texture2D resource and binds it by resource key. The uploaded spectrum resource is a rolling column matrix now: physical columns are flattened `(history age, source lane)` pairs, and Mimir emits one - Tube claim/lowering per source with `ColumnStride=sourceCount` so each lane - traverses its own age trail at `z = age * 0.1`. The resource uses a fixed - history-column capacity so frame-to-frame content versions do not resize the - GPU buffer while the history fills. `MIMIR_SPECTRUM_TUBE_SUBDIVISIONS` - controls the Mimir-requested Catmull-Rom subdivision count so Fensalir's - overflow report has a direct cost lever. + Tube claim/lowering per active source. The resource uses fixed history and + source-lane capacities, so frame-to-frame content versions and source + 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 + requested Catmull-Rom subdivision count so Fensalir's overflow report has a + direct cost lever. - `MimirFensalirFieldLowering` now emits `AquariumFieldResourceDeclaration` rows for live native/GPU payload views. Observation claims reference `mimir:resource:*` keys, and Fensalir validation/planning can reject or defer diff --git a/notes/current-system-map.md b/notes/current-system-map.md index 40367c1..35d957f 100644 --- a/notes/current-system-map.md +++ b/notes/current-system-map.md @@ -387,9 +387,11 @@ blackbody ramp as a Texture2D field resource and binds the TubeField material by resource key. Runtime spectrum columns now represent `(history age, source lane)` pairs rather than only the latest spectra; Mimir emits one TubeField claim/lowering per source, each striding through the shared resource to render -its own age trail at `z = age * 0.1`. The resource advertises a fixed history -capacity, so content updates do not resize the GPU buffer while the rolling -trail fills. `MIMIR_SPECTRUM_TUBE_SUBDIVISIONS` is the runtime cost lever for +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 truncation. diff --git a/src/Mimir.BufferSmoke/Program.cs b/src/Mimir.BufferSmoke/Program.cs index 9224e71..0687441 100644 --- a/src/Mimir.BufferSmoke/Program.cs +++ b/src/Mimir.BufferSmoke/Program.cs @@ -631,8 +631,10 @@ static int RunMimirSpectrumUploadSmoke() { var previousSyntheticPreview = Environment.GetEnvironmentVariable("MIMIR_SYNTHETIC_SPECTRUM_PREVIEW"); var previousTubeSubdivisions = Environment.GetEnvironmentVariable("MIMIR_SPECTRUM_TUBE_SUBDIVISIONS"); + var previousSourceLanes = Environment.GetEnvironmentVariable("MIMIR_SPECTRUM_SOURCE_LANES"); Environment.SetEnvironmentVariable("MIMIR_SYNTHETIC_SPECTRUM_PREVIEW", "1"); Environment.SetEnvironmentVariable("MIMIR_SPECTRUM_TUBE_SUBDIVISIONS", "2"); + Environment.SetEnvironmentVariable("MIMIR_SPECTRUM_SOURCE_LANES", "8"); try { var settings = new MimirSynchronizationSettings @@ -672,7 +674,7 @@ static int RunMimirSpectrumUploadSmoke() var lowering = matchingLowerings.FirstOrDefault(); var validation = AquariumFieldEvidenceValidator.Validate(field); var plan = AquariumFieldLoweringPlanner.Plan(field); - const int expectedSourceCount = 4; + const int expectedSourceLaneCapacity = 8; const int expectedHistoryCount = 3; const int expectedHistoryCapacity = 40; const int expectedSubdivisions = 2; @@ -706,11 +708,11 @@ static int RunMimirSpectrumUploadSmoke() ramp.Access == AquariumFieldShaderAccess.ShaderResource && string.Equals(ramp.Format, "Rgba8Unorm", StringComparison.OrdinalIgnoreCase) && string.Equals(ramp.SourceUri, @"D:\WIP4\Projects\Aetheria\Assets\Resources\Ramps\blackbody.png", StringComparison.OrdinalIgnoreCase) && - resource.Height == expectedSourceCount * expectedHistoryCapacity && + resource.Height == expectedSourceLaneCapacity * expectedHistoryCapacity && lowering.Width * resource.Height == upload.Float32Data.Count && matchingLowerings.All(item => item.ColumnCount == expectedHistoryCount && - item.ColumnStride == expectedSourceCount && + item.ColumnStride == expectedSourceLaneCapacity && item.RollingModulo == resource.Height && item.CatmullRomSubdivisions == expectedSubdivisions && Math.Abs(item.ColumnStep.Z - 0.1f) < 0.0001f && @@ -726,6 +728,7 @@ static int RunMimirSpectrumUploadSmoke() { Environment.SetEnvironmentVariable("MIMIR_SYNTHETIC_SPECTRUM_PREVIEW", previousSyntheticPreview); Environment.SetEnvironmentVariable("MIMIR_SPECTRUM_TUBE_SUBDIVISIONS", previousTubeSubdivisions); + Environment.SetEnvironmentVariable("MIMIR_SPECTRUM_SOURCE_LANES", previousSourceLanes); } } diff --git a/src/Mimir.Runtime/MimirRuntime.cs b/src/Mimir.Runtime/MimirRuntime.cs index d2377d5..a6db33d 100644 --- a/src/Mimir.Runtime/MimirRuntime.cs +++ b/src/Mimir.Runtime/MimirRuntime.cs @@ -31,6 +31,7 @@ public sealed class MimirRuntime : IAquariumRuntime private const float SpectrumCameraFitPadding = 1.12f; private const float SpectrumFrustumMinimumNear = 0.01f; private const float SpectrumSplineTubePadding = 0.18f; + private const int DefaultSpectrumSourceLaneCapacity = 8; private const string SpectrumFieldResourceKey = "mimir:resource:spectrum:field-upload"; private const string SpectrumRampResourceKey = "aquarium:resource:ramp:blackbody"; private const string SpectrumRampTexturePath = @"D:\WIP4\Projects\Aetheria\Assets\Resources\Ramps\blackbody.png"; @@ -45,6 +46,7 @@ public sealed class MimirRuntime : IAquariumRuntime private readonly float audioSyncUpdateIntervalSeconds; private readonly float spectrumUpdateIntervalSeconds; private readonly int spectrumTubeSubdivisions; + private readonly int spectrumSourceLaneCapacity; private readonly float calibrationGain; private readonly float watermarkGain; private readonly bool syntheticSpectrumPreview; @@ -60,6 +62,7 @@ public sealed class MimirRuntime : IAquariumRuntime private double lastAudioSyncAnalysisMilliseconds; private double lastSpectrumAnalysisMilliseconds; private double lastPassiveSynchronizationConfidence; + private int lastDroppedSpectrumSourceLaneCount; private float spectrumCameraDistanceMultiplier = SpectrumCameraDefaultDistanceMultiplier; private float spectrumCameraAngleDegrees = SpectrumCameraDefaultAngleDegrees; private AquariumCameraFrustum lastSpectrumFrustum = AquariumCameraFrustum.Default; @@ -115,6 +118,7 @@ public sealed class MimirRuntime : IAquariumRuntime audioSyncUpdateIntervalSeconds = ParseAudioSyncIntervalSeconds(); spectrumUpdateIntervalSeconds = ParseSpectrumUpdateIntervalSeconds(); spectrumTubeSubdivisions = ParseSpectrumTubeSubdivisions(); + spectrumSourceLaneCapacity = ParseSpectrumSourceLaneCapacity(); calibrationGain = settings.Audio.CalibrationGain; watermarkGain = settings.Audio.WatermarkGain; syntheticSpectrumPreview = IsTruthy(Environment.GetEnvironmentVariable("MIMIR_SYNTHETIC_SPECTRUM_PREVIEW")); @@ -244,18 +248,23 @@ public sealed class MimirRuntime : IAquariumRuntime var historyFrames = spectrumHistory.Count > 0 ? spectrumHistory.Reverse().ToArray() : [new MimirSpectrumHistoryFrame(spectrumHistorySequence, runtimeSeconds, spectra)]; - var sourceIds = historyFrames + var allSourceIds = historyFrames .SelectMany(static history => history.Spectra) .Where(static spectrum => spectrum.BandDecibels.Count >= 2) .Select(static spectrum => spectrum.SourceId) .Distinct(StringComparer.Ordinal) .OrderBy(static sourceId => sourceId, StringComparer.Ordinal) .ToArray(); + var sourceIds = allSourceIds + .Take(spectrumSourceLaneCapacity) + .ToArray(); if (sourceIds.Length == 0) { + lastDroppedSpectrumSourceLaneCount = 0; return frame; } + lastDroppedSpectrumSourceLaneCount = allSourceIds.Length - sourceIds.Length; var sourceIndexById = sourceIds .Select((sourceId, index) => (sourceId, index)) .ToDictionary(static item => item.sourceId, static item => item.index, StringComparer.Ordinal); @@ -268,7 +277,7 @@ public sealed class MimirRuntime : IAquariumRuntime var historyCount = historyFrames.Length; var sourceCount = sourceIds.Length; var activeColumnCount = checked(sourceCount * historyCount); - var resourceColumnCapacity = checked(sourceCount * SpectrumHistoryWindowCount); + var resourceColumnCapacity = checked(spectrumSourceLaneCapacity * SpectrumHistoryWindowCount); var samples = new float[width * resourceColumnCapacity]; for (var historyIndex = 0; historyIndex < historyFrames.Length; historyIndex++) { @@ -279,7 +288,7 @@ public sealed class MimirRuntime : IAquariumRuntime continue; } - var physicalColumn = historyIndex * sourceCount + sourceIndex; + var physicalColumn = historyIndex * spectrumSourceLaneCapacity + sourceIndex; WriteNormalizedSpectrum(spectrum, samples.AsSpan(physicalColumn * width, width)); } } @@ -367,7 +376,7 @@ public sealed class MimirRuntime : IAquariumRuntime StrideBytes: sizeof(float), FirstColumn: sourceIndex, ColumnCount: historyCount, - ColumnStride: sourceCount, + ColumnStride: spectrumSourceLaneCapacity, RollingModulo: resourceColumnCapacity, RollingOffset: 0, Origin: new Vector3(-5.0f, sourceIndex * SpectrumChannelSeparation, 0.0f), @@ -672,7 +681,13 @@ public sealed class MimirRuntime : IAquariumRuntime panel.Readout("Frustum", DescribeSpectrumFrustum); panel.Readout( "Tube budget", - () => $"{spectrumHistory.Count} age columns x {lastAudioSpectra.Count} lanes x {spectrumTubeSubdivisions} subdivisions", + () => + { + var dropped = lastDroppedSpectrumSourceLaneCount > 0 + ? $" dropped={lastDroppedSpectrumSourceLaneCount}" + : ""; + return $"{spectrumHistory.Count}/{SpectrumHistoryWindowCount} age columns x {Math.Min(lastAudioSpectra.Count, spectrumSourceLaneCapacity)}/{spectrumSourceLaneCapacity} lanes x {spectrumTubeSubdivisions} subdivisions{dropped}"; + }, "Current Mimir-side TubeField subdivision cost before Fensalir applies its fixed geometry budget."); panel.TextBox( "Spectra", @@ -975,6 +990,13 @@ public sealed class MimirRuntime : IAquariumRuntime : 4; } + private static int ParseSpectrumSourceLaneCapacity() + { + return int.TryParse(Environment.GetEnvironmentVariable("MIMIR_SPECTRUM_SOURCE_LANES"), out var value) + ? Math.Clamp(value, 1, 32) + : DefaultSpectrumSourceLaneCapacity; + } + private static bool IsTruthy(string? value) => string.Equals(value, "1", StringComparison.OrdinalIgnoreCase) || string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) || diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 0c9f984..0c67c13 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -165,3 +165,4 @@ {"ts": "2026-05-27T11:33:22+01:00", "type": "implementation", "status": "ok", "note": "Mimir spectrum upload is now a rolling column matrix instead of a latest-frame row dump. The shared Float32 resource flattens physical columns as history-age/source-lane pairs; Mimir emits one TubeField claim/lowering per source with ColumnStride=sourceCount and ColumnStep.z=0.1 so each source gets its own age trail through the shared GPU buffer."} {"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."} diff --git a/state/map.yaml b/state/map.yaml index f40b6ad..82a50b8 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -363,14 +363,16 @@ accepted_design: GPU-resident Texture2D field resource and binds the TubeField material by `RampResourceKey`. Runtime spectrum columns now represent flattened `(history age, source - lane)` pairs. Mimir emits one TubeField claim/lowering per source, each - using `ColumnStride=sourceCount` through the shared Float32 resource so the - generated geometry renders a true age trail at `z = age * 0.1`. The - spectrum resource advertises a fixed history capacity so frame content - versions do not resize the GPU buffer while the rolling trail fills. - `MIMIR_SPECTRUM_TUBE_SUBDIVISIONS` controls the Mimir-requested Catmull-Rom - subdivisions so Fensalir's TubeField truncation report has a direct cost - lever. + lane)` pairs. Mimir emits one TubeField claim/lowering per active source. + The spectrum resource advertises fixed history and source-lane capacities + so frame content versions and source topology changes do not resize the GPU + buffer while the rolling trail fills. `MIMIR_SPECTRUM_SOURCE_LANES` sets + 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. - `config/perfect-machine.example.json` declares live owners, sensors, reservoir views, and runtime policy. - `src/Mimir.Runtime/Synchronization` now includes the Perfect Machine module