Threading contract
The contract
Section titled “The contract”- Render thread:
sumi_create,sumi_destroy,sumi_resize,sumi_update,sumi_render, params, gestures and triggers are called from a single thread and are not thread-safe with each other. - MIDI producer:
sumi_push_midimay be called from exactly one other thread, concurrently with the render thread. The queue between them is a lock-free single-producer / single-consumer ring (4096 messages, drop-oldest on overflow with a diagnostics counter). A host with several MIDI sources serializes them before pushing. - The probe is the exception:
sumi_layout_probeis instance-free and pure, callable from any thread with a params snapshot — the UI thread hit-tests without a round trip to the MIDI thread.
Overflow is survivable
Section titled “Overflow is survivable”Drop-oldest can discard a Note Off and leave a voice feeding ink forever. So whenever the dropped counter increments, the voice mapper arms a per-voice inactivity timeout (about ten seconds without any message for that voice while other traffic flows → a synthesized VoiceEnd, logged). Timeouts are armed only after an overflow — normal operation never expires a voice.
Coalescing
Section titled “Coalescing”Continuous dimensions (press, glide, slide, swirl, breath) are coalesced per
voice per frame: the simulator consumes at most one value per dimension per
voice per sumi_update, exponentially smoothed (~30 ms). Deformation events
are capped per frame (a budget of passes, overflow merged within a voice across
frames, never culling one echo of a set while feeding another), so an
aftertouch-dense Osmose or a ten-voice Jankó performance cannot starve the
frame rate.
Host-generated MIDI counts as device MIDI
Section titled “Host-generated MIDI counts as device MIDI”The tablets’ Play surface synthesizes MPE. Those bytes flow through the shell’s same single producer — never from the UI thread:
- iOS: a serial MIDI dispatch queue owns the
hostmpestate, the strip engine, the limiters and the byte log. Touch-down hops onto it synchronously for the voice id (microseconds). - Android: the AMidi poller thread is the Phase-4 host thread. It owns all
hostmpestate and is the only caller ofsumi_push_midi; the UI thread posts commands, and the two synchronous hops (touch-down and pen-down need the voice id) are bounded at 250 ms with a saturation fallback, so a wedged MIDI thread can never freeze the UI.
Every generated byte and every device byte cross one merge point per shell, which also feeds the external-occupancy mask, the echo guard and the byte log. That single point is why one analyser set can verify both platforms from their logs.
Android teardown
Section titled “Android teardown”surfaceDestroyed fires on the UI thread while the render thread may be
mid-frame, and Android requires that it not return until nothing touches the
surface. The JNI layer blocks it until the render loop has finished the
in-flight frame and unbound the surface, and only then releases the native
window. Skipping this yields intermittent EGL errors and native crashes on
rotation or backgrounding that no functional test catches reliably. The core is
untouched — it is pure host plumbing, consistent with the render-thread rule.
midi-sink 2.0.0-rc.5 · AGPL-3.0 · source
