Follow-up to #237 / #238. The assertion infrastructure landed with six permit_alloc escape hatches in audio/engine.rs and a handful of known dealloc-on-RT cases. This issue tracks the proper fixes.
Allocations on RT thread (currently permit_alloc'd)
In Engine::process() per audio block
In Engine::handle_messages()
Deallocations on RT thread (not currently caught)
These slip past assert_no_alloc because the surrounding retire is permit_alloc'd, but they're real. Should be folded into the same fix.
Class of issue: warm-up blindness in tests
The assert_engine_alloc_free helper in rustortion-core/tests/no_alloc.rs runs one engine.process() before the assertion to amortise first-call setup. That call also drains any pending EngineMessage, hiding allocations performed by handle_messages from the audit. The pitch_shifter_does_not_allocate test was restructured in #238 to send SetPitchShift inside the asserted scope, which exposed the PitchShifter::new allocation listed above.
Other message handlers should be audited the same way as part of fixing the offenders above:
Test coverage gaps to close alongside
Out of scope (separate PRs)
- Plugin (
RustortionPlugin::process) — flip nih-plug's assert_process_allocs feature.
- Standalone JACK callback (
ProcessHandler::process) — wrap in assert_no_alloc in dev/debug builds.
Done when
All six permit_alloc calls are deleted from engine.rs, the dealloc-on-RT cases are routed through rt_drop.retire, the warm-up-blind message arms above have dedicated tests, and the test gaps above are closed.
Follow-up to #237 / #238. The assertion infrastructure landed with six
permit_allocescape hatches inaudio/engine.rsand a handful of known dealloc-on-RT cases. This issue tracks the proper fixes.Allocations on RT thread (currently
permit_alloc'd)In
Engine::process()per audio blockaudio/peak_meter.rs:62—Arc::new(PeakMeterInfo)per block. ReplaceArc<ArcSwap<PeakMeterInfo>>with three atomics (AtomicU32× 2 +AtomicBool) —PeakMeterInfois plain data, no need for refcounting. ~30 line change. Permit atengine.rs:174.audio/pitch_shifter.rs— Vec scratch buffers inprocess_block. Preallocate inPitchShifter::newto the max buffer size. Permit atengine.rs:165.audio/recorder.rs:47—Vec::with_capacity(samples.len())per block. Preallocate inRecorder::new. Permit atengine.rs:180.In
Engine::handle_messages()audio/engine.rs:296—Box::new(old_convolver)to type-erase forRtDropHandle::retire'sBox<dyn Send>. Hold convolver asBox<Convolver>on the engine side somem::replacereturns the box directly.audio/engine.rs:333— same pattern for samplers. Changesamplers: Samplers→samplers: Box<Samplers>.audio/engine.rs:381(handle_pitch_shift) —PitchShifter::new(...)constructs FFT scratch buffers on the RT thread whenSetPitchShiftis drained. Move the construction off the RT thread: build aPitchShifterinEngineHandle::set_pitch_shift(GUI side) and ship the constructed instance over the channel asEngineMessage::SetPitchShift(Box<PitchShifter>)(orOption<Box<PitchShifter>>so the0semitones bypass case can shipNone).Cost of the Box wraps is negligible:
SamplersandConvolverare already heap-heavy internally (FFT planners, IR buffers); one pointer indirection perprocess()(not per sample) and 16 bytes inEngine. Idiomatic — everyStageis alreadyBox<dyn Stage>.Alternative: typed retire helpers on
RtDropHandle(one per concrete type), keeping internal storage asBox<dyn Send>but moving the boxing to GUI-thread code. Cleaner separation of concerns.Deallocations on RT thread (not currently caught)
These slip past
assert_no_allocbecause the surroundingretireispermit_alloc'd, but they're real. Should be folded into the same fix.SetSamplersarm —*new_samplersderefs theBox; the emptyBox<Samplers>shell drops on RT. Goes away if we holdsamplersasBox<Samplers>(above).SwapIrConvolverarm — same pattern;*preparedleaves theBox<PreparedIr>shell +PreparedIr.name: Stringto drop on RT. Hold convolver asBox, retirePreparedIrwhole.SetInputFiltersarm —self.input_highpass = hp;directly assigns; the previousOption<Box<dyn Stage>>filter drops on RT. Usemem::replace+rt_drop.retire.Class of issue: warm-up blindness in tests
The
assert_engine_alloc_freehelper inrustortion-core/tests/no_alloc.rsruns oneengine.process()before the assertion to amortise first-call setup. That call also drains any pendingEngineMessage, hiding allocations performed byhandle_messagesfrom the audit. Thepitch_shifter_does_not_allocatetest was restructured in #238 to sendSetPitchShiftinside the asserted scope, which exposed thePitchShifter::newallocation listed above.Other message handlers should be audited the same way as part of fixing the offenders above:
AddStage—Vec::insertmay growAmplifierChain.stages.AmplifierChainhas nowith_capacityAPI.ReplaceStage,RemoveStage— chain operations + retire.SetTunerEnabled(true)— first toggle may grow tuner buffer (extend_from_slice).StartRecording— sender-sideRecorder::newis GUI-thread (fine) but verify the handler itself is alloc-free.Test coverage gaps to close alongside
engine_tuner_enabled_no_output(standalone) oncetuner.processno longer grows its buffer.samplers_preserve_tone_signal(standalone) — currently isolated to Samplers; trivially wrappable.AddStage,ReplaceStage, etc.) that send the message inside the asserted scope so warm-up blindness can't hide regressions.Out of scope (separate PRs)
RustortionPlugin::process) — flip nih-plug'sassert_process_allocsfeature.ProcessHandler::process) — wrap inassert_no_allocin dev/debug builds.Done when
All six
permit_alloccalls are deleted fromengine.rs, the dealloc-on-RT cases are routed throughrt_drop.retire, the warm-up-blind message arms above have dedicated tests, and the test gaps above are closed.