Upload spectrum ring slots by offset

This commit is contained in:
Meta Vens 2026-05-27 12:01:34 +01:00
commit 07a7864944
6 changed files with 49 additions and 26 deletions

View file

@ -156,9 +156,11 @@ a named invariant that the native runtime cannot protect yet.
truncated and reported in the runtime UI. Lowerings use that fixed capacity
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.
before shader sampling. Mimir emits one Float32 resource upload per sampled
history slot with an explicit element offset, rather than uploading the full
fixed-capacity backing buffer every frame. `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

View file

@ -393,9 +393,11 @@ resize the GPU buffer while the rolling trail fills. `MIMIR_SPECTRUM_SOURCE_LANE
sets the fixed lane budget; surplus sources are truncated and reported in the
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.
shader sampling. Mimir emits one Float32 resource upload per sampled history
slot with an explicit element offset instead of uploading the full
fixed-capacity backing buffer every frame. `MIMIR_SPECTRUM_TUBE_SUBDIVISIONS`
is the runtime cost lever for lowering Catmull-Rom subdivision count when
Fensalir reports TubeField truncation.
## Known Risks

View file

@ -655,8 +655,9 @@ static int RunMimirSpectrumUploadSmoke()
var frame = runtime.Frame;
var field = frame.Scene.FieldEvidenceFrame;
var upload = field.ResourceUploads.SingleOrDefault(upload =>
string.Equals(upload.ResourceKey, "mimir:resource:spectrum:field-upload", StringComparison.Ordinal));
var matchingUploads = field.ResourceUploads
.Where(upload => string.Equals(upload.ResourceKey, "mimir:resource:spectrum:field-upload", StringComparison.Ordinal))
.ToArray();
var matchingResources = field.Resources
.Where(resource => string.Equals(resource.ResourceKey, "mimir:resource:spectrum:field-upload", StringComparison.Ordinal))
.ToArray();
@ -686,19 +687,30 @@ static int RunMimirSpectrumUploadSmoke()
.ToArray();
Console.WriteLine(
$"mimir-spectrum-upload-smoke resources={field.Resources.Count} uploads={field.ResourceUploads.Count} claims={field.Claims.Count} tubeLowerings={field.TubeSplineLowerings.Count} planned={plan.Packets.Count} errors={validation.HasErrors} legacySplines={frame.Scene.SplineFrame.Splines.Count} bufferFields={frame.Scene.BufferFieldFrame.SplineTubeFields.Count + frame.Scene.BufferFieldFrame.TextureSplineFields.Count} uploadFloats={upload?.Float32Data.Count ?? 0}");
$"mimir-spectrum-upload-smoke resources={field.Resources.Count} uploads={field.ResourceUploads.Count} claims={field.Claims.Count} tubeLowerings={field.TubeSplineLowerings.Count} planned={plan.Packets.Count} errors={validation.HasErrors} legacySplines={frame.Scene.SplineFrame.Splines.Count} bufferFields={frame.Scene.BufferFieldFrame.SplineTubeFields.Count + frame.Scene.BufferFieldFrame.TextureSplineFields.Count} uploadFloats={matchingUploads.Sum(upload => upload.Float32Data.Count)}");
return
!validation.HasErrors &&
!frame.Scene.SplineFrame.HasInput &&
!frame.Scene.BufferFieldFrame.HasInput &&
upload is not null &&
matchingUploads.Length == expectedHistoryCount &&
matchingResources.Length == 1 &&
matchingRamps.Length == 1 &&
matchingLowerings.Length == 4 &&
matchingClaims.Length == matchingLowerings.Length &&
upload.Version == resource.Version &&
upload.Float32Data.Count == resource.DepthOrCount &&
matchingUploads.All(upload => upload.Version == resource.Version) &&
matchingUploads.Sum(upload => upload.Float32Data.Count) == lowering.Width * expectedSourceLaneCapacity * expectedHistoryCount &&
matchingUploads.All(upload =>
upload.Float32Data.Count == lowering.Width * expectedSourceLaneCapacity &&
upload.ElementOffset >= 0 &&
upload.ElementOffset + upload.Float32Data.Count <= resource.DepthOrCount) &&
matchingUploads.Select(upload => upload.ElementOffset).Order().SequenceEqual(
new[]
{
0,
lowering.Width * expectedSourceLaneCapacity * (expectedHistoryCapacity - 2),
lowering.Width * expectedSourceLaneCapacity * (expectedHistoryCapacity - 1),
}.Order()) &&
resource.Kind == AquariumFieldResourceKind.StructuredBuffer &&
resource.Residency == AquariumFieldResourceResidency.GpuResident &&
resource.Access == AquariumFieldShaderAccess.ShaderResource &&
@ -710,7 +722,6 @@ static int RunMimirSpectrumUploadSmoke()
string.Equals(ramp.Format, "Rgba8Unorm", StringComparison.OrdinalIgnoreCase) &&
string.Equals(ramp.SourceUri, @"D:\WIP4\Projects\Aetheria\Assets\Resources\Ramps\blackbody.png", StringComparison.OrdinalIgnoreCase) &&
resource.Height == expectedSourceLaneCapacity * expectedHistoryCapacity &&
lowering.Width * resource.Height == upload.Float32Data.Count &&
matchingLowerings.All(item =>
item.ColumnCount == expectedHistoryCount &&
item.ColumnStride == expectedSourceLaneCapacity &&

View file

@ -280,10 +280,12 @@ public sealed class MimirRuntime : IAquariumRuntime
var resourceColumnCapacity = checked(spectrumSourceLaneCapacity * SpectrumHistoryWindowCount);
var newestHistorySlot = PositiveModulo(-historyFrames[0].Sequence, SpectrumHistoryWindowCount);
var rollingOffset = checked(newestHistorySlot * spectrumSourceLaneCapacity);
var samples = new float[width * resourceColumnCapacity];
var resourceUploads = new List<AquariumFieldResourceUpload>(historyFrames.Length);
for (var historyIndex = 0; historyIndex < historyFrames.Length; historyIndex++)
{
var historySlot = PositiveModulo(-historyFrames[historyIndex].Sequence, SpectrumHistoryWindowCount);
var uploadColumnOffset = checked(historySlot * spectrumSourceLaneCapacity);
var samples = new float[width * spectrumSourceLaneCapacity];
foreach (var spectrum in historyFrames[historyIndex].Spectra)
{
if (!sourceIndexById.TryGetValue(spectrum.SourceId, out var sourceIndex))
@ -291,9 +293,16 @@ public sealed class MimirRuntime : IAquariumRuntime
continue;
}
var physicalColumn = historySlot * spectrumSourceLaneCapacity + sourceIndex;
WriteNormalizedSpectrum(spectrum, samples.AsSpan(physicalColumn * width, width));
WriteNormalizedSpectrum(spectrum, samples.AsSpan(sourceIndex * width, width));
}
resourceUploads.Add(new AquariumFieldResourceUpload
{
ResourceKey = SpectrumFieldResourceKey,
Version = unchecked((ulong)Math.Max(0, spectrumHistorySequence)),
ElementOffset = checked(uploadColumnOffset * width),
Float32Data = samples,
});
}
const string domainKey = "mimir:domain:spectrum:field-upload";
@ -323,7 +332,7 @@ public sealed class MimirRuntime : IAquariumRuntime
Format: "Float32",
Width: width,
Height: resourceColumnCapacity,
DepthOrCount: samples.Length,
DepthOrCount: checked(width * resourceColumnCapacity),
StrideBytes: sizeof(float),
ValidFromNs: 0,
ValidUntilNs: 0,
@ -422,12 +431,7 @@ public sealed class MimirRuntime : IAquariumRuntime
ResourceUploads =
[
.. frame.ResourceUploads,
new AquariumFieldResourceUpload
{
ResourceKey = SpectrumFieldResourceKey,
Version = version,
Float32Data = samples,
},
.. resourceUploads,
],
TubeSplineLowerings = [.. frame.TubeSplineLowerings, .. lowerings],
AccumulationWindowSeconds = frame.AccumulationWindowSeconds,

View file

@ -167,3 +167,4 @@
{"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."}
{"ts": "2026-05-27T12:00:41+01:00", "type": "implementation", "status": "ok", "note": "Mimir now emits partial spectrum resource uploads for the active ring slots it samples. The declared GPU buffer remains fixed at 48 bands x 8 lanes x 40 history slots, but the smoke now verifies three offset uploads totaling 1152 floats for the three active ages instead of a full 15360-float backing-store upload. Headless Fensalir rendered the offset-copy path with empty stderr."}

View file

@ -372,9 +372,12 @@ accepted_design:
shared Float32 resource so each active lane renders a true age trail at
`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.
the upload matrix before shader sampling. Mimir emits one Float32 resource
upload per sampled history slot with an explicit element offset instead of
uploading the full fixed-capacity backing buffer every frame.
`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