Skip to content

Syphon input source (macOS) + camera & source-lifecycle fixes#618

Merged
aalex merged 11 commits into
devfrom
feat-syphon
Jun 23, 2026
Merged

Syphon input source (macOS) + camera & source-lifecycle fixes#618
aalex merged 11 commits into
devfrom
feat-syphon

Conversation

@aalex

@aalex aalex commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

Adds a Syphon input source so MapMap can receive live video shared by other macOS apps (Resolume, VDMX, MadMapper, openFrameworks/Processing, …), plus several source/camera lifecycle fixes uncovered along the way.

macOS-only, gated behind a HAVE_SYPHON qmake define; projects with a Syphon source still load on Linux/Windows (skipped with a warning).

Feature: Syphon input source

  • New Syphon source (a Texture) backed by an Objective-C++ client wrapper (SyphonOpenGLClient + SyphonServerDirectory). Frames are blitted on the GPU from Syphon's GL_TEXTURE_RECTANGLE into the source's own GL_TEXTURE_2D via an FBO — zero-copy, no glReadPixels, no per-frame re-upload.
  • UX: an "Add Syphon Source" toolbar/menu action with a live server picker (icons, empty state, auto-refresh), an editable server dropdown + status in the property panel, a "Respect source alpha" toggle, and auto-numbered default names (Syphon, Syphon 2, …). Uses a white version of the official Syphon icon.
  • Persisted server identity reconnects across launches and server restarts; a new mapping's input shape auto-fits to the first frame's resolution.
  • Vendored prebuilt Syphon.framework (BSD-2) under third_party/macos/, embedded in the app bundle via @rpath.

Fixes (app-wide, not just Syphon)

  • Crash on quit/undo after removing a sourceMappingManager::removeSource() called the virtual ~Source() explicitly, double-destroying the (still QSharedPointer-owned) object. Removed it; QSharedPointer owns the lifetime.
  • Texture deleted without a GL context (Deletion of Texture paints will cause memory problems because glDeleteTextures() is called out of GL context #229)~Texture() no longer calls glDeleteTextures directly; ids are queued and freed during paint.
  • Camera showed no video — the bundle had no NSCameraUsageDescription. Wired the project's real Info.plist (proper com.artpluscode.MapMap id + version) via QMAKE_INFO_PLIST and added the camera/microphone usage strings.
  • Camera image upside-down — removed a stale frame flip; Qt 6 already delivers upright frames.
  • Camera device now released on source removal (not just on destruction), so the same camera can be re-opened in a session; removeSource() is also no longer called inside Q_ASSERT (which is compiled out in release builds).

Verification

  • Builds cleanly (Qt 6.11, arm64); embedded framework loads via @rpath.
  • Existing unit tests pass.
  • Syphon input verified live (Simple Server → MapMap), including reconnect and re-pointing.

Notes / follow-ups

  • Syphon output (publishing MapMap's composition as a Syphon server) is a planned next step.
  • Notarized macOS packaging (Developer ID) is still pending.

🤖 Generated with Claude Code

aalex added 11 commits June 23, 2026 01:43
Add a new Syphon source type so MapMap can receive live video shared by
other macOS applications (Resolume, VDMX, MadMapper, openFrameworks, ...).

- Syphon source (Texture subclass) backed by an Objective-C++ client
  wrapper (SyphonOpenGLClient + SyphonServerDirectory). Each frame is
  blitted on the GPU from Syphon's IOSurface-backed GL_TEXTURE_RECTANGLE
  into the source's own GL_TEXTURE_2D via an FBO: zero-copy, no glReadPixels
  and no per-frame re-upload.
- UX: "Add Syphon Source" toolbar/menu action with a live server picker
  (SyphonServerDialog), plus an editable server dropdown in the property
  panel (SyphonGui) to re-point a source. Default name from the server,
  falling back to an auto-numbered "Syphon N" (generateUniqueSourceName).
- Persisted server identity reconnects across launches and server restarts.
- macOS-only, gated behind a HAVE_SYPHON qmake define; projects containing a
  Syphon source load gracefully on other platforms (skipped with a warning).
- Vendored, prebuilt Syphon.framework (BSD 2-Clause) under third_party/macos,
  embedded in the app bundle via @rpath. See third_party/macos/README.md.
- Server picker shows each hosting application's icon (NSImage -> QImage).
- Add a per-source "Respect source alpha" toggle; default forces frames
  opaque (matching video/image sources) and is persisted with the project.
- Auto-fit a mapping's input shape to the Syphon source's real resolution
  once the first frame arrives, for shapes still at the default size (so
  mappings added before a frame had arrived snap to the right aspect).
Texture objects are usually destroyed (e.g. when a source is removed) while
no OpenGL context is current, so calling glDeleteTextures() in ~Texture() was
unsafe: depending on the driver it leaked the texture or could delete an
unrelated one in whatever context happened to be current.

Defer the deletion instead. ~Texture() now queues the id, and the renderer
frees all queued ids via Texture::deleteOrphanedTextures() at the start of
painting, when the shared GL context is guaranteed to be current. Affects all
texture sources (Video, Image, Syphon).
Replace the drawn placeholder glyph with Syphon's own icon (from
Syphon-Framework, BSD 2-Clause) for the "Add Syphon Source" toolbar/menu
action and for Syphon source items in the source list. Vendored as a Qt
resource (:/add-syphon).
The full-colour Syphon logo stood out among the white toolbar glyphs, so use
a white treatment of it (luminance->alpha, brightened) for the "Add Syphon
Source" button and Syphon source items, matching the rest of the toolbar.
The colour version is no longer referenced and is removed.
- Picker shows a live status line that doubles as an empty state: "No Syphon
  servers found yet..." when none are advertised, otherwise "%n server(s)
  available" (the list already refreshes automatically every second).
- Picker entries get a tooltip with the full identity (application, server
  name, UUID).
- Property panel now distinguishes "No server selected" from
  "Waiting for server..." and "Connected".
MappingManager::removeSource() explicitly called source->~Source(). Because
the destructor is virtual, that ran the full destructor chain on an object
still owned by QSharedPointer (e.g. retained by the undo stack for undo), so
the object was destroyed a second time when the last shared pointer was
released — a double-free that crashed in ~QObject() on quit (and on undo).

It was most visible with Syphon sources, which carry extra state (an
Objective-C client and a queued signal connection), but the bug applied to
any removed source.

Drop the explicit destructor call and let QSharedPointer own the lifetime;
removing the manager's references is enough.
Move the capture-device / media-player teardown into VideoImpl::freeResources()
(now overridden by CameraImpl and VideoPlayerImpl) so it can run without
destroying the impl object, and route the destructors and loadMovie() through
it (re-entrant, null-safe).

Add reversible Source::releaseResources()/reacquireResources() hooks. Video
releases its device via unloadMovie() and re-opens it via build() (which reuses
the stored URI/device id). MappingManager::removeSource() now releases a removed
source's device immediately — even though the source may linger in the undo
stack — and RemoveSourceCommand::undo() re-acquires it. This lets the same
camera be re-opened within a session, replacing the old explicit-destructor
hack that caused the double-free crash.

Also call removeSource() unconditionally instead of inside Q_ASSERT(), which is
compiled out in release builds and would otherwise skip the removal entirely.
macOS refuses camera access unless the app bundle's Info.plist declares
NSCameraUsageDescription, so QCamera never started and camera sources showed
no video (Syphon and video files were unaffected). The project already had a
proper custom Info.plist (com.artpluscode.MapMap identity, version 0.6.3) but
it was never wired into the build, so qmake generated a default plist without
any usage description.

Wire the custom Info.plist via QMAKE_INFO_PLIST and add NSCameraUsageDescription
(+ NSMicrophoneUsageDescription). macOS now prompts for camera permission on
first use; once granted, camera sources display video. This also gives the
bundle its real identifier and version.
CameraSurface flipped incoming frames (horizontal mirror + 180-degree
rotation, which nets to a vertical flip) as a historical OpenGL orientation
workaround. Qt 6 already delivers upright, top-left-origin frames — the same
as the video-file path (VideoPlayerImpl), which uses them untransformed
through the same GL upload — so the flip only made camera sources appear
upside-down. Use the frame as-is on macOS (and Windows, which was already
untransformed); Linux keeps its existing branch.
@aalex aalex self-assigned this Jun 23, 2026
@aalex aalex added this to the 1.0 milestone Jun 23, 2026
@aalex aalex merged commit 0f83327 into dev Jun 23, 2026
3 checks passed
@aalex aalex deleted the feat-syphon branch June 23, 2026 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant