diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 35c1c939..a9a4b6a8 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,15 +1,5 @@ -# These are supported funding model platforms - -github: [ @artpluscode, @mapmapteam ] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -polar: # Replace with a single Polar username -buy_me_a_coffee: # Replace with a single Buy Me a Coffee username -thanks_dev: # Replace with a single thanks.dev username -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] +# Funding for MapMap. +# GitHub Sponsors (the project account) is the primary destination. +# Open Collective is kept for organisations that need a receipt/invoice. +github: [mapmapteam] +open_collective: mapmap diff --git a/src/core/MM.cpp b/src/core/MM.cpp index f81c7bf2..e5b3321f 100644 --- a/src/core/MM.cpp +++ b/src/core/MM.cpp @@ -28,6 +28,15 @@ const QString MM::COPYRIGHT_OWNERS = "Alexandre Quessy, Sofian Audry, Dame Diong const QString MM::ORGANIZATION_NAME = "MapMap"; const QString MM::ORGANIZATION_DOMAIN = "artpluscode.com"; const QString MM::WEBSITE_URL = "https://mapmapteam.github.io"; +// Commercial services landing page operated by Art Plus Code, the company that +// sponsors MapMap. This is the high-intent lead-generation entry point for paid +// work (installations, custom development, integration, training, support). +const QString MM::SERVICES_URL = "https://www.artpluscode.com/en/services/video-mapping-projection/"; +// Primary, consolidated destination for project donations. GitHub Sponsors is +// the simplest path for the typical donor (already on GitHub); the project's +// Open Collective (opencollective.com/mapmap) remains for organisations that +// need a receipt. +const QString MM::DONATE_URL = "https://github.com/sponsors/mapmapteam"; const QString MM::FILE_EXTENSION = "mmp"; const QString MM::VIDEO_FILES_FILTER = "*.mov *.mp4 *.avi *.ogg *.ogv *.mpeg *.mpeg1 *.mpeg4 *.mpg *.mpg2 *.mp2 *.mjpq *.mjp *.wmv *.webm *sock"; const QString MM::IMAGE_FILES_FILTER = "*.jpg *.jpeg *.gif *.png *.tiff *.tif *.bmp"; diff --git a/src/core/MM.h b/src/core/MM.h index d0a1d5cf..ea27955f 100644 --- a/src/core/MM.h +++ b/src/core/MM.h @@ -56,6 +56,8 @@ class MM static const QString ORGANIZATION_NAME; static const QString ORGANIZATION_DOMAIN; static const QString WEBSITE_URL; + static const QString SERVICES_URL; + static const QString DONATE_URL; static const QString FILE_EXTENSION; static const QString VIDEO_FILES_FILTER; static const QString IMAGE_FILES_FILTER; diff --git a/src/core/SyphonOutput.h b/src/core/SyphonOutput.h new file mode 100644 index 00000000..0b67e502 --- /dev/null +++ b/src/core/SyphonOutput.h @@ -0,0 +1,76 @@ +/* + * SyphonOutput.h + * + * (c) 2026 Alexandre Quessy -- alexandre(@)quessy(.)net + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SYPHON_OUTPUT_H_ +#define SYPHON_OUTPUT_H_ + +#include + +// Syphon is a macOS-only inter-application video-sharing framework. +#ifdef HAVE_SYPHON + +#include + +namespace mmp { + +// Objective-C++ implementation (SyphonOpenGLServer wrapper), defined in +// SyphonServerImpl.mm. Kept opaque so this header stays pure C++. +class SyphonServerImpl; + +/** + * Publishes MapMap's rendered output composition as a Syphon server, so other + * macOS applications can receive it (a "virtual projector"). Opt-in: nothing is + * published until setEnabled(true). + * + * publishCurrentFramebuffer() must be called from the output canvas's GL + * context while it is current (e.g. inside QPainter::beginNativePainting()), + * right after the clean composition has been rendered. + */ +class SyphonOutput +{ +public: + SyphonOutput(); + ~SyphonOutput(); + + void setEnabled(bool on); + bool isEnabled() const { return _enabled; } + + /// Human-readable server name shown to Syphon clients (default "MapMap"). + void setServerName(const QString& name); + QString serverName() const { return _serverName; } + + /** + * Publishes the contents of the currently-bound framebuffer (queried from GL, + * along with the viewport size). Must be called from the output canvas's GL + * context while current — inside QPainter::beginNativePainting() — right after + * the composition is rendered. No-op unless enabled. Safe to call every frame. + */ + void publishCurrentFramebuffer(); + +private: + bool _enabled; + QString _serverName; + SyphonServerImpl* _impl; +}; + +} + +#endif // HAVE_SYPHON + +#endif /* SYPHON_OUTPUT_H_ */ diff --git a/src/core/SyphonServerImpl.mm b/src/core/SyphonServerImpl.mm new file mode 100644 index 00000000..263cca43 --- /dev/null +++ b/src/core/SyphonServerImpl.mm @@ -0,0 +1,197 @@ +/* + * SyphonServerImpl.mm + * + * (c) 2026 Alexandre Quessy -- alexandre(@)quessy(.)net + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Objective-C++ glue publishing MapMap's output composition as a Syphon + * server. Compiled only on macOS (gated in core.pri); manual reference + * counting (no ARC). + * + * Each frame we MSAA-resolve/blit the output canvas's framebuffer into the + * server's own FBO (bindToDrawFrameOfSize/unbindAndPublish), so the work is + * entirely on the GPU. + */ + +#include "SyphonOutput.h" + +#ifdef HAVE_SYPHON + +#include + +#import +#import +#import +#import + +#import + +namespace mmp { + +static NSString* qStringToNS(const QString& s) +{ + return [NSString stringWithUTF8String:(s.isEmpty() ? "MapMap" : s.toUtf8().constData())]; +} + +class SyphonServerImpl +{ +public: + SyphonServerImpl() + : _server(nil), _serverCtx(NULL), _failed(false), _logged(false) {} + + ~SyphonServerImpl() { teardown(); } + + void teardown() + { + if (_server != nil) + { + [_server stop]; + [_server release]; + _server = nil; + } + _serverCtx = NULL; + _failed = false; + _logged = false; + } + + void setName(const QString& name) + { + @autoreleasepool { + if (_server != nil) + _server.name = qStringToNS(name); + } + } + + void publish(GLuint sourceFbo, int w, int h, const QString& name) + { + if (w <= 0 || h <= 0) + return; + + CGLContextObj cgl = CGLGetCurrentContext(); + if (cgl == NULL) + return; + + @autoreleasepool { + // (Re)create the server if needed, or if the GL context changed. + if (_server != nil && _serverCtx != cgl) + teardown(); + + if (_server == nil) + { + if (_failed) + return; // Already failed on this context; don't retry every frame. + + _server = [[SyphonOpenGLServer alloc] initWithName:qStringToNS(name) + context:cgl + options:nil]; + if (_server == nil) + { + _failed = true; + NSLog(@"[SyphonOutput] SyphonOpenGLServer init FAILED for context %p " + @"(legacy OpenGL output may be unsupported on this GPU).", cgl); + return; + } + _serverCtx = cgl; + NSLog(@"[SyphonOutput] server started: %@ context %p", qStringToNS(name), cgl); + } + + glGetError(); // clear any pre-existing error + + // Bind the server's FBO and blit our composition into it. A same-size + // blit from the (multisampled) widget FBO resolves MSAA in one step. + if ([_server bindToDrawFrameOfSize:NSMakeSize(w, h)]) + { + GLint prevRead = 0; + glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING_EXT, &prevRead); + + glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, sourceFbo); + glBlitFramebufferEXT(0, 0, w, h, 0, 0, w, h, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, (GLuint) prevRead); + + [_server unbindAndPublish]; // restores the previously-bound FBO + flushes + + if (!_logged) + { + _logged = true; + NSLog(@"[SyphonOutput] first publish %dx%d hasClients=%d glError=0x%x", + w, h, (int) [_server hasClients], (unsigned) glGetError()); + } + } + else if (!_logged) + { + _logged = true; + NSLog(@"[SyphonOutput] bindToDrawFrameOfSize FAILED at %dx%d", w, h); + } + } + } + +private: + SyphonOpenGLServer* _server; + CGLContextObj _serverCtx; // context the server was created with + bool _failed; // init failed on _serverCtx; stop retrying + bool _logged; // one-shot diagnostics +}; + +// --------------------------------------------------------------------------- +// SyphonOutput (C++ side). +// --------------------------------------------------------------------------- + +SyphonOutput::SyphonOutput() + : _enabled(false), + _serverName(QStringLiteral("MapMap")), + _impl(new SyphonServerImpl()) +{ +} + +SyphonOutput::~SyphonOutput() +{ + delete _impl; +} + +void SyphonOutput::setEnabled(bool on) +{ + if (on == _enabled) + return; + _enabled = on; + // The server is created lazily on the next publish (needs a GL context); when + // disabling, tear it down so it disappears from the Syphon directory. + if (!on && _impl) + _impl->teardown(); +} + +void SyphonOutput::setServerName(const QString& name) +{ + _serverName = name.isEmpty() ? QStringLiteral("MapMap") : name; + if (_impl) + _impl->setName(_serverName); +} + +void SyphonOutput::publishCurrentFramebuffer() +{ + if (!_enabled || !_impl) + return; + + GLint fbo = 0; + GLint viewport[4] = { 0, 0, 0, 0 }; + glGetIntegerv(GL_FRAMEBUFFER_BINDING_EXT, &fbo); + glGetIntegerv(GL_VIEWPORT, viewport); + + _impl->publish((GLuint) fbo, viewport[2], viewport[3], _serverName); +} + +} + +#endif // HAVE_SYPHON diff --git a/src/core/core.pri b/src/core/core.pri index 98f69c88..26e25594 100644 --- a/src/core/core.pri +++ b/src/core/core.pri @@ -40,6 +40,6 @@ SOURCES += $$PWD/Commands.cpp \ # Syphon (macOS-only) inter-application video sharing. macx { - HEADERS += $$PWD/Syphon.h - OBJECTIVE_SOURCES += $$PWD/SyphonImpl.mm + HEADERS += $$PWD/Syphon.h $$PWD/SyphonOutput.h + OBJECTIVE_SOURCES += $$PWD/SyphonImpl.mm $$PWD/SyphonServerImpl.mm } diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp index ca13da11..d13f8ad0 100644 --- a/src/gui/AboutDialog.cpp +++ b/src/gui/AboutDialog.cpp @@ -104,6 +104,14 @@ void AboutDialog::createAboutTab() // Visit our website for more information QString projectWebsiteText = "

