App: Invoke signalBeforeRecompute() on the GUI thread

Historically, `App::Document::recompute()` ran entirely on the **main**
(GUI) thread and directly emitted `signalBeforeRecompute()`.

* Add-ons like **Assembly3** and others depend on that signal for
setup/teardown hooks before any recompute work begins.

* After offloading `recompute()` into a background worker thread to keep
the UI responsive, calling `signalBeforeRecompute()` directly from the
worker would break thread-affinity rules and silently break
compatibility with those add-ons.

**Solution**

1. **Introduce a generic hook** (`PreRecomputeHook`) in
**App::Document**:

* A `std::function<void()>` that, if set, is invoked at the very
start of `recompute()`.

* Core code stays Qt-free—only knows to call a callback if one
exists.

2. **Wire up the hook in `Gui::Document`**:

* In the GUI wrapper’s constructor, install a hook that calls
`callSignalBeforeRecompute()`.

* `callSignalBeforeRecompute()` uses `QMetaObject::invokeMethod(...,
Qt::BlockingQueuedConnection)` to enqueue `signalBeforeRecompute()` on
the GUI thread and **block** the worker until it completes.

    * If already on the GUI thread, it simply calls the signal directly.

3. **Maintain add-on compatibility**:

* From the add-on’s perspective nothing changes—they still receive
`signalBeforeRecompute()` on the main thread before any recompute work.

* Internally, the recompute body now runs on a worker thread,
improving UI responsiveness without breaking existing hooks.

**Result**

* **Recompute** remains fully backward-compatible for add-ons like
Assembly3.

* **UI thread** still handles all GUI-related signaling.

* **Worker thread** performs the actual heavy lifting, unblocked only
once the GUI is primed and all pre-recompute signals have been
delivered.
This commit is contained in:
Joao Matos
2025-05-18 13:23:08 +01:00
committed by Benjamin Nauck
parent 6cd828d986
commit 1f5e0f6494
5 changed files with 39 additions and 1 deletions

View File

@@ -2878,6 +2878,11 @@ void Document::renameObjectIdentifiers(
}
}
void Document::setPreRecomputeHook(const PreRecomputeHook& hook)
{
d->_preRecomputeHook = hook;
}
int Document::recompute(const std::vector<DocumentObject*>& objs,
bool force,
bool* hasError,
@@ -2918,7 +2923,13 @@ int Document::recompute(const std::vector<DocumentObject*>& objs,
FC_TIME_INIT(t);
Base::ObjectStatusLocker<Document::Status, Document> exe(Document::Recomputing, this);
signalBeforeRecompute(*this);
// This will hop into the main thread, fire signalBeforeRecompute(),
// and *block* the worker until the main thread is done, avoiding races
// between any running Python code and the rest of the recompute call.
if (d->_preRecomputeHook) {
d->_preRecomputeHook();
}
//////////////////////////////////////////////////////////////////////////
// FIXME Comment by Realthunder:

View File

@@ -193,6 +193,8 @@ public:
//@}
// NOLINTEND
using PreRecomputeHook = std::function<void()>;
void setPreRecomputeHook(const PreRecomputeHook& hook);
void clearDocument();

View File

@@ -98,6 +98,8 @@ struct DocumentP
StringHasherRef Hasher {new StringHasher};
Document::PreRecomputeHook _preRecomputeHook;
DocumentP();
void addRecomputeLog(const char* why, App::DocumentObject* obj)

View File

@@ -39,6 +39,7 @@
# include <QOpenGLWidget>
# include <QTextStream>
# include <QTimer>
# include <QThread>
# include <QStatusBar>
# include <Inventor/actions/SoSearchAction.h>
# include <Inventor/nodes/SoSeparator.h>
@@ -505,6 +506,8 @@ Document::Document(App::Document* pcDocument,Application * app)
(std::bind(&Gui::Document::slotTransactionRemove, this, sp::_1, sp::_2));
//NOLINTEND
pcDocument->setPreRecomputeHook([this] { callSignalBeforeRecompute(); });
// pointer to the python class
// NOTE: As this Python object doesn't get returned to the interpreter we
// mustn't increment it (Werner Jan-12-2006)
@@ -1193,6 +1196,25 @@ void Document::slotTouchedObject(const App::DocumentObject &Obj)
}
}
// helper that guarantees signalBeforeRecompute call is executed in the GUI thread and
// that the worker waits until it finishes
void Document::callSignalBeforeRecompute()
{
auto invokeSignalBeforeRecompute = [this]{
// this runs in the GUI thread
this->getDocument()->signalBeforeRecompute(*this->getDocument());
};
if (QThread::currentThread() == qApp->thread()) {
// already on GUI thread no hop, just call it
invokeSignalBeforeRecompute();
} else {
// hop to GUI and *block* until it returns
QMetaObject::invokeMethod(qApp, std::move(invokeSignalBeforeRecompute),
Qt::BlockingQueuedConnection);
}
}
void Document::addViewProvider(Gui::ViewProviderDocumentObject* vp)
{
// Hint: The undo/redo first adds the view provider to the Gui

View File

@@ -94,6 +94,7 @@ protected:
void slotSkipRecompute(const App::Document &doc, const std::vector<App::DocumentObject*> &objs);
void slotTouchedObject(const App::DocumentObject &);
void slotChangePropertyEditor(const App::Document&, const App::Property &);
void callSignalBeforeRecompute();
//@}
public: