diff --git a/iOS/Views/Visualizer/MitsuhaVisualizerView.swift b/iOS/Views/Visualizer/MitsuhaVisualizerView.swift index 7259c72..36648cb 100644 --- a/iOS/Views/Visualizer/MitsuhaVisualizerView.swift +++ b/iOS/Views/Visualizer/MitsuhaVisualizerView.swift @@ -143,13 +143,34 @@ final class WaveStateCache { // MARK: - Level Box /// Per-instance mutable render state. Zero @Published properties → no observation warnings. +/// All working buffers pre-allocated here so the 60fps render loop does zero heap allocation. fileprivate final class VisualizerLevelBox: ObservableObject { - var displayLevels: [Float] = [] - var peakFollower: Float = 0.01 + var displayLevels: [Float] = [] + var targetLevels: [Float] = [] // scratch buffer — pre-allocated, mutated in-place each frame + var idleLevels: [Float] = [] // flat idle wave — pre-allocated, updated only when count changes + var peakFollower: Float = 0.01 var levelHistoryBuf: [[Float]] = [] var historyWriteIdx: Int = 0 var wobblePhaseOffset: Double = 0 - var lastTickTime: CFTimeInterval = 0 + var lastTickTime: CFTimeInterval = 0 + + /// Resize all per-frame buffers when point count changes (rare — settings slider). + func resizeIfNeeded(count: Int, idleAmplitude: Float) { + guard count > 0 else { return } + if targetLevels.count != count { + targetLevels = [Float](repeating: 0, count: count) + displayLevels = [Float](repeating: idleAmplitude, count: count) + idleLevels = [Float](repeating: idleAmplitude, count: count) + // Pre-fill history ring with idle so depth ghost doesn't start blank + levelHistoryBuf = [[Float]]( + repeating: [Float](repeating: idleAmplitude, count: count), + count: MitsuhaVisualizerView.historySize + ) + historyWriteIdx = 0 + } else if idleLevels.first != idleAmplitude { + for i in 0..