Skip to content

Threading 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_midi may 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_probe is 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.

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.

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.

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 hostmpe state, 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 hostmpe state and is the only caller of sumi_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.

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