Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 5 additions & 15 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions src/core/MM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 2 additions & 0 deletions src/core/MM.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
76 changes: 76 additions & 0 deletions src/core/SyphonOutput.h
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

#ifndef SYPHON_OUTPUT_H_
#define SYPHON_OUTPUT_H_

#include <QtGlobal>

// Syphon is a macOS-only inter-application video-sharing framework.
#ifdef HAVE_SYPHON

#include <QString>

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_ */
197 changes: 197 additions & 0 deletions src/core/SyphonServerImpl.mm
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
* 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 <QDebug>

#import <Foundation/Foundation.h>
#import <OpenGL/OpenGL.h>
#import <OpenGL/gl.h>
#import <OpenGL/glext.h>

#import <Syphon/SyphonOpenGLServer.h>

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
4 changes: 2 additions & 2 deletions src/core/core.pri
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
10 changes: 10 additions & 0 deletions src/gui/AboutDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ void AboutDialog::createAboutTab()
// Visit our website for more information
QString projectWebsiteText = "<p>" + tr("See the ") + QString("<a href=\"%1\">").arg(MM::WEBSITE_URL) +
tr("%1 website").arg(MM::APPLICATION_NAME) + "</a> for more information on this software.</p>";
// 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 = "<p>" + 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("<a href=\"%1\">").arg(MM::SERVICES_URL) + tr("Hire us") + "</a>.</p>";
QString supportText = "<p>" + tr("Enjoying %1? ").arg(MM::APPLICATION_NAME)
+ QString("<a href=\"%1\">").arg(MM::DONATE_URL) + tr("Support the project") + "</a>.</p>";

// Append texts
QString aboutText;
Expand All @@ -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);
Expand Down
Loading
Loading