Syphon input source (macOS) + camera & source-lifecycle fixes#618
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_SYPHONqmake define; projects with a Syphon source still load on Linux/Windows (skipped with a warning).Feature: Syphon input source
Syphonsource (aTexture) backed by an Objective-C++ client wrapper (SyphonOpenGLClient+SyphonServerDirectory). Frames are blitted on the GPU from Syphon'sGL_TEXTURE_RECTANGLEinto the source's ownGL_TEXTURE_2Dvia an FBO — zero-copy, noglReadPixels, no per-frame re-upload.Syphon,Syphon 2, …). Uses a white version of the official Syphon icon.Syphon.framework(BSD-2) underthird_party/macos/, embedded in the app bundle via@rpath.Fixes (app-wide, not just Syphon)
MappingManager::removeSource()called the virtual~Source()explicitly, double-destroying the (stillQSharedPointer-owned) object. Removed it;QSharedPointerowns the lifetime.~Texture()no longer callsglDeleteTexturesdirectly; ids are queued and freed during paint.NSCameraUsageDescription. Wired the project's realInfo.plist(propercom.artpluscode.MapMapid + version) viaQMAKE_INFO_PLISTand added the camera/microphone usage strings.removeSource()is also no longer called insideQ_ASSERT(which is compiled out in release builds).Verification
@rpath.Notes / follow-ups
🤖 Generated with Claude Code