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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ qrc_*.cpp
src/*/mocs/*
src/*/objs/*
src/*/qrc/*
*_plugin_import.cpp
tests/target_wrapper.sh

# Shadow build directory
build/

# Folders not to be included
html/
Expand Down
Binary file added resources/images/icons/syphon-white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions resources/interface.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<file alias="add-video">images/icons/add_movie_w.png</file>
<file alias="add-camera">images/icons/add_camera_w.png</file>
<file alias="add-color">images/icons/add_paint_w.png</file>
<file alias="add-syphon">images/icons/syphon-white.png</file>
<file alias="add-mesh">images/shapes/add_quad.png</file>
<file alias="add-triangle">images/shapes/add_triangle.png</file>
<file alias="add-ellipse">images/shapes/add_circle.png</file>
Expand Down
7 changes: 6 additions & 1 deletion resources/macOS/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/><key>NOTE</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>MapMap uses the camera to display a live camera feed as a video-mapping source.</string>
<key>NSMicrophoneUsageDescription</key>
<string>MapMap may access the microphone when capturing audio from a camera device.</string>
<key>NOTE</key>
<string>This file need to be update at every release</string>

</dict>
Expand Down
7 changes: 7 additions & 0 deletions src/app/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@

#include "MetaObjectRegistry.h"

#ifdef Q_OS_MAC
#include "Syphon.h"
#endif

#include <stdlib.h>

MM_USE_NAMESPACE
Expand Down Expand Up @@ -40,6 +44,9 @@ void initRegistry()
registry.add<Video>();
registry.add<Image>();
registry.add<Color>();
#ifdef Q_OS_MAC
registry.add<Syphon>();
#endif

// Layers (formerly Mappings).
registry.add<TextureLayer>();
Expand Down
12 changes: 12 additions & 0 deletions src/core/CameraImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,30 @@ CameraImpl::CameraImpl()
}

CameraImpl::~CameraImpl()
{
freeResources();
}

void CameraImpl::freeResources()
{
if (_camera)
_camera->stop();
delete _camera;
_camera = nullptr;
delete _captureSession;
_captureSession = nullptr;
delete _cameraSurface;
_cameraSurface = nullptr;
VideoImpl::freeResources();
}

bool CameraImpl::loadMovie(const QString &deviceId)
{
VideoImpl::loadMovie(deviceId);

// Release any previously-opened device before re-acquiring.
freeResources();

// Find the camera device matching the given ID.
QCameraDevice dev;
for (const QCameraDevice& d : QMediaDevices::videoInputs()) {
Expand Down
4 changes: 4 additions & 0 deletions src/core/CameraImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ class CameraImpl : public VideoImpl
bool hasBits() const override { return _cameraSurface && _cameraSurface->isActive(); }
bool bitsHaveChanged() const override { return true; }

protected:
// Stops and releases the capture device (called by unloadMovie() and dtor).
void freeResources() override;

private:
QCamera *_camera;
QMediaCaptureSession *_captureSession;
Expand Down
7 changes: 5 additions & 2 deletions src/core/CameraSurface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,13 @@ void CameraSurface::onVideoFrameChanged(const QVideoFrame& frame)
if (img.isNull())
return;

#ifdef Q_OS_WIN
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
// Qt 6 delivers upright, top-left-origin frames here — the same as the
// video-file path (VideoPlayerImpl), which shares the OpenGL upload — so no
// flip is needed. The old flip made macOS camera sources appear upside-down.
_temporaryImage = img;
#else
// Straighten the image for OpenGL (bottom-left origin convention).
// Linux: historical orientation fix (kept; adjust if cameras look flipped).
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
_temporaryImage = img.mirrored(true, false).transformed(QTransform().rotate(180));
Expand Down
4 changes: 4 additions & 0 deletions src/core/Commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,10 @@ void RemoveSourceCommand::undo()
if (!_source.isNull())
{
MappingManager& manager = _mainWindow->getMappingManager();

// Re-open any device/player released when the source was removed.
_source->reacquireResources();

uid lastId = manager.addSource(_source);
_mainWindow->addSourceItem(lastId, _source->getIcon(), _source->getName());

Expand Down
37 changes: 35 additions & 2 deletions src/core/MappingManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@ Source::ptr MappingManager::getSourceByName(QString name)
return _getElementByName(sourceVector, name);
}

QString MappingManager::generateUniqueSourceName(const QString& baseName) const
{
auto isTaken = [this](const QString& candidate) {
for (const Source::ptr& source : sourceVector)
if (source->getName() == candidate)
return true;
return false;
};

if (!isTaken(baseName))
return baseName;

for (int n = 2; ; ++n)
{
const QString candidate = QString("%1 %2").arg(baseName).arg(n);
if (!isTaken(candidate))
return candidate;
}
}

QVector<Source::ptr> MappingManager::getSourcesByNameRegExp(QString namePattern)
{
return _getElementsByNameRegExp(sourceVector, namePattern);
Expand Down Expand Up @@ -99,12 +119,25 @@ bool MappingManager::removeSource(uid sourceId)
removeLayer(it.key());
}

// Remove source.
// Remove source. Its lifetime is managed by QSharedPointer: dropping the
// manager's references here is enough. The source may still be held by the
// undo stack (so it can be restored), and is freed once the last shared
// pointer is released.
//
// NOTE: do NOT call source->~Source() explicitly. Because ~Source() is
// virtual, that runs the full destructor chain on an object the shared
// pointers still own, so the object is destroyed a second time when the
// last reference is released — a double-free that crashes on quit/undo
// (notably with Syphon sources, which carry extra state).
int idx = sourceVector.lastIndexOf(source);
Q_ASSERT(idx != -1);
sourceVector.remove(idx);
sourceMap.remove(sourceId);
source->~Source(); // FIX ME: Explicit call of source destructor in order add Camera more than once

// Release the source's heavy external resources (e.g. a camera device)
// immediately, even though the object itself may live on in the undo stack.
// reacquireResources() restores them if the removal is undone.
source->releaseResources();
return true;
}
else
Expand Down
6 changes: 6 additions & 0 deletions src/core/MappingManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ class MappingManager
/// Returns mapping with given name (first match).
Source::ptr getSourceByName(QString name);

/**
* Returns a source name based on baseName that is not already used by any
* source, appending " 2", " 3", ... when needed (e.g. "Syphon", "Syphon 2").
*/
QString generateUniqueSourceName(const QString& baseName) const;

