From b80ba1d428befa604a87d498923512463b65d84c Mon Sep 17 00:00:00 2001 From: Bart Janssens Date: Sun, 1 Mar 2026 22:09:48 +0100 Subject: [PATCH 1/3] Add toLocalFIle method Issue https://github.com/JuliaGraphics/QML.jl/issues/232 --- wrap_qml.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wrap_qml.cpp b/wrap_qml.cpp index 5cbc1f3..2e8049c 100644 --- a/wrap_qml.cpp +++ b/wrap_qml.cpp @@ -590,7 +590,8 @@ JLCXX_MODULE define_julia_module(jlcxx::Module& qml_module) qml_module.add_type("QUrl") .constructor() - .method("toString", [] (const QUrl& url) { return url.toString(); }); + .method("toString", [] (const QUrl& url) { return url.toString(); }) + .method("toLocalFile", &QUrl::toLocalFile); qml_module.method("QUrlFromLocalFile", QUrl::fromLocalFile); auto qvar_type = qml_module.add_type("QVariant"); From 9f4e1c1f2d7a3661fb511344d837c7307d1ddfc2 Mon Sep 17 00:00:00 2001 From: Bart Janssens Date: Sat, 21 Mar 2026 10:04:50 +0100 Subject: [PATCH 2/3] Add ImageProvider and improve foreign thread support --- CMakeLists.txt | 4 +- application_manager.cpp | 10 ++- application_manager.hpp | 2 + foreign_thread_manager.cpp | 46 ++++++----- foreign_thread_manager.hpp | 7 +- julia_imageprovider.cpp | 81 +++++++++++++++++++ julia_imageprovider.hpp | 49 ++++++++++++ wrap_qml.cpp | 156 +++++++++++++++++++++++++++++++++++++ 8 files changed, 327 insertions(+), 28 deletions(-) create mode 100644 julia_imageprovider.cpp create mode 100644 julia_imageprovider.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f6e4502..2fa4e18 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.16.0) project(JlQML) -set(JlQML_VERSION 0.9.0) +set(JlQML_VERSION 0.10.0) message(STATUS "Project version: v${JlQML_VERSION}") set(CMAKE_MACOSX_RPATH 1) @@ -61,6 +61,8 @@ qt6_add_qml_module(jlqml julia_display.cpp julia_function.hpp julia_function.cpp + julia_imageprovider.hpp + julia_imageprovider.cpp julia_itemmodel.hpp julia_itemmodel.cpp julia_painteditem.hpp diff --git a/application_manager.cpp b/application_manager.cpp index 821d3c4..4b3131f 100644 --- a/application_manager.cpp +++ b/application_manager.cpp @@ -94,6 +94,14 @@ void ApplicationManager::exec() { app->exit(status); }); + + QTimer* main_timer = new QTimer(m_engine); + main_timer->setInterval(0); + QObject::connect(main_timer, &QTimer::timeout, [this]{ + m_event_hook(); + }); + main_timer->start(); + const int status = app->exec(); if (status != 0) { @@ -109,7 +117,7 @@ void ApplicationManager::add_import_path(std::string path) m_import_paths.push_back(path); } -ApplicationManager::ApplicationManager() +ApplicationManager::ApplicationManager() : m_event_hook(jl_get_function(m_qml_mod, "process_eventloop_updates")) { if(QProcessEnvironment::systemEnvironment().contains("QSG_RENDER_LOOP")) { diff --git a/application_manager.hpp b/application_manager.hpp index 361d389..178ec7e 100644 --- a/application_manager.hpp +++ b/application_manager.hpp @@ -12,6 +12,7 @@ #include #include "jlcxx/jlcxx.hpp" +#include "jlcxx/functions.hpp" #include "julia_api.hpp" @@ -59,6 +60,7 @@ class ApplicationManager QQmlEngine* m_engine = nullptr; QQmlContext* m_root_ctx = nullptr; std::vector m_import_paths; + jlcxx::JuliaFunction m_event_hook; }; } diff --git a/foreign_thread_manager.cpp b/foreign_thread_manager.cpp index 18eb6d2..e657a8f 100644 --- a/foreign_thread_manager.cpp +++ b/foreign_thread_manager.cpp @@ -10,6 +10,24 @@ ForeignThreadManager& ForeignThreadManager::instance() return instance; } +void ForeignThreadManager::gc_safe_enter(int& state) +{ + jl_ptls_t ptls = jl_current_task->ptls; + state = jl_gc_safe_enter(ptls); + #if (JULIA_VERSION_MAJOR * 100 + JULIA_VERSION_MINOR) > 111 + ptls->engine_nqueued++; + #endif +} + +void ForeignThreadManager::gc_safe_leave(int state) +{ + jl_ptls_t ptls = jl_current_task->ptls; + jl_gc_safe_leave(ptls, state); + #if (JULIA_VERSION_MAJOR * 100 + JULIA_VERSION_MINOR) > 111 + ptls->engine_nqueued--; + #endif +} + void ForeignThreadManager::add_thread(QThread *t) { m_mutex.lock(); @@ -41,23 +59,7 @@ void ForeignThreadManager::clear(QThread* main_thread) #define JL_GC_STATE_UNSAFE 0 #endif -void _gc_safe_enter(int& state) -{ - jl_ptls_t ptls = jl_current_task->ptls; - state = jl_gc_safe_enter(ptls); - #if (JULIA_VERSION_MAJOR * 100 + JULIA_VERSION_MINOR) > 111 - ptls->engine_nqueued++; - #endif -} -void _gc_safe_leave(int state) -{ - jl_ptls_t ptls = jl_current_task->ptls; - jl_gc_safe_leave(ptls, state); - #if (JULIA_VERSION_MAJOR * 100 + JULIA_VERSION_MINOR) > 111 - ptls->engine_nqueued--; - #endif -} void ForeignThreadManager::add_window(QQuickItem* item) { @@ -76,28 +78,24 @@ void ForeignThreadManager::add_window(QQuickItem* item) { // afterAnimating is the last signal sent before locking the main loop, so indicate // that the main loop is in a safe state for the Julia GC to run - _gc_safe_enter(m_main_gc_state); + gc_safe_enter(m_main_gc_state); }); item->connect(w, &QQuickWindow::beforeRendering, w, [&m_main_gc_state] () { // beforeRendering is the first signal sent after unlocking the main thread, so restore // the Julia GC state here. Queued connection so this is executed on the main thread - _gc_safe_leave(m_main_gc_state); + gc_safe_leave(m_main_gc_state); }, Qt::QueuedConnection); item->connect(w, &QQuickWindow::afterFrameEnd, w, [&m_render_gc_state] () { // After the rendering is done, the render thread may block for event handling, so we mark it as in a GC safe state // to prevent deadlock - _gc_safe_enter(m_render_gc_state); + gc_safe_enter(m_render_gc_state); }, Qt::DirectConnection); item->connect(w, &QQuickWindow::beforeFrameBegin, w, [&m_render_gc_state] () { // Reset GC state when the render thread might execute Julia code again - _gc_safe_leave(m_render_gc_state); + gc_safe_leave(m_render_gc_state); }, Qt::DirectConnection); }); } - -ForeignThreadManager::ForeignThreadManager() -{ -} diff --git a/foreign_thread_manager.hpp b/foreign_thread_manager.hpp index 4bf6114..64ad0ff 100644 --- a/foreign_thread_manager.hpp +++ b/foreign_thread_manager.hpp @@ -3,17 +3,20 @@ #include #include -class ForeignThreadManager +class ForeignThreadManager : public QObject { + Q_OBJECT public: static ForeignThreadManager& instance(); + static void gc_safe_enter(int& state); + static void gc_safe_leave(int state); void add_thread(QThread* t); void clear(QThread* main_thread); void add_window(QQuickItem* item); private: - ForeignThreadManager(); + using QObject::QObject; QSet m_threads; QMutex m_mutex; }; diff --git a/julia_imageprovider.cpp b/julia_imageprovider.cpp new file mode 100644 index 0000000..0cea9cd --- /dev/null +++ b/julia_imageprovider.cpp @@ -0,0 +1,81 @@ +#include "jlcxx/functions.hpp" + +#include "julia_api.hpp" +#include "julia_imageprovider.hpp" +#include "foreign_thread_manager.hpp" +#include + +namespace qmlwrap +{ + +JuliaImageProvider::JuliaImageProvider(QQmlImageProviderBase::ImageType type) : QQuickImageProvider(type) +{ +} + +template +inline ImageT process_request(JuliaImageProvider::callback_t f, const QString &id, QSize *size, const QSize &requestedSize, int& m_gc_state) +{ + if(f == nullptr) + { + throw std::runtime_error("No callback function set for JuliaImageProvider"); + } + + int main_gc_state = JL_GC_STATE_UNSAFE; + if(!QThread::isMainThread()) + { + // Set the main thread as GC safe and the image loading thread as unsafe before calling the julia function + ForeignThreadManager::instance().add_thread(QThread::currentThread()); + ForeignThreadManager::gc_safe_enter(m_gc_state); + QMetaObject::invokeMethod(&ForeignThreadManager::instance(), [&]{ ForeignThreadManager::gc_safe_enter(main_gc_state); }, + Qt::BlockingQueuedConnection); + ForeignThreadManager::gc_safe_leave(m_gc_state); + } + + ImageResult response = jlcxx::unbox>(f(id, requestedSize.width(), requestedSize.height())); + + if(!QThread::isMainThread()) + { + // Return to the initial GC state + ForeignThreadManager::gc_safe_enter(m_gc_state); + QMetaObject::invokeMethod(&ForeignThreadManager::instance(), [&]{ ForeignThreadManager::gc_safe_leave(main_gc_state); }, + Qt::BlockingQueuedConnection); + } + + *size = response.m_size; + return std::move(response.m_image); +} + +QImage JuliaImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize) +{ + if(imageType() != QQmlImageProviderBase::Image) + { + throw std::runtime_error("JuliaImageProvider is not of type Image"); + } + return process_request(m_callback, id, size, requestedSize, m_gc_state); +} + +QPixmap JuliaImageProvider::requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) +{ + if(imageType() != QQmlImageProviderBase::Pixmap) + { + throw std::runtime_error("JuliaImageProvider is not of type Pixmap"); + } + return process_request(m_callback, id, size, requestedSize, m_gc_state); +} + +QQuickTextureFactory *JuliaImageProvider::requestTexture(const QString &id, QSize *size, const QSize &requestedSize) +{ + if(imageType() != QQmlImageProviderBase::Texture) + { + throw std::runtime_error("JuliaImageProvider is not of type Texture"); + } + return process_request(m_callback, id, size, requestedSize, m_gc_state); +} + +void JuliaImageProvider::set_callback(jlcxx::SafeCFunction fdata) +{ + m_callback = jlcxx::make_function_pointer(fdata); +} + +} // namespace qmlwrap + diff --git a/julia_imageprovider.hpp b/julia_imageprovider.hpp new file mode 100644 index 0000000..c39b413 --- /dev/null +++ b/julia_imageprovider.hpp @@ -0,0 +1,49 @@ +#ifndef QML_JULIA_IMAGEPROVIDER_H +#define QML_JULIA_IMAGEPROVIDER_H + +#include "jlcxx/jlcxx.hpp" +#include "jlcxx/functions.hpp" + +#include +#include + +// #include "jlqml.hpp" + +namespace qmlwrap +{ + +// Wrapper type to easily return the image and its original size from Julia +template +struct ImageResult +{ + using image_type = ImageT; + + ImageResult(ImageT& image, int width, int height) : m_image(std::move(image)), m_size(width, height) + { + } + ImageT m_image; + QSize m_size; +}; + +/// Provide access to QPainter from Julia +class JuliaImageProvider : public QQuickImageProvider +{ + Q_OBJECT +public: + typedef jl_value_t* (*callback_t)(const QString&,int,int); + JuliaImageProvider(QQmlImageProviderBase::ImageType type); + + virtual QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override; + virtual QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override; + virtual QQuickTextureFactory* requestTexture(const QString &id, QSize *size, const QSize &requestedSize) override; + + void set_callback(jlcxx::SafeCFunction fdata); + +private: + callback_t m_callback = nullptr; + int m_gc_state = JL_GC_STATE_UNSAFE; +}; + +} // namespace qmlwrap + +#endif diff --git a/wrap_qml.cpp b/wrap_qml.cpp index 2e8049c..caaadb9 100644 --- a/wrap_qml.cpp +++ b/wrap_qml.cpp @@ -17,6 +17,7 @@ #include "julia_api.hpp" #include "julia_canvas.hpp" #include "julia_display.hpp" +#include "julia_imageprovider.hpp" #include "julia_itemmodel.hpp" #include "julia_painteditem.hpp" #include "julia_property_map.hpp" @@ -45,6 +46,11 @@ template<> struct SuperType { using type = QObject; }; template<> struct SuperType { using type = QObject; }; template<> struct SuperType { using type = QWindow; }; template<> struct SuperType { using type = QAbstractTableModel; }; +template<> struct SuperType { using type = QPaintDevice; }; +template<> struct SuperType { using type = QPaintDevice; }; +template<> struct SuperType { using type = QObject; }; +template<> struct SuperType { using type = QQmlImageProviderBase; }; +template<> struct SuperType { using type = QQuickImageProvider; }; } @@ -266,6 +272,17 @@ struct WrapQtAssociativeContainer } }; +struct WrapImageResult +{ + template + void operator()(TypeWrapperT&& wrapped) + { + using WrappedT = typename TypeWrapperT::type; + using image_type = typename WrappedT::image_type; + wrapped.template constructor(); + } +}; + } JLCXX_MODULE define_julia_module(jlcxx::Module& qml_module) @@ -361,6 +378,21 @@ JLCXX_MODULE define_julia_module(jlcxx::Module& qml_module) }) ); + qml_module.add_enum("ImageType", + std::vector({ + "Image", + "Pixmap", + "Texture", + "ImageResponse" + }), + std::vector({ + QQmlImageProviderBase::Image, + QQmlImageProviderBase::Pixmap, + QQmlImageProviderBase::Texture, + QQmlImageProviderBase::ImageResponse + }) + ); + qml_module.add_enum("MouseButton", std::vector({ "NoButton", @@ -656,7 +688,10 @@ JLCXX_MODULE define_julia_module(jlcxx::Module& qml_module) .method("_set_context_property", static_cast(&QQmlContext::setContextProperty)) .method("context_object", &QQmlContext::contextObject); + qml_module.add_type("QQmlImageProviderBase", julia_base_type()); + qml_module.add_type("QQmlEngine", julia_base_type()) + .method("addImageProvider", &QQmlEngine::addImageProvider) .method("clearComponentCache", &QQmlEngine::clearComponentCache) .method("clearSingletons", &QQmlEngine::clearSingletons) .method("root_context", &QQmlEngine::rootContext) @@ -856,4 +891,125 @@ JLCXX_MODULE define_julia_module(jlcxx::Module& qml_module) f(path); }); }); + + qml_module.add_enum("Format", + std::vector({ + "Format_Invalid", + "Format_Mono", + "Format_MonoLSB", + "Format_Indexed8", + "Format_RGB32", + "Format_ARGB32", + "Format_ARGB32_Premultiplied", + "Format_RGB16", + "Format_ARGB8565_Premultiplied", + "Format_RGB666", + "Format_ARGB6666_Premultiplied", + "Format_RGB555", + "Format_ARGB8555_Premultiplied", + "Format_RGB888", + "Format_RGB444", + "Format_ARGB4444_Premultiplied", + "Format_RGBX8888", + "Format_RGBA8888", + "Format_RGBA8888_Premultiplied", + "Format_BGR30", + "Format_A2BGR30_Premultiplied", + "Format_RGB30", + "Format_A2RGB30_Premultiplied", + "Format_Alpha8", + "Format_Grayscale8", + "Format_RGBX64", + "Format_RGBA64", + "Format_RGBA64_Premultiplied", + "Format_Grayscale16", + "Format_BGR888", + "Format_RGBX16FPx4", + "Format_RGBA16FPx4", + "Format_RGBA16FPx4_Premultiplied", + "Format_RGBX32FPx4", + "Format_RGBA32FPx4", + "Format_RGBA32FPx4_Premultiplied", + "Format_CMYK8888" + }), + std::vector({ + QImage::Format_Invalid, + QImage::Format_Mono, + QImage::Format_MonoLSB, + QImage::Format_Indexed8, + QImage::Format_RGB32, + QImage::Format_ARGB32, + QImage::Format_ARGB32_Premultiplied, + QImage::Format_RGB16, + QImage::Format_ARGB8565_Premultiplied, + QImage::Format_RGB666, + QImage::Format_ARGB6666_Premultiplied, + QImage::Format_RGB555, + QImage::Format_ARGB8555_Premultiplied, + QImage::Format_RGB888, + QImage::Format_RGB444, + QImage::Format_ARGB4444_Premultiplied, + QImage::Format_RGBX8888, + QImage::Format_RGBA8888, + QImage::Format_RGBA8888_Premultiplied, + QImage::Format_BGR30, + QImage::Format_A2BGR30_Premultiplied, + QImage::Format_RGB30, + QImage::Format_A2RGB30_Premultiplied, + QImage::Format_Alpha8, + QImage::Format_Grayscale8, + QImage::Format_RGBX64, + QImage::Format_RGBA64, + QImage::Format_RGBA64_Premultiplied, + QImage::Format_Grayscale16, + QImage::Format_BGR888, + QImage::Format_RGBX16FPx4, + QImage::Format_RGBA16FPx4, + QImage::Format_RGBA16FPx4_Premultiplied, + QImage::Format_RGBX32FPx4, + QImage::Format_RGBA32FPx4, + QImage::Format_RGBA32FPx4_Premultiplied, + QImage::Format_CMYK8888 + }) + ); + + qml_module.add_type("QColor") + .constructor() + .constructor() + .constructor() + .constructor(); + + qml_module.add_type("QImage", julia_base_type()) + .constructor() + .constructor() + .constructor() + .constructor() + .constructor() + .constructor() + .constructor() + .constructor() + .constructor() + .constructor() + .constructor() + .constructor() + .method("copy", static_cast(&QImage::copy)) + .method("copy", [] (const QImage& i) { return i.copy(); } ) + .method("height", &QImage::height) + .method("width", &QImage::width); + + qml_module.add_type("QPixmap", julia_base_type()) + .constructor() + .constructor() + .constructor() + .constructor() + .method("fill", &QPixmap::fill); + qml_module.method("fromImage", [] (const QImage& image) { return QPixmap::fromImage(image); } ); + + qml_module.add_type("QQuickImageProvider", julia_base_type()); + qml_module.add_type("JuliaImageProvider", julia_base_type()) + .constructor(jlcxx::finalize_policy::no) // No finalizer because the engine takes ownership + .method("set_callback", &qmlwrap::JuliaImageProvider::set_callback); + + qml_module.add_type>>("ImageResult") + .apply, qmlwrap::ImageResult>(qmlwrap::WrapImageResult()); } From 4c272a3f7da1ee7e7f1949b8342a2a5c68b58e87 Mon Sep 17 00:00:00 2001 From: Bart Janssens Date: Sun, 22 Mar 2026 22:30:44 +0100 Subject: [PATCH 3/3] Improve support for multithreading --- application_manager.cpp | 6 +- foreign_thread_manager.cpp | 128 ++++++++++++++++++------------------- foreign_thread_manager.hpp | 38 ++++++++--- julia_canvas.cpp | 6 +- julia_function.cpp | 2 + julia_imageprovider.cpp | 29 ++------- julia_imageprovider.hpp | 1 - julia_itemmodel.cpp | 19 ++++++ julia_painteditem.cpp | 2 + makie_viewport.cpp | 3 + opengl_viewport.cpp | 2 +- wrap_qml.cpp | 4 ++ 12 files changed, 133 insertions(+), 107 deletions(-) diff --git a/application_manager.cpp b/application_manager.cpp index 4b3131f..ef73476 100644 --- a/application_manager.cpp +++ b/application_manager.cpp @@ -97,7 +97,9 @@ void ApplicationManager::exec() QTimer* main_timer = new QTimer(m_engine); main_timer->setInterval(0); - QObject::connect(main_timer, &QTimer::timeout, [this]{ + QObject::connect(main_timer, &QTimer::timeout, [this] + { + GCGuard gc_guard; m_event_hook(); }); main_timer->start(); @@ -144,6 +146,7 @@ void ApplicationManager::cleanup() } delete JuliaSingleton::s_singletonInstance; JuliaSingleton::s_singletonInstance = nullptr; + ForeignThreadManager::instance().cleanup(); } void ApplicationManager::check_no_engine() @@ -164,7 +167,6 @@ void ApplicationManager::set_engine(QQmlEngine* e) { e->addImportPath(QString::fromStdString(path)); } - ForeignThreadManager::instance().clear(QThread::currentThread()); } void ApplicationManager::process_events() diff --git a/foreign_thread_manager.cpp b/foreign_thread_manager.cpp index e657a8f..e97b909 100644 --- a/foreign_thread_manager.cpp +++ b/foreign_thread_manager.cpp @@ -1,101 +1,95 @@ -#include - #include "jlcxx/jlcxx.hpp" #include "foreign_thread_manager.hpp" + +namespace qmlwrap +{ + +QMutex ForeignThreadManager::m_juliamutex; +thread_local ForeignThreadManager* ForeignThreadManager::m_instance = nullptr; + ForeignThreadManager& ForeignThreadManager::instance() { - static ForeignThreadManager instance; - return instance; + if (m_instance == nullptr) + { + m_instance = new ForeignThreadManager(); + } + return *m_instance; +} + +ForeignThreadManager::~ForeignThreadManager() +{ + gc_safe_leave(); +} + +ForeignThreadManager::ForeignThreadManager() +{ + if (!QThread::isMainThread()) + { + #if (JULIA_VERSION_MAJOR * 100 + JULIA_VERSION_MINOR) >= 109 + jl_adopt_thread(); + #endif + } + gc_safe_enter(); } -void ForeignThreadManager::gc_safe_enter(int& state) +void ForeignThreadManager::gc_safe_enter() { jl_ptls_t ptls = jl_current_task->ptls; - state = jl_gc_safe_enter(ptls); + m_state = jl_gc_safe_enter(ptls); #if (JULIA_VERSION_MAJOR * 100 + JULIA_VERSION_MINOR) > 111 ptls->engine_nqueued++; #endif } -void ForeignThreadManager::gc_safe_leave(int state) +void ForeignThreadManager::gc_safe_leave() { jl_ptls_t ptls = jl_current_task->ptls; - jl_gc_safe_leave(ptls, state); + jl_gc_safe_leave(ptls, m_state); #if (JULIA_VERSION_MAJOR * 100 + JULIA_VERSION_MINOR) > 111 ptls->engine_nqueued--; #endif } -void ForeignThreadManager::add_thread(QThread *t) +void ForeignThreadManager::begin_julia() { - m_mutex.lock(); - if(!m_threads.contains(t)) + if(m_depth == 0) { - m_threads.insert(t); - #if (JULIA_VERSION_MAJOR * 100 + JULIA_VERSION_MINOR) >= 109 - jl_adopt_thread(); - #else - if(m_threads.size() > 1) - { - std::cout << "Warning: using multiple threads in Julia versions older than 1.9 will probably crash" << std::endl; - } - #endif + m_juliamutex.lock(); + gc_safe_leave(); } - m_mutex.unlock(); + ++m_depth; } -void ForeignThreadManager::clear(QThread* main_thread) +void ForeignThreadManager::end_julia() { - m_mutex.lock(); - m_threads.clear(); - m_threads.insert(main_thread); - m_mutex.unlock(); + --m_depth; + if(m_depth == 0) + { + gc_safe_enter(); + m_juliamutex.unlock(); + } } -// This was defined after Julia 1.10 -#ifndef JL_GC_STATE_UNSAFE - #define JL_GC_STATE_UNSAFE 0 -#endif - +void ForeignThreadManager::cleanup() +{ + if(!QThread::isMainThread()) + { + return; + } + delete m_instance; + m_instance = nullptr; +} +GCGuard::GCGuard() +{ + ForeignThreadManager::instance().begin_julia(); +} -void ForeignThreadManager::add_window(QQuickItem* item) +GCGuard::~GCGuard() { - QObject::connect(item, &QQuickItem::windowChanged, [item, m_main_gc_state = JL_GC_STATE_UNSAFE, m_render_gc_state = JL_GC_STATE_UNSAFE] (QQuickWindow* w) mutable - { - if (w == nullptr) - { - return; - } - item->connect(w, &QQuickWindow::sceneGraphInitialized, [] () - { - // Adopt the render thread when it is first initialized - ForeignThreadManager::instance().add_thread(QThread::currentThread()); - }); - item->connect(w, &QQuickWindow::afterAnimating, [&m_main_gc_state] () - { - // afterAnimating is the last signal sent before locking the main loop, so indicate - // that the main loop is in a safe state for the Julia GC to run - gc_safe_enter(m_main_gc_state); - }); - item->connect(w, &QQuickWindow::beforeRendering, w, [&m_main_gc_state] () - { - // beforeRendering is the first signal sent after unlocking the main thread, so restore - // the Julia GC state here. Queued connection so this is executed on the main thread - gc_safe_leave(m_main_gc_state); - }, Qt::QueuedConnection); - item->connect(w, &QQuickWindow::afterFrameEnd, w, [&m_render_gc_state] () - { - // After the rendering is done, the render thread may block for event handling, so we mark it as in a GC safe state - // to prevent deadlock - gc_safe_enter(m_render_gc_state); - }, Qt::DirectConnection); - item->connect(w, &QQuickWindow::beforeFrameBegin, w, [&m_render_gc_state] () - { - // Reset GC state when the render thread might execute Julia code again - gc_safe_leave(m_render_gc_state); - }, Qt::DirectConnection); - }); + ForeignThreadManager::instance().end_julia(); +} + } diff --git a/foreign_thread_manager.hpp b/foreign_thread_manager.hpp index 64ad0ff..2726519 100644 --- a/foreign_thread_manager.hpp +++ b/foreign_thread_manager.hpp @@ -3,20 +3,38 @@ #include #include -class ForeignThreadManager : public QObject + +namespace qmlwrap +{ + +class ForeignThreadManager { - Q_OBJECT public: static ForeignThreadManager& instance(); - static void gc_safe_enter(int& state); - static void gc_safe_leave(int state); + ~ForeignThreadManager(); + + void gc_safe_enter(); + void gc_safe_leave(); + + void begin_julia(); + void end_julia(); - void add_thread(QThread* t); - void clear(QThread* main_thread); - void add_window(QQuickItem* item); + // Remove the current instance, to be called after exec finishes. + void cleanup(); private: - using QObject::QObject; - QSet m_threads; - QMutex m_mutex; + ForeignThreadManager(); + + int m_state = 0; + int m_depth = 0; + static thread_local ForeignThreadManager* m_instance; + static QMutex m_juliamutex; }; + +struct GCGuard +{ + GCGuard(); + ~GCGuard(); +}; + +} diff --git a/julia_canvas.cpp b/julia_canvas.cpp index 28f6933..f308ce2 100644 --- a/julia_canvas.cpp +++ b/julia_canvas.cpp @@ -11,7 +11,6 @@ namespace qmlwrap JuliaCanvas::JuliaCanvas(QQuickItem *parent) : QQuickPaintedItem(parent) { - ForeignThreadManager::instance().add_window(this); } void JuliaCanvas::paint(QPainter *painter) @@ -22,7 +21,10 @@ void JuliaCanvas::paint(QPainter *painter) unsigned int *draw_buffer = new unsigned int[iwidth * iheight]; // call julia painter - m_callback(draw_buffer, iwidth, iheight); + { + GCGuard gc_guard; + m_callback(draw_buffer, iwidth, iheight); + } // make QImage QImage *image = new QImage((uchar*)draw_buffer, width(), height(), QImage::Format_ARGB32); diff --git a/julia_function.cpp b/julia_function.cpp index a3c4692..9e5e1d7 100644 --- a/julia_function.cpp +++ b/julia_function.cpp @@ -2,6 +2,7 @@ #include "julia_function.hpp" #include "jlqml.hpp" +#include "foreign_thread_manager.hpp" namespace qmlwrap { @@ -20,6 +21,7 @@ JuliaFunction::~JuliaFunction() QVariant JuliaFunction::call(const QVariantList& args) { + GCGuard gc_guard; using call_julia_func_t = void* (*) (jl_value_t*, const void*); static call_julia_func_t call_func = reinterpret_cast(jlcxx::unbox(jlcxx::JuliaFunction(jl_get_function(m_qml_mod, "get_julia_call"))())); QVariant result_var = *reinterpret_cast(call_func(m_f, &args)); diff --git a/julia_imageprovider.cpp b/julia_imageprovider.cpp index 0cea9cd..d330861 100644 --- a/julia_imageprovider.cpp +++ b/julia_imageprovider.cpp @@ -13,34 +13,15 @@ JuliaImageProvider::JuliaImageProvider(QQmlImageProviderBase::ImageType type) : } template -inline ImageT process_request(JuliaImageProvider::callback_t f, const QString &id, QSize *size, const QSize &requestedSize, int& m_gc_state) +inline ImageT process_request(JuliaImageProvider::callback_t f, const QString &id, QSize *size, const QSize &requestedSize) { if(f == nullptr) { throw std::runtime_error("No callback function set for JuliaImageProvider"); } - int main_gc_state = JL_GC_STATE_UNSAFE; - if(!QThread::isMainThread()) - { - // Set the main thread as GC safe and the image loading thread as unsafe before calling the julia function - ForeignThreadManager::instance().add_thread(QThread::currentThread()); - ForeignThreadManager::gc_safe_enter(m_gc_state); - QMetaObject::invokeMethod(&ForeignThreadManager::instance(), [&]{ ForeignThreadManager::gc_safe_enter(main_gc_state); }, - Qt::BlockingQueuedConnection); - ForeignThreadManager::gc_safe_leave(m_gc_state); - } - + GCGuard gc_guard; ImageResult response = jlcxx::unbox>(f(id, requestedSize.width(), requestedSize.height())); - - if(!QThread::isMainThread()) - { - // Return to the initial GC state - ForeignThreadManager::gc_safe_enter(m_gc_state); - QMetaObject::invokeMethod(&ForeignThreadManager::instance(), [&]{ ForeignThreadManager::gc_safe_leave(main_gc_state); }, - Qt::BlockingQueuedConnection); - } - *size = response.m_size; return std::move(response.m_image); } @@ -51,7 +32,7 @@ QImage JuliaImageProvider::requestImage(const QString &id, QSize *size, const QS { throw std::runtime_error("JuliaImageProvider is not of type Image"); } - return process_request(m_callback, id, size, requestedSize, m_gc_state); + return process_request(m_callback, id, size, requestedSize); } QPixmap JuliaImageProvider::requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) @@ -60,7 +41,7 @@ QPixmap JuliaImageProvider::requestPixmap(const QString &id, QSize *size, const { throw std::runtime_error("JuliaImageProvider is not of type Pixmap"); } - return process_request(m_callback, id, size, requestedSize, m_gc_state); + return process_request(m_callback, id, size, requestedSize); } QQuickTextureFactory *JuliaImageProvider::requestTexture(const QString &id, QSize *size, const QSize &requestedSize) @@ -69,7 +50,7 @@ QQuickTextureFactory *JuliaImageProvider::requestTexture(const QString &id, QSiz { throw std::runtime_error("JuliaImageProvider is not of type Texture"); } - return process_request(m_callback, id, size, requestedSize, m_gc_state); + return process_request(m_callback, id, size, requestedSize); } void JuliaImageProvider::set_callback(jlcxx::SafeCFunction fdata) diff --git a/julia_imageprovider.hpp b/julia_imageprovider.hpp index c39b413..f9eb43e 100644 --- a/julia_imageprovider.hpp +++ b/julia_imageprovider.hpp @@ -41,7 +41,6 @@ class JuliaImageProvider : public QQuickImageProvider private: callback_t m_callback = nullptr; - int m_gc_state = JL_GC_STATE_UNSAFE; }; } // namespace qmlwrap diff --git a/julia_itemmodel.cpp b/julia_itemmodel.cpp index b2c288c..0e1485b 100644 --- a/julia_itemmodel.cpp +++ b/julia_itemmodel.cpp @@ -1,4 +1,5 @@ #include "julia_itemmodel.hpp" +#include "foreign_thread_manager.hpp" #include @@ -40,18 +41,21 @@ auto safe_unbox(jl_value_t* arg) int JuliaItemModel::rowCount(const QModelIndex& parent) const { + GCGuard gc_guard; static const jlcxx::JuliaFunction rowcount(jl_get_function(m_qml_mod, "rowcount")); return safe_unbox(rowcount(m_data)); } int JuliaItemModel::columnCount(const QModelIndex& parent) const { + GCGuard gc_guard; static const jlcxx::JuliaFunction colcount(jl_get_function(m_qml_mod, "colcount")); return safe_unbox(colcount(m_data)); } QVariant JuliaItemModel::data(const QModelIndex& index, int role) const { + GCGuard gc_guard; static const jlcxx::JuliaFunction data_f(jl_get_function(m_qml_mod, "data")); // The static cast avoids sending a reference to the Julia function, which would require adding an extra method return safe_unbox(data_f(m_data, static_cast(role), index.row()+1, index.column()+1)); @@ -59,18 +63,21 @@ QVariant JuliaItemModel::data(const QModelIndex& index, int role) const QVariant JuliaItemModel::headerData(int section, Qt::Orientation orientation, int role) const { + GCGuard gc_guard; static const jlcxx::JuliaFunction headerdata_f(jl_get_function(m_qml_mod, "headerdata")); return safe_unbox(headerdata_f(m_data, section+1, static_cast(orientation), static_cast(role))); } bool JuliaItemModel::setData(const QModelIndex& index, const QVariant& value, int role) { + GCGuard gc_guard; static const jlcxx::JuliaFunction setdata(jl_get_function(m_qml_mod, "setdata!")); return safe_unbox(setdata(this, value, static_cast(role), index.row()+1, index.column()+1)); } bool JuliaItemModel::setHeaderData(int section, Qt::Orientation orientation, const QVariant& value, int role) { + GCGuard gc_guard; static const jlcxx::JuliaFunction setheaderdata_f(jl_get_function(m_qml_mod, "setheaderdata!")); return safe_unbox(setheaderdata_f(this, section+1, static_cast(orientation), value, static_cast(role))); } @@ -85,72 +92,84 @@ Qt::ItemFlags JuliaItemModel::flags(const QModelIndex& index) const QHash JuliaItemModel::roleNames() const { + GCGuard gc_guard; static const jlcxx::JuliaFunction rolenames(jl_get_function(m_qml_mod, "rolenames")); return safe_unbox>(rolenames(m_data)); } void JuliaItemModel::clear() { + GCGuard gc_guard; static const jlcxx::JuliaFunction clear_f(jl_get_function(m_qml_mod, "clear!")); clear_f(this); } void JuliaItemModel::appendRow(const QVariant& row) { + GCGuard gc_guard; static const jlcxx::JuliaFunction append_row_f(jl_get_function(m_qml_mod, "append_row!")); append_row_f(this, row); } void JuliaItemModel::insertRow(int rowIndex, const QVariant& row) { + GCGuard gc_guard; static const jlcxx::JuliaFunction insert_row_f(jl_get_function(m_qml_mod, "insert_row!")); insert_row_f(this, rowIndex+1, row); } void JuliaItemModel::moveRow(int fromRowIndex, int toRowIndex, int rows) { + GCGuard gc_guard; static const jlcxx::JuliaFunction move_row_f(jl_get_function(m_qml_mod, "move_rows!")); move_row_f(this, fromRowIndex+1, toRowIndex+1, static_cast(rows)); } void JuliaItemModel::removeRow(int rowIndex, int rows) { + GCGuard gc_guard; static const jlcxx::JuliaFunction remove_row_f(jl_get_function(m_qml_mod, "remove_rows!")); remove_row_f(this, rowIndex+1, static_cast(rows)); } void JuliaItemModel::setRow(int rowIndex, const QVariant& row) { + GCGuard gc_guard; static const jlcxx::JuliaFunction set_row_f(jl_get_function(m_qml_mod, "set_row!")); set_row_f(this, rowIndex + 1, row); } void JuliaItemModel::appendColumn(const QVariant& column) { + GCGuard gc_guard; static const jlcxx::JuliaFunction append_column_f(jl_get_function(m_qml_mod, "append_column!")); append_column_f(this, column); } void JuliaItemModel::insertColumn(int columnIndex, const QVariant& column) { + GCGuard gc_guard; static const jlcxx::JuliaFunction insert_column_f(jl_get_function(m_qml_mod, "insert_column!")); insert_column_f(this, columnIndex+1, column); } void JuliaItemModel::moveColumn(int fromColumnIndex, int toColumnIndex, int columns) { + GCGuard gc_guard; static const jlcxx::JuliaFunction move_column_f(jl_get_function(m_qml_mod, "move_columns!")); move_column_f(this, fromColumnIndex+1, toColumnIndex+1, static_cast(columns)); } void JuliaItemModel::removeColumn(int columnIndex, int columns) { + GCGuard gc_guard; static const jlcxx::JuliaFunction remove_column_f(jl_get_function(m_qml_mod, "remove_columns!")); remove_column_f(this, columnIndex+1, static_cast(columns)); } void JuliaItemModel::setColumn(int columnIndex, const QVariant& column) { + GCGuard gc_guard; static const jlcxx::JuliaFunction set_column_f(jl_get_function(m_qml_mod, "set_column!")); set_column_f(this, columnIndex + 1, column); } diff --git a/julia_painteditem.cpp b/julia_painteditem.cpp index 02bde40..edd85d3 100644 --- a/julia_painteditem.cpp +++ b/julia_painteditem.cpp @@ -4,6 +4,7 @@ #include "julia_api.hpp" #include "julia_painteditem.hpp" +#include "foreign_thread_manager.hpp" namespace qmlwrap { @@ -14,6 +15,7 @@ JuliaPaintedItem::JuliaPaintedItem(QQuickItem *parent) : QQuickPaintedItem(paren void JuliaPaintedItem::paint(QPainter* painter) { + GCGuard gc_guard; m_callback(painter, this); } diff --git a/makie_viewport.cpp b/makie_viewport.cpp index 40882a6..c77372a 100644 --- a/makie_viewport.cpp +++ b/makie_viewport.cpp @@ -2,6 +2,7 @@ #include "makie_viewport.hpp" #include "julia_api.hpp" +#include "foreign_thread_manager.hpp" #include #include @@ -27,6 +28,7 @@ class MakieRenderFunction : public RenderFunction } void render() override { + GCGuard gc_guard; if(m_scene != nullptr) { m_scene_render_function(m_screen_ptr, m_scene); @@ -126,6 +128,7 @@ void MakieViewport::setScene(qvariant_any_t scene) void MakieViewport::setup_buffer(QOpenGLFramebufferObject* fbo) { + GCGuard gc_guard; if(m_screen == nullptr) { m_screen = MakieSupport::instance().setup_screen(std::forward(fbo), window()); diff --git a/opengl_viewport.cpp b/opengl_viewport.cpp index c20c411..6a6fb05 100644 --- a/opengl_viewport.cpp +++ b/opengl_viewport.cpp @@ -64,7 +64,6 @@ OpenGLViewport::OpenGLViewport(QQuickItem *parent, RenderFunction* render_func) } QObject::connect(this, &OpenGLViewport::renderFunctionChanged, this, &OpenGLViewport::update); setMirrorVertically(true); - ForeignThreadManager::instance().add_window(this); } void OpenGLViewport::render() @@ -90,6 +89,7 @@ void DefaultRenderFunction::setRenderFunction(jlcxx::SafeCFunction f) void DefaultRenderFunction::render() { + GCGuard gc_guard; m_render_function(); } diff --git a/wrap_qml.cpp b/wrap_qml.cpp index caaadb9..f978c5d 100644 --- a/wrap_qml.cpp +++ b/wrap_qml.cpp @@ -14,6 +14,7 @@ #include #include "application_manager.hpp" +#include "foreign_thread_manager.hpp" #include "julia_api.hpp" #include "julia_canvas.hpp" #include "julia_display.hpp" @@ -566,6 +567,7 @@ JLCXX_MODULE define_julia_module(jlcxx::Module& qml_module) QObject::connect(&obj, &QObject::destroyed, [jl_f](QObject* o) { static JuliaFunction f(jl_f); + qmlwrap::GCGuard gc_guard; f(o); }); }); @@ -662,6 +664,7 @@ JLCXX_MODULE define_julia_module(jlcxx::Module& qml_module) { const jlcxx::JuliaFunction on_value_changed(callback); jl_value_t* julia_propmap = julia_property_map; + qmlwrap::GCGuard gc_guard; on_value_changed(julia_propmap, key, newvalue); }); }); @@ -888,6 +891,7 @@ JLCXX_MODULE define_julia_module(jlcxx::Module& qml_module) QObject::connect(&watcher, &QFileSystemWatcher::fileChanged, [jl_f](const QString& path) { static JuliaFunction f(jl_f); + qmlwrap::GCGuard gc_guard; f(path); }); });