diff --git a/CMakeLists.txt b/CMakeLists.txt index ef6ab14..f6e4502 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.16.0) project(JlQML) -set(JlQML_VERSION 0.8.0) +set(JlQML_VERSION 0.9.0) message(STATUS "Project version: v${JlQML_VERSION}") set(CMAKE_MACOSX_RPATH 1) diff --git a/application_manager.cpp b/application_manager.cpp index 97a7616..821d3c4 100644 --- a/application_manager.cpp +++ b/application_manager.cpp @@ -94,7 +94,6 @@ void ApplicationManager::exec() { app->exit(status); }); - ForeignThreadManager::instance().clear(QThread::currentThread()); const int status = app->exec(); if (status != 0) { @@ -112,7 +111,10 @@ void ApplicationManager::add_import_path(std::string path) ApplicationManager::ApplicationManager() { - qputenv("QSG_RENDER_LOOP", QProcessEnvironment::systemEnvironment().value("QSG_RENDER_LOOP").toLocal8Bit()); + if(QProcessEnvironment::systemEnvironment().contains("QSG_RENDER_LOOP")) + { + qputenv("QSG_RENDER_LOOP", QProcessEnvironment::systemEnvironment().value("QSG_RENDER_LOOP").toLocal8Bit()); + } qInstallMessageHandler(julia_message_output); @@ -154,6 +156,7 @@ 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 0cd1e7a..cd50308 100644 --- a/foreign_thread_manager.cpp +++ b/foreign_thread_manager.cpp @@ -1,3 +1,5 @@ +#include + #include "jlcxx/jlcxx.hpp" #include "foreign_thread_manager.hpp" @@ -34,6 +36,68 @@ void ForeignThreadManager::clear(QThread* main_thread) m_mutex.unlock(); } +// This was defined after Julia 1.10 +#ifndef JL_GC_STATE_UNSAFE + #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) > 110 + 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) > 110 + ptls->engine_nqueued--; + #endif +} + +void ForeignThreadManager::add_window(QQuickItem* item) +{ + 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::ForeignThreadManager() { } diff --git a/foreign_thread_manager.hpp b/foreign_thread_manager.hpp index bd28539..4bf6114 100644 --- a/foreign_thread_manager.hpp +++ b/foreign_thread_manager.hpp @@ -1,5 +1,6 @@ #include #include +#include #include class ForeignThreadManager @@ -9,6 +10,7 @@ class ForeignThreadManager void add_thread(QThread* t); void clear(QThread* main_thread); + void add_window(QQuickItem* item); private: ForeignThreadManager(); diff --git a/julia_canvas.cpp b/julia_canvas.cpp index 81c6e71..28f6933 100644 --- a/julia_canvas.cpp +++ b/julia_canvas.cpp @@ -11,18 +11,18 @@ namespace qmlwrap JuliaCanvas::JuliaCanvas(QQuickItem *parent) : QQuickPaintedItem(parent) { + ForeignThreadManager::instance().add_window(this); } void JuliaCanvas::paint(QPainter *painter) { - ForeignThreadManager::instance().add_thread(QThread::currentThread()); // allocate buffer for julia callback to draw on int iwidth = width(); int iheight = height(); unsigned int *draw_buffer = new unsigned int[iwidth * iheight]; // call julia painter - m_callback(jlcxx::make_julia_array(draw_buffer, iwidth*iheight), iwidth, iheight); + m_callback(draw_buffer, iwidth, iheight); // make QImage QImage *image = new QImage((uchar*)draw_buffer, width(), height(), QImage::Format_ARGB32); @@ -37,7 +37,7 @@ void JuliaCanvas::paint(QPainter *painter) void JuliaCanvas::setPaintFunction(jlcxx::SafeCFunction f) { - m_callback = jlcxx::make_function_pointer, int, int)>(f); + m_callback = jlcxx::make_function_pointer(f); } } // namespace qmlwrap diff --git a/julia_canvas.hpp b/julia_canvas.hpp index 52cab23..acce2e6 100644 --- a/julia_canvas.hpp +++ b/julia_canvas.hpp @@ -18,7 +18,7 @@ class JuliaCanvas : public QQuickPaintedItem Q_PROPERTY(jlcxx::SafeCFunction paintFunction READ paintFunction WRITE setPaintFunction) public: - typedef void (*callback_t)(jlcxx::ArrayRef, int, int); + typedef void (*callback_t)(unsigned int*, int, int); JuliaCanvas(QQuickItem *parent = 0); void paint(QPainter *painter); void setPaintFunction(jlcxx::SafeCFunction f); diff --git a/julia_painteditem.cpp b/julia_painteditem.cpp index 024d3eb..02bde40 100644 --- a/julia_painteditem.cpp +++ b/julia_painteditem.cpp @@ -10,10 +10,6 @@ namespace qmlwrap JuliaPaintedItem::JuliaPaintedItem(QQuickItem *parent) : QQuickPaintedItem(parent) { - if(qgetenv("QSG_RENDER_LOOP") != "basic") - { - qFatal("QSG_RENDER_LOOP must be set to basic to use JuliaPaintedItem. Add the line\nENV[\"QSG_RENDER_LOOP\"] = \"basic\"\nat the top of your Julia program"); - } } void JuliaPaintedItem::paint(QPainter* painter) diff --git a/opengl_viewport.cpp b/opengl_viewport.cpp index 2a27379..c20c411 100644 --- a/opengl_viewport.cpp +++ b/opengl_viewport.cpp @@ -1,8 +1,10 @@ #include #include +#include #include #include +#include "foreign_thread_manager.hpp" #include "opengl_viewport.hpp" namespace qmlwrap @@ -26,6 +28,7 @@ class OpenGLViewport::JuliaRenderer : public QQuickFramebufferObject::Renderer m_vp->render(); m_vp->post_render(); m_vp->window()->endExternalCommands(); + QQuickOpenGLUtils::resetOpenGLState(); } void synchronize(QQuickFramebufferObject *item) @@ -59,12 +62,9 @@ OpenGLViewport::OpenGLViewport(QQuickItem *parent, RenderFunction* render_func) { qFatal("OpenGL rendering required for OpenGLViewport or MakieViewport. Add the line\nQML.setGraphicsApi(QML.OpenGL)\nbefore loading the QML program."); } - if(qgetenv("QSG_RENDER_LOOP") != "basic") - { - qFatal("QSG_RENDER_LOOP must be set to basic to use OpenGLViewport or MakieViewport. Add the line\nENV[\"QSG_RENDER_LOOP\"] = \"basic\"\nat the top of your Julia program"); - } QObject::connect(this, &OpenGLViewport::renderFunctionChanged, this, &OpenGLViewport::update); setMirrorVertically(true); + ForeignThreadManager::instance().add_window(this); } void OpenGLViewport::render() diff --git a/test/test_module_load.cpp b/test/test_module_load.cpp index 2786271..03396a3 100644 --- a/test/test_module_load.cpp +++ b/test/test_module_load.cpp @@ -26,7 +26,11 @@ int main() throw std::runtime_error("Error opening jlqml lib " + libpath); } void* regfunc = nullptr; + #if (JULIA_VERSION_MAJOR * 100 + JULIA_VERSION_MINOR) >= 114 + if(!jl_dlsym(lib, "define_julia_module", ®func, false, false)) + #else if(!jl_dlsym(lib, "define_julia_module", ®func, false)) + #endif { throw std::runtime_error("Error finding entrypoint define_julia_module"); }; diff --git a/wrap_qml.cpp b/wrap_qml.cpp index dd67899..5cbc1f3 100644 --- a/wrap_qml.cpp +++ b/wrap_qml.cpp @@ -738,7 +738,7 @@ JLCXX_MODULE define_julia_module(jlcxx::Module& qml_module) qml_module.method("init_qquickview", []() { return qmlwrap::ApplicationManager::instance().init_qquickview(); }); qml_module.method("cleanup", []() { qmlwrap::ApplicationManager::instance().cleanup(); }); qml_module.method("qmlcontext", []() { return qmlwrap::ApplicationManager::instance().root_context(); }); - qml_module.method("exec", []() { qmlwrap::ApplicationManager::instance().exec(); }); + qml_module.method("app_exec", []() { qmlwrap::ApplicationManager::instance().exec(); }); qml_module.method("process_events", qmlwrap::ApplicationManager::process_events); qml_module.method("add_import_path", [](std::string path) { qmlwrap::ApplicationManager::instance().add_import_path(path); });