Silence legacy spectrum spline path

This commit is contained in:
Meta Vens 2026-05-26 23:25:16 +01:00
commit 907cf89d03
6 changed files with 23 additions and 268 deletions

View file

@ -93,8 +93,8 @@ Derived state:
- spectrum lines, debug views, screenshots, OBS bridge endpoints, FFmpeg/SRT
helpers, and Eve dashboard feeds are views or diagnostics.
- The direct spline-spectrum dashboard is useful because it reveals buffers; it
is not the architectural owner of field rendering.
- The old direct spline-spectrum dashboard is silent. FieldEvidence TubeField
owns the spectrum surface path; screenshots and logs remain diagnostics.
Forbidden writers:
@ -294,14 +294,11 @@ flowchart TD
## Migration Plan
### Phase 0: Freeze The Current Debug Surface
### Phase 0: Silence Legacy Debug Surfaces
Keep the direct `AquariumSplineFrame` dashboard because it exposes live audio
buffers and is useful for sanity checks. Label it as deterministic debug
surface, not production field evidence.
Delete or keep dead any point-splat spectrum path. It already proved the wrong
owner.
The direct `AquariumSplineFrame` spectrum dashboard and point-splat spectrum
path are no longer scene writers. Live spectrum rendering flows through
FieldEvidence TubeField claims and generated mesh expansion.
### Phase 1: Add Bridge DTOs In Mimir.Runtime

View file

@ -133,22 +133,12 @@ a named invariant that the native runtime cannot protect yet.
service and can emit live sync telemetry with
`MIMIR_SYNC_TELEMETRY_SECONDS`. UI and telemetry read cached reports/states;
they do not run synchronization analysis.
- `MimirRuntime` publishes live ASIO spectrum history to Fensalir as direct
`AquariumSplineFrame` tube surfaces. The spline shader owns visible coverage:
it evaluates capsule/tube distance and writes scene color plus temporal
reservoir guide MRTs. Direct spline rendering is deterministic; native
blue-noise jitter belongs in a future stochastic surface-claim producer, not
in the direct debug surface. The old
`AquariumBufferFieldFrame` / `ReservoirSplats` dashboard path is no longer
allowed to decide this surface. Spectrum rows are timestamped history frames:
row identity is stable, source identity is stable, band identity is stable,
and Z is continuous time-age instead of integer queue age. Current budget
knobs are `MIMIR_SPECTRUM_SPLINE_WINDOW_STRIDE`,
`MIMIR_SPECTRUM_SPLINE_BAND_STRIDE`, and
`MIMIR_SPECTRUM_SPLINE_SUBDIVISIONS`; defaults draw every second history row,
every second frequency bin, and one subdivision per segment. Decimation is
allowed only by persistent sequence or frequency-bin identity, never by
render-frame phase.
- `MimirRuntime` no longer submits the legacy direct `AquariumSplineFrame`
spectrum dashboard. Live spectrum visualization authority is the
`AquariumFieldEvidenceFrame`: Mimir declares the rolling resource, emits a
Tube claim and `AquariumFieldTubeSplineLowering`, and Fensalir plans/expands
the TubeField generated mesh. The old `AquariumBufferFieldFrame` /
`ReservoirSplats` dashboard path is also silent.
- `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

@ -372,28 +372,12 @@ larger claims dishonest. Fensalir's SDF probe/lowering stage should emit
surface splats at that detail-adaptive density, not one splat per pixel by
default.
The live debug spectrum view is a Fensalir spline-tube SDF surface. Mimir
submits rolling ASIO spectrum history as `AquariumSplineFrame` trails:
frequency lives on X, amplitude on Y, history on Z, and channels stack along
Y. `AquariumBufferFieldFrame` / fractal `ReservoirSplats` no longer own this
dashboard, because point splats were the wrong visible primitive for a surface
whose truth is a tube SDF.
Fensalir's spline shader owns visible coverage. Geometry is only the
conservative segment envelope; the pixel shader evaluates capsule/tube
distance, writes color/metadata/control, and emits `reservoirGuide` for the
scene temporal path. The direct spline fallback does not use stochastic
blue-noise perturbation; blue-noise sampling belongs in a real reservoir
producer, not in a direct debug surface where the viewer needs coherent lines.
Mimir must not rotate
which rows or bands exist per frame: that made the surface temporally
incoherent and impossible to follow. The live model stores timestamped spectrum
history frames with monotonically increasing sequence ids, computes Z from
continuous sample age, keeps source ordering stable across the whole history,
and keeps band selection stable. `MIMIR_SPECTRUM_SPLINE_BAND_STRIDE` and
`MIMIR_SPECTRUM_SPLINE_WINDOW_STRIDE` and `MIMIR_SPECTRUM_SPLINE_SUBDIVISIONS`
remain budget knobs, but they may only select by persistent frequency-bin or
history-sequence identity. Render-frame phase is forbidden as a visual owner.
The live debug spectrum view is now field-evidence owned. Mimir no longer
submits direct `AquariumSplineFrame` trails for the spectrum dashboard and does
not submit `AquariumBufferFieldFrame` / fractal `ReservoirSplats` for it either.
Mimir declares the rolling resource, publishes a Tube claim plus
`AquariumFieldTubeSplineLowering`, and Fensalir owns planning, generated mesh
expansion, and TubeField material rendering.
## Known Risks

View file

@ -31,11 +31,6 @@ 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 float SpectrumSplineEmission = 1.85f;
private const float SpectrumSplineHotBloom = 8.0f;
private const int DefaultSpectrumSplineWindowStride = 2;
private const int DefaultSpectrumSplineBandStride = 1;
private const int DefaultSpectrumSplineSubdivisions = 2;
private readonly MimirSynchronizationHub synchronization;
private readonly MimirFensalirFieldLowering fieldLowering;
private readonly MimirAudioSpectrumAnalyzer spectrumAnalyzer;
@ -145,7 +140,6 @@ public sealed class MimirRuntime : IAquariumRuntime
{
var channelCount = Math.Max(1, lastAudioSpectra.Count);
var fieldEvidenceFrame = BuildFieldEvidenceFrame();
var splineFrame = BuildSpectrumSplineFrame();
var windowCount = Math.Max(1, spectrumHistory.Count);
var (splineMin, splineMax) = SpectrumSplineAabb(channelCount, windowCount);
var cameraTarget = (splineMin + splineMax) * 0.5f;
@ -172,7 +166,7 @@ public sealed class MimirRuntime : IAquariumRuntime
UseStarfieldBackground = false,
FieldEvidenceFrame = fieldEvidenceFrame,
BufferFieldFrame = AquariumBufferFieldFrame.Empty,
SplineFrame = splineFrame,
SplineFrame = AquariumSplineFrame.Empty,
});
}
@ -742,27 +736,6 @@ public sealed class MimirRuntime : IAquariumRuntime
: 96;
}
private static int ParseSpectrumSplineBandStride()
{
return int.TryParse(Environment.GetEnvironmentVariable("MIMIR_SPECTRUM_SPLINE_BAND_STRIDE"), out var value)
? Math.Clamp(value, 1, 16)
: DefaultSpectrumSplineBandStride;
}
private static int ParseSpectrumSplineWindowStride()
{
return int.TryParse(Environment.GetEnvironmentVariable("MIMIR_SPECTRUM_SPLINE_WINDOW_STRIDE"), out var value)
? Math.Clamp(value, 1, 16)
: DefaultSpectrumSplineWindowStride;
}
private static int ParseSpectrumSplineSubdivisions()
{
return int.TryParse(Environment.GetEnvironmentVariable("MIMIR_SPECTRUM_SPLINE_SUBDIVISIONS"), out var value)
? Math.Clamp(value, 1, 8)
: DefaultSpectrumSplineSubdivisions;
}
private static bool IsTruthy(string? value) =>
string.Equals(value, "1", StringComparison.OrdinalIgnoreCase) ||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) ||
@ -914,198 +887,6 @@ public sealed class MimirRuntime : IAquariumRuntime
return (min, max);
}
private AquariumSplineFrame BuildSpectrumSplineFrame()
{
if (!sceneReady || spectrumHistory.Count == 0)
{
return AquariumSplineFrame.Empty;
}
var windows = spectrumHistory.ToArray();
var sourceIds = windows
.SelectMany(window => window.Spectra)
.Select(spectrum => spectrum.SourceId)
.Distinct(StringComparer.Ordinal)
.OrderBy(sourceId => sourceId, StringComparer.Ordinal)
.ToArray();
if (sourceIds.Length == 0)
{
return AquariumSplineFrame.Empty;
}
var windowStride = ParseSpectrumSplineWindowStride();
var bandStride = ParseSpectrumSplineBandStride();
var subdivisions = ParseSpectrumSplineSubdivisions();
var approximateWindowCount = Math.Max(1, (windows.Length + windowStride - 1) / windowStride);
var splines = new List<AquariumSpline3D>(approximateWindowCount * sourceIds.Length + sourceIds.Length * 4);
AddSpectrumGridSplines(splines, sourceIds.Length, windows.Length);
foreach (var window in windows)
{
var isLatest = window.Sequence == windows[^1].Sequence;
if (!isLatest && windowStride > 1 && window.Sequence % windowStride != 0)
{
continue;
}
var ageSeconds = Math.Max(0.0f, runtimeSeconds - window.RuntimeSeconds);
var ageT = Math.Clamp(ageSeconds / Math.Max(spectrumUpdateIntervalSeconds * Math.Max(1, SpectrumHistoryWindowCount - 1), 0.0001f), 0.0f, 1.0f);
var alpha = Math.Clamp(0.10f + MathF.Pow(1.0f - ageT, 1.35f) * 0.90f, 0.10f, 1.0f);
var z = ageSeconds * SpectrumWindowDepthSeparation / Math.Max(spectrumUpdateIntervalSeconds, 0.0001f);
var spectraBySource = window.Spectra.ToDictionary(spectrum => spectrum.SourceId, StringComparer.Ordinal);
for (var channelIndex = 0; channelIndex < sourceIds.Length; channelIndex++)
{
if (!spectraBySource.TryGetValue(sourceIds[channelIndex], out var spectrum) || spectrum.BandDecibels.Count < 2)
{
continue;
}
var points = BuildSpectrumWindowSpline(
spectrum,
channelIndex * SpectrumChannelSeparation,
z,
alpha,
bandStride);
splines.Add(new AquariumSpline3D(
$"spectrum-{spectrum.SourceId}-{window.Sequence}",
points,
new AquariumSplineStyle(0.020f, SpectrumSplineEmission, 1.0f, 0.72f, 0.20f),
CatmullRomSubdivisions: subdivisions));
}
}
return new AquariumSplineFrame { Splines = splines };
}
private static IReadOnlyList<AquariumSplineVertex> BuildSpectrumWindowSpline(
MimirAudioSpectrumSnapshot spectrum,
float channelBaseY,
float z,
float alpha,
int bandStride)
{
var bands = spectrum.BandDecibels;
var envelope = SmoothedSpectrumEnvelope(spectrum);
var points = new List<AquariumSplineVertex>((bands.Count + bandStride - 1) / bandStride + 2);
for (var index = 0; index < bands.Count; index++)
{
var isEdge = index == 0 || index == bands.Count - 1;
if (!isEdge && bandStride > 1 && index % bandStride != 0)
{
continue;
}
var x = -SpectrumWidth * 0.5f + SpectrumWidth * index / Math.Max(1, bands.Count - 1);
var normalized = envelope[index];
var y = channelBaseY + normalized * SpectrumAmplitudeHeight;
points.Add(new AquariumSplineVertex(
new Vector3(x, y, z),
SpectrumColor(normalized, alpha)));
}
return points;
}
private static float[] SmoothedSpectrumEnvelope(MimirAudioSpectrumSnapshot spectrum)
{
var bands = spectrum.BandDecibels;
var floor = Math.Min(spectrum.NoiseFloorDb, bands.Min());
var ceiling = Math.Max(-24.0, Percentile(bands, 0.96));
var span = Math.Max(18.0, ceiling - floor);
var normalized = new float[bands.Count];
for (var index = 0; index < bands.Count; index++)
{
normalized[index] = (float)Math.Clamp((bands[index] - floor) / span, 0.0, 1.35);
}
return GaussianSmooth5(normalized);
}
private static float[] GaussianSmooth5(IReadOnlyList<float> values)
{
var smoothed = new float[values.Count];
for (var index = 0; index < values.Count; index++)
{
var value =
values[Math.Max(0, index - 2)] * 0.0625f +
values[Math.Max(0, index - 1)] * 0.25f +
values[index] * 0.375f +
values[Math.Min(values.Count - 1, index + 1)] * 0.25f +
values[Math.Min(values.Count - 1, index + 2)] * 0.0625f;
smoothed[index] = Math.Clamp(value, 0.0f, 1.25f);
}
return smoothed;
}
private static double Percentile(IReadOnlyList<double> values, double percentile)
{
if (values.Count == 0)
{
return 0.0;
}
var sorted = values.OrderBy(value => value).ToArray();
var index = Math.Clamp((int)Math.Round((sorted.Length - 1) * percentile), 0, sorted.Length - 1);
return sorted[index];
}
private static void AddSpectrumGridSplines(
List<AquariumSpline3D> splines,
int channelCount,
int windowCount)
{
var nearZ = 0.0f;
var farZ = Math.Max(1, windowCount - 1) * SpectrumWindowDepthSeparation;
for (var channelIndex = 0; channelIndex < channelCount; channelIndex++)
{
var y = channelIndex * SpectrumChannelSeparation;
var color = new Vector4(0.24f, 0.90f, 1.05f, 0.28f);
splines.Add(new AquariumSpline3D(
$"spectrum-grid-x-{channelIndex}",
[
new AquariumSplineVertex(new Vector3(-SpectrumWidth * 0.5f, y, nearZ), color),
new AquariumSplineVertex(new Vector3(SpectrumWidth * 0.5f, y, nearZ), color),
new AquariumSplineVertex(new Vector3(SpectrumWidth * 0.5f, y, farZ), color),
new AquariumSplineVertex(new Vector3(-SpectrumWidth * 0.5f, y, farZ), color),
new AquariumSplineVertex(new Vector3(-SpectrumWidth * 0.5f, y, nearZ), color),
],
new AquariumSplineStyle(0.006f, 0.8f, 0.7f, 0.72f, 0.28f),
CatmullRomSubdivisions: 1));
for (var line = 0; line <= 4; line++)
{
var x = -SpectrumWidth * 0.5f + SpectrumWidth * line / 4.0f;
splines.Add(new AquariumSpline3D(
$"spectrum-grid-z-{channelIndex}-{line}",
[
new AquariumSplineVertex(new Vector3(x, y, nearZ), color),
new AquariumSplineVertex(new Vector3(x, y, farZ), color),
],
new AquariumSplineStyle(0.005f, 0.8f, 0.7f, 0.72f, 0.28f),
CatmullRomSubdivisions: 1));
}
}
}
private static Vector4 SpectrumColor(float normalized, float alpha)
{
var hot = Math.Clamp(normalized, 0.0f, 1.0f);
var hardDrive = SmoothStep(0.58f, 0.96f, hot);
var bloom = 1.0f + SpectrumSplineHotBloom * hardDrive * hardDrive;
var red = (0.72f + 2.35f * hot) * bloom;
var green = (0.10f + 1.90f * hot * hot) * bloom;
var blue = (0.40f * (1.0f - hot) + 0.16f * hot + 0.22f * hardDrive) * (1.0f + 2.8f * hardDrive);
var drivenAlpha = alpha * Math.Clamp(0.34f + 0.88f * hot + 0.35f * hardDrive, 0.24f, 1.0f);
return new Vector4(red, green, blue, drivenAlpha);
}
private static float SmoothStep(float edge0, float edge1, float value)
{
var t = Math.Clamp((value - edge0) / Math.Max(0.0001f, edge1 - edge0), 0.0f, 1.0f);
return t * t * (3.0f - 2.0f * t);
}
private static string SpectrumBars(MimirAudioSpectrumSnapshot spectrum)
{
const string ramp = " .:-=+*#%@";

View file

@ -156,3 +156,4 @@
{"ts": "2026-05-26T22:55:00+01:00", "type": "implementation", "status": "ok", "note": "Fensalir TubeField expansion now follows selected backend packet authority. D3D12Renderer expands only TubeSplineLowerings whose claim has a planned TubeField packet; unplanned lowerings are counted in debug status instead of rendered. This prevents explicit lowering metadata from bypassing validation/planning."}
{"ts": "2026-05-26T22:58:00+01:00", "type": "implementation", "status": "ok", "note": "Fensalir validation now rejects TubeSpline lowering metadata if the referenced claim is not Tube-encoded or if the lowering resource differs from the claim payload resource. TubeField split authority is caught at validation, before renderer packet gating. Fensalir build/tests/headless reload passed."}
{"ts": "2026-05-26T23:03:00+01:00", "type": "implementation", "status": "ok", "note": "Mimir's typed Fensalir surface-intent lowering now emits AquariumFieldTubeSplineLowering metadata for audio spectrum/waveform Tube claims. The hardware-free field evidence smoke now proves one resource, three claims, one TubeSpline lowering, one planned TubeField packet, and two deferred non-backend claims; the DSL smoke still proves resource-bound TubeSpline lowering with ramp binding."}
{"ts": "2026-05-26T23:18:00+01:00", "type": "correction", "status": "ok", "note": "Silenced the legacy direct spectrum spline path. MimirRuntime now submits AquariumSplineFrame.Empty and the old BuildSpectrumSplineFrame/direct spline styling helpers were deleted, so live spectrum visualization authority is FieldEvidence TubeField only. Mimir build and both Fensalir field smokes passed."}

View file

@ -350,7 +350,9 @@ accepted_design:
not match the claim payload. Mimir's typed surface-intent lowering now emits
TubeSpline lowering metadata for audio spectrum/waveform Tube claims, so the
generated mesh path is fed by real Mimir evidence rather than only the DSL
smoke.
smoke. The legacy direct `AquariumSplineFrame` spectrum dashboard is silent:
`MimirRuntime` submits an empty spline frame and the old direct spectrum
spline builder has been deleted.
- `config/perfect-machine.example.json` declares live owners, sensors,
reservoir views, and runtime policy.
- `src/Mimir.Runtime/Synchronization` now includes the Perfect Machine module