" + tr("See the ") + QString("").arg(MM::WEBSITE_URL) + tr("%1 website").arg(MM::APPLICATION_NAME) + " for more information on this software.

"; + // Sponsor + commercial services. MapMap is the funnel; Art Plus Code is the + // business. Keep the two calls to action distinct (services vs. donation). + QString servicesText = "

" + tr("%1 is developed and sponsored by Art Plus Code. " + "Need a custom video mapping installation, a new feature, integration or training? ") + .arg(MM::APPLICATION_NAME) + + QString("").arg(MM::SERVICES_URL) + tr("Hire us") + ".

"; + QString supportText = "

" + tr("Enjoying %1? ").arg(MM::APPLICATION_NAME) + + QString("").arg(MM::DONATE_URL) + tr("Support the project") + ".

"; // Append texts QString aboutText; @@ -112,6 +120,8 @@ void AboutDialog::createAboutTab() aboutText.append(licenseNoticeText); aboutText.append(aboutMappingText); aboutText.append(projectWebsiteText); + aboutText.append(servicesText); + aboutText.append(supportText); // Set about text aboutTextBrowser->setText(aboutText); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 254e2f07..c1414c62 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -2248,6 +2248,24 @@ void MainWindow::createActions() connect(displayTestSignalAction, SIGNAL(toggled(bool)), outputWindow, SLOT(setDisplayTestSignal(bool))); // connect(displayTestSignalAction, SIGNAL(toggled(bool)), this, SLOT(update())); +#if defined(HAVE_SYPHON) && defined(SYPHON_OUTPUT_EXPERIMENTAL) + // Publish the output as a Syphon server (opt-in, macOS only). + // EXPERIMENTAL / DISABLED — see the SYPHON_OUTPUT_EXPERIMENTAL note in src/src.pri. + publishSyphonOutputAction = new QAction(tr("&Publish Syphon Output"), this); + publishSyphonOutputAction->setToolTip(tr("Publish the output composition as a Syphon server other apps can receive")); + publishSyphonOutputAction->setIconVisibleInMenu(false); + publishSyphonOutputAction->setCheckable(true); + publishSyphonOutputAction->setChecked(false); + publishSyphonOutputAction->setShortcutContext(Qt::ApplicationShortcut); + addAction(publishSyphonOutputAction); + connect(publishSyphonOutputAction, SIGNAL(toggled(bool)), outputWindow, SLOT(setSyphonOutputEnabled(bool))); + connect(publishSyphonOutputAction, &QAction::toggled, this, [](bool on) { + QSettings s; s.setValue("publishSyphonOutput", on); + }); + // Restore the persisted state (this fires the connections above). + publishSyphonOutputAction->setChecked(settings.value("publishSyphonOutput", false).toBool()); +#endif + // Toggle display of Undo History displayUndoHistoryAction = new QAction(tr("Display &Undo History"), this); displayUndoHistoryAction->setShortcut(Qt::ALT | Qt::Key_U); @@ -2347,9 +2365,14 @@ void MainWindow::createActions() // Bug report bugReportAction = new QAction(tr("Report an issue"), this); connect(bugReportAction, SIGNAL(triggered()), this, SLOT(reportBug())); - // Support - supportAction = new QAction(tr("Technical support"), this); - connect(supportAction, SIGNAL(triggered()), this, SLOT(technicalSupport())); + // Professional services & custom development by Art Plus Code (sponsor). + servicesAction = new QAction(tr("Professional services && custom development…"), this); + servicesAction->setToolTip(tr("Hire Art Plus Code for video mapping installations, custom features, integration and training")); + connect(servicesAction, SIGNAL(triggered()), this, SLOT(professionalServices())); + // Support the project (donations, consolidated on Open Collective). + donateAction = new QAction(tr("Support the project (donate)…"), this); + donateAction->setToolTip(tr("Help fund MapMap's ongoing development")); + connect(donateAction, SIGNAL(triggered()), this, SLOT(donate())); // Documentation docAction = new QAction(tr("Documentation"), this); connect(docAction, SIGNAL(triggered()), this, SLOT(documentation())); @@ -2467,6 +2490,9 @@ void MainWindow::createMenus() viewMenu->addSeparator(); viewMenu->addAction(outputFullScreenAction); viewMenu->addAction(displayTestSignalAction); +#if defined(HAVE_SYPHON) && defined(SYPHON_OUTPUT_EXPERIMENTAL) + viewMenu->addAction(publishSyphonOutputAction); +#endif viewMenu->addAction(displayControlsAction); viewMenu->addAction(displaySourceControlsAction); outputScreenMenu = viewMenu->addMenu(tr("&Output screen")); @@ -2505,9 +2531,12 @@ void MainWindow::createMenus() helpMenu = menuBar->addMenu(tr("&Help")); helpMenu->addAction(docAction); helpMenu->addAction(shortcutAction); - helpMenu->addAction(feedbackAction); - helpMenu->addAction(supportAction); + helpMenu->addSeparator(); helpMenu->addAction(bugReportAction); + helpMenu->addAction(feedbackAction); + helpMenu->addSeparator(); + helpMenu->addAction(servicesAction); + helpMenu->addAction(donateAction); helpMenu->addSeparator(); helpMenu->addAction(aboutAction); diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index f70d0011..2a2a639b 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -158,9 +158,15 @@ private slots: void sendFeedback() { QDesktopServices::openUrl(QUrl("mailto:mapmap@artpluscode.com")); } - // Technical support - void technicalSupport() { - QDesktopServices::openUrl(QUrl(MM::WEBSITE_URL)); + // Professional services and custom development by Art Plus Code, the company + // that sponsors MapMap (video mapping installations, custom features, + // integration, training and paid support). + void professionalServices() { + QDesktopServices::openUrl(QUrl(MM::SERVICES_URL)); + } + // Support the project with a donation (consolidated on Open Collective). + void donate() { + QDesktopServices::openUrl(QUrl(MM::DONATE_URL)); } // Report an issues void reportBug() { @@ -452,6 +458,7 @@ public slots: QAction *displayControlsAction; QAction *displaySourceControlsAction; QAction *displayTestSignalAction; + QAction *publishSyphonOutputAction; QAction *stickyVerticesAction; QAction *displayUndoHistoryAction; QAction *displayZoomToolAction; @@ -471,7 +478,8 @@ public slots: // help actions QAction *bugReportAction; - QAction *supportAction; + QAction *servicesAction; + QAction *donateAction; QAction *docAction; QAction *feedbackAction; QAction *shortcutAction; diff --git a/src/gui/OutputGLCanvas.cpp b/src/gui/OutputGLCanvas.cpp index cf6185b3..126bb6d8 100644 --- a/src/gui/OutputGLCanvas.cpp +++ b/src/gui/OutputGLCanvas.cpp @@ -46,8 +46,51 @@ void OutputGLCanvas::setSceneRectToViewportGeometry() setSceneRect(viewport()->geometry()); } +void OutputGLCanvas::setSyphonOutputEnabled(bool on) +{ +#ifdef HAVE_SYPHON + _syphonOutput.setEnabled(on); + // Repaint so the server starts/stops publishing promptly. + if (viewport()) + viewport()->update(); +#else + Q_UNUSED(on); +#endif +} + +bool OutputGLCanvas::isSyphonOutputEnabled() const +{ +#ifdef HAVE_SYPHON + return _syphonOutput.isEnabled(); +#else + return false; +#endif +} + +void OutputGLCanvas::setSyphonServerName(const QString& name) +{ +#ifdef HAVE_SYPHON + _syphonOutput.setServerName(name); +#else + Q_UNUSED(name); +#endif +} + void OutputGLCanvas::drawForeground(QPainter *painter , const QRectF &rect) { +#if defined(HAVE_SYPHON) && defined(SYPHON_OUTPUT_EXPERIMENTAL) + // Publish the clean composition (background + mappings, no editing overlays or + // test signal) to Syphon before the foreground is drawn. drawForeground runs + // after the background and all items, so the framebuffer holds the full frame. + // DISABLED — see the SYPHON_OUTPUT_EXPERIMENTAL note in src/src.pri. + if (_syphonOutput.isEnabled()) + { + painter->beginNativePainting(); + _syphonOutput.publishCurrentFramebuffer(); + painter->endNativePainting(); + } +#endif + QSettings settings; bool controlOnMouseOver = settings.value("showControlOnMouseOver", MM::SHOW_OUTPUT_ON_MOUSE_HOVER).toBool(); diff --git a/src/gui/OutputGLCanvas.h b/src/gui/OutputGLCanvas.h index a62c3883..8a53b47f 100644 --- a/src/gui/OutputGLCanvas.h +++ b/src/gui/OutputGLCanvas.h @@ -24,6 +24,10 @@ #include "MapperGLCanvas.h" +#ifdef HAVE_SYPHON +#include "SyphonOutput.h" +#endif + namespace mmp { class OutputGLCanvas: public MapperGLCanvas @@ -49,6 +53,12 @@ class OutputGLCanvas: public MapperGLCanvas _displayTestSignal = displayTestSignal; } + // Syphon output (macOS): publish the rendered output as a Syphon server. + // No-ops on platforms without Syphon support. + void setSyphonOutputEnabled(bool on); + bool isSyphonOutputEnabled() const; + void setSyphonServerName(const QString& name); + private: void _drawClassicTestSignal(QPainter* painter); void _drawPALTestCard(QPainter *painter); @@ -64,6 +74,10 @@ class OutputGLCanvas: public MapperGLCanvas QImage _ntscTestCard; bool _windowIsHovered; +#ifdef HAVE_SYPHON + SyphonOutput _syphonOutput; +#endif + protected: // overriden from QGlWidget: virtual void resizeGL(int width, int height); diff --git a/src/gui/OutputGLWindow.cpp b/src/gui/OutputGLWindow.cpp index 95858075..f86c3204 100644 --- a/src/gui/OutputGLWindow.cpp +++ b/src/gui/OutputGLWindow.cpp @@ -87,6 +87,11 @@ void OutputGLWindow::setCanvasDisplayCrosshair(bool crosshair) _resetCursor(_isFullScreen); } +void OutputGLWindow::setSyphonOutputEnabled(bool on) +{ + canvas->setSyphonOutputEnabled(on); +} + void OutputGLWindow::setDisplayTestSignal(bool displayTestSignal) { canvas->setDisplayTestSignal(displayTestSignal); diff --git a/src/gui/OutputGLWindow.h b/src/gui/OutputGLWindow.h index 15885181..862d5e6f 100644 --- a/src/gui/OutputGLWindow.h +++ b/src/gui/OutputGLWindow.h @@ -48,6 +48,7 @@ public slots: void setFullScreen(bool fullScreen); void setCanvasDisplayCrosshair(bool crosshair); void setDisplayTestSignal(bool displayTestSignal); + void setSyphonOutputEnabled(bool on); signals: void closed(); diff --git a/src/src.pri b/src/src.pri index bf4ef3e5..466f1c89 100644 --- a/src/src.pri +++ b/src/src.pri @@ -55,6 +55,15 @@ macx { syphon_framework.path = Contents/Frameworks QMAKE_BUNDLE_DATA += syphon_framework + # Syphon OUTPUT (publishing MapMap's output as a Syphon server) is EXPERIMENTAL + # and DISABLED by default: SyphonOpenGLServer cannot create its IOSurface + # texture in MapMap's legacy-OpenGL-over-Metal context on Apple Silicon (it + # floods "cannot create texture, Metal texture cache was released" and never + # publishes). The implementation is kept (SyphonOutput / SyphonServerImpl.mm + + # the OutputGLCanvas hook + the "Publish Syphon Output" menu item) but compiled + # out. Uncomment to build it back in for development. Tracked on the roadmap. + # DEFINES += SYPHON_OUTPUT_EXPERIMENTAL + # With Xcode Tools > 1.5, to reduce the size of your binary even more: # LIBS += -dead_strip # This tells qmake not to put the executable inside a bundle.