Phasion Live 1.10.0 — NDI fixed, displays steady
1.9.0's Phase D win (per-lane Syphon via Electron useSharedTexture) broke NDI as a side-effect: image.toBitmap() returns an empty buffer in shared-texture mode, so NDI receivers got either a still frame or all-black. 1.10.0 fixes NDI end-to-end and adds operator controls so the CPU cost only happens when a real consumer is actually watching.
Also: a Rust display sidecar for fullscreen on Sequoia (the menubar no longer leaks into your projector output), and a handful of polish fixes that surfaced during the NDI work.
📺 NDI — full rework
The headline regression from 1.9.0 is fixed, and the feature is substantially better than it was before.
Phase D NDI fast path
New native syphon.readbackOffscreenLaneBgra(lane) locks the lane's published IOSurface read-only and memcpys it into a Node Buffer. The IOSurface is MTLStorageModeShared, so this is a straight CPU copy from shared memory — no GPU stall, no Metal command-buffer flush. Cost: ~1.6 MB/frame at 480p, ~8 MB/frame at 1080p — only paid when NDI is enabled.
ndi.publishFrameLaneBgra now accepts a stride parameter so the addon can honor the IOSurface's padded row stride directly via line_stride_in_bytes, no tight-packing memcpy required.
Fix: shear at non-aligned widths
854×480 (the old 480p preset) ships with IOSurfaceBytesPerRow=3456 not the expected 3416 — IOSurface aligns to 64 bytes. NDI's BGRA pipeline skewed the image diagonally when xres didn't match stride/4. Two fixes:
1. The 480p preset is now 848×480 (16-pixel aligned, 0.7% aspect delta from true 16:9 — imperceptible). The custom-resolution dialog snaps widths to the nearest multiple of 16 with a friendly notice explaining why. 2. For any non-aligned widths that slip through, the readback passes paddedWidth = stride/4 to NDI so the receiver sees the visible content cleanly + a small black bar on the right.
Per-output NDI broadcast (opt-in)
The NDI panel now has three checkboxes — Out 1, Out 2, Out 3 — that control which lanes get an NDI sender. All three default to OFF: master Enable starts the framework, you opt into each output explicitly. Disabling an output completely stops that lane's sender (no readback, no publish, no mDNS announce — zero CPU). The receiver count (N) shows in green next to each output when consumers connect.
Receiver-count gate
When a lane has zero NDI receivers (get_no_connections polled every 30 frames), the paint bridge skips the IOSurface readback + publish entirely. With Resolume / OBS / Synesthesia subscribed, the lane publishes at full rate. Result on a 3-output rig with Resolume pulling Out 1 only:
| State | Lane 1 paint cost | Lanes 2/3 paint cost | |---|---|---| | NDI off | 1.3 ms | 1.3 ms | | NDI on, no receivers | 1.3 ms | 1.3 ms | | NDI on, Resolume on Out 1 | ~8 ms | 1.3 ms |
Operator visibility for phantom subscribers
Some apps (NDI Tools' Virtual Input system extension, NDI Studio Monitor) auto-subscribe to any source they discover via mDNS, even when nobody's actively viewing. From NDI's perspective these are legitimate receivers — connection count goes up, frames get encoded.
To make this visible:
- Receiver count
(N) next to each per-output checkbox tells you
immediately how many consumers are subscribed.
- A ↻ Restart Senders button destroys + recreates all senders
in one shot. Legitimate downstream tools (Resolume, OBS) reconnect within 1-2 s. Passive subscribers may not, so this is a useful diagnostic for "who is keeping CPU high?"
- The per-output checkbox is the lever: uncheck Out N to fully
stop work on that lane, whether the cause is a phantom or a real consumer you no longer need.
NDI Tally exposed
NDIlib_send_get_tally is now wired into the addon (ndi.getLaneTally(lane) returns {onProgram, onPreview, anyActive, available}). Currently surfaced in diagnostic logs only; not used as the publish gate because most consumers (Synesthesia, OBS in non-broadcast mode) don't set tally. Available for future work.
🖥️ Display sidecar — native fullscreen on Sequoia
Electron 41's setSimpleFullScreen leaves the macOS menubar visible on Sequoia, breaking a dark venue projector setup. The fix is a separate Rust sidecar binary (phasion-display) — winit + wgpu + syphon subscription — that opens an NSWindow in native fullscreen on a chosen display, subscribes to the lane's Syphon source, and paints it edge-to-edge.
In the Output Preview row, ⛶ Send to Display now spawns the sidecar. The Settings → Output Windows panel has per-output display dropdowns: pick a display per output, then ⛶ Send to N Displays spawns sidecars in parallel for everything you assigned.
The sidecar's CPU cost is small (subscribes to an existing Syphon IOSurface, no encoding) and it's bundled in the .app via extraResources so it just works after install.
Fix: stale display assignments inflated the "Send to N" count
If you assigned a display, then unplugged it, the saved assignment lingered — the per-row dropdown correctly showed "(no display)" but the aggregate button counted the stale entry ("Send to 2 Displays" when only one was assignable). Now the cleanup function filters out assignments whose display ID isn't in screen.getAllDisplays() and persists the cleanup back to config.
🎚️ Syphon settings — simplified
The Syphon settings panel had two confusing toggles:
- Visualizer "Enable" — controlled the legacy single-server
Syphon path that Phase D's per-lane servers replaced. Operator observation: "I disable visualizer and it still outputs anyway" — because per-lane (phasion1/output1, etc.) keeps publishing regardless. The toggle was vestigial.
- Media Capture "Enable" — duplicated the Enable toggle in the
📹 Media Capture menu, which already starts/stops Syphon automatically via the media-capture lifecycle.
Both toggles are now hidden. The Visualizer subsection shows an explanation of the per-lane auto-publish behavior; the Media Capture subsection keeps the server-name input. Server names persist exactly as before.
🛠️ Other fixes
OSRSharedTextureNotReleased warning — gracefully suppressed
during normal operation. The deferred-release ring from 1.9.0 already handles this correctly; the warning was a Chromium dev notice that doesn't affect frame delivery.
- Per-lane diagnostic counters in the offscreen paint bridge
stats log (gate=N readback=N skipped=N conn=N tally=on/off) — always-on, useful for triaging perf reports.
Kill switches
PHASION_SHARED_TEXTURE=0 — fall back to 1.8.x's CPU bitmap path
if Phase D regresses on a future macOS / Electron release.
- Per-lane NDI checkboxes default OFF on fresh install. Existing
users keep their saved state.
Known limitations
- NDI Virtual Input extension keeps subscribing to any
discovered NDI source even when not actively used. Phasion can't tell the OS extension apart from a legitimate Resolume layer. The per-output checkboxes are the workaround; long-term users may prefer to disable the extension in System Settings → Privacy & Security → Login Items & Extensions → Camera Extensions.
- NDI tally isn't used as the publish gate because Synesthesia
and OBS consume NDI streams without setting program/preview tally. Available via diagnostic logs.