/// Returns all mappings with given regexp.
QVector<Source::ptr> getSourcesByNameRegExp(QString namePattern);

Expand Down
4 changes: 3 additions & 1 deletion src/core/ProjectReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ Source::ptr ProjectReader::parseSource(const QJsonObject& obj)
}
else
{
_errorString = QObject::tr("Unable to create source of type '%1'.").arg(className);
// Unknown source type (e.g. a macOS-only Syphon source opened on another
// platform). Skip it with a warning rather than failing the whole load.
qWarning() << "Skipping unsupported source of type" << className;
return Source::ptr();
}
}
Expand Down
34 changes: 34 additions & 0 deletions src/core/Source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,24 @@ namespace mmp {

UidAllocator Source::allocator;

QVector<GLuint> Texture::_orphanedTextures;
QMutex Texture::_orphanedTexturesMutex;

void Texture::orphanTexture(GLuint id)
{
QMutexLocker locker(&_orphanedTexturesMutex);
_orphanedTextures.append(id);
}

void Texture::deleteOrphanedTextures()
{
QMutexLocker locker(&_orphanedTexturesMutex);
if (_orphanedTextures.isEmpty())
return;
glDeleteTextures((GLsizei) _orphanedTextures.size(), _orphanedTextures.constData());
_orphanedTextures.clear();
}

void Texture::update()
{
if (textureId == 0)
Expand Down Expand Up @@ -250,6 +268,22 @@ void Video::rewind()
_impl->resetMovie();
}

void Video::releaseResources()
{
// Release the capture device / media player but keep the impl object, so the
// device is freed as soon as the source is removed (e.g. to let the same
// camera be re-opened) without invalidating the source. See reacquireResources().
if (_impl)
_impl->unloadMovie();
}

void Video::reacquireResources()
{
// Re-open using the stored URI/device id (set by loadMovie()).
if (_impl)
_impl->build();
}

void Video::lockMutex() {
_impl->lockMutex();
}
Expand Down
38 changes: 34 additions & 4 deletions src/core/Source.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include <QColor>
#include <QElapsedTimer>
#include <QMutex>
#include <QVector>

#if __APPLE__
#include <OpenGL/gl.h>
Expand Down Expand Up @@ -70,7 +71,7 @@ class Source : public Element
public:

enum SourceType {
Video, Image, Color
Video, Image, Color, Syphon
};

typedef QSharedPointer<Source> ptr;
Expand Down Expand Up @@ -100,6 +101,14 @@ class Source : public Element
/// Rewinds.
virtual void rewind() {}

/// Releases heavy external resources (capture devices, players, network
/// clients) while keeping the object valid — e.g. when the source is removed
/// but kept in the undo history. Reversible via reacquireResources().
virtual void releaseResources() {}

/// Re-acquires resources released by releaseResources() (e.g. on undo).
virtual void reacquireResources() {}

/// Locks mutex (default = no effect).
virtual void lockMutex() {}

Expand Down Expand Up @@ -169,12 +178,22 @@ class Texture : public Source

public:
virtual ~Texture() {
// TODO: this needs to be fixed: it will not work unless it is executed from within a GL context
// see issue #229
// A Texture is usually destroyed (e.g. when its source is removed) when no
// GL context is current, so we cannot call glDeleteTextures here (issue
// #229). Instead the id is queued and freed by deleteOrphanedTextures(),
// which the renderer calls while a context is current.
if (textureId != 0)
glDeleteTextures(1, &textureId);
orphanTexture(textureId);
}

/// Frees textures queued by destroyed Texture objects. MUST be called with a
/// current GL context (the renderer calls it at the start of painting).
static void deleteOrphanedTextures();

protected:
/// Queues a texture id for deferred deletion (see issue #229).
static void orphanTexture(GLuint id);

public:
virtual void update();

Expand Down Expand Up @@ -220,6 +239,12 @@ class Texture : public Source
protected:
// Lists QProperties that should NOT be parsed automatically.
virtual QList<QString> _propertiesSpecial() const { return Source::_propertiesSpecial() << "x" << "y"; }

private:
// Texture ids whose Texture objects were destroyed without a current GL
// context; freed later by deleteOrphanedTextures(). See issue #229.
static QVector<GLuint> _orphanedTextures;
static QMutex _orphanedTexturesMutex;
};

/**
Expand Down Expand Up @@ -321,6 +346,11 @@ class Video : public Texture
/// Rewinds.
virtual void rewind();

/// Releases the capture device / player (keeps the object valid).
virtual void releaseResources();
/// Re-opens the capture device / player released by releaseResources().
virtual void reacquireResources();

/// Locks mutex (default = no effect).
virtual void lockMutex();

Expand Down
Loading
Loading