Harden the GL canvases against dropped frames and UI-thread freezes

Viewport: GLCanvas3D::on_idle() cleared m_dirty even when
_refresh_if_shown_on_screen() rendered nothing because the canvas was not on
screen yet — a frame requested while the notebook was still showing a page got
silently swallowed and the viewport stayed blank until some later event dirtied
it again. _refresh_if_shown_on_screen() now reports whether it rendered, and
on_idle keeps the canvas dirty when it did not. Covers Design/Prepare/Preview.

Freeze: a big STEP (17.8 MB) spent ~40-50 s inside OCCT on the UI thread
(read_step_solids + recompute), so the window stopped repainting and the
compositor marked the app unresponsive. Both now run on a worker thread behind
an app-modal pulsing progress dialog: the UI keeps painting and the document
cannot be touched while the worker owns it. OCCT's Standard_Failure is not a
std::exception, so the worker catches it explicitly — an escaping exception
would terminate the process.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
This commit is contained in:
Tommaso Bianchi
2026-07-15 00:04:25 +02:00
co-authored by Claude Opus 4.8
parent 6d9368276c
commit 01aa6903f0
3 changed files with 66 additions and 13 deletions
+48 -2
View File
@@ -27,6 +27,7 @@
#include <wx/dialog.h>
#include <wx/colordlg.h>
#include <wx/menu.h>
#include <wx/progdlg.h>
#include <string>
#include <memory>
@@ -34,6 +35,8 @@
#include <cmath>
#include <cstdio>
#include <algorithm>
#include <thread>
#include <atomic>
#include "slic3r/GUI/wxExtensions.hpp" // ScalableButton, create_scaled_bitmap
#include "Widgets/Label.hpp" // HarmonyOS Sans fonts (Head_*/Body_*) shared with the rest of Orca
@@ -2369,6 +2372,28 @@ void DesignPanel::on_import_svg()
add_imported_sketch(svg_to_regions(path, 1.0), _L("SVG"));
}
// Run a long CAD computation off the UI thread. A big STEP costs tens of seconds in OCCT
// (parse + tessellate); running it inline froze the whole window — the compositor marked the
// app unresponsive and nothing repainted. The dialog is app-modal, so the document cannot be
// touched while the worker owns it. Exceptions must not escape the worker: `work` is expected
// to swallow them (OCCT throws Standard_Failure, which is not a std::exception).
static void run_off_ui_thread(wxWindow* parent, const wxString& message, const std::function<void()>& work)
{
wxProgressDialog dlg(_L("SnapOrca"), message, 100, parent,
wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_SMOOTH);
std::atomic<bool> done{false};
std::thread worker([&work, &done]() {
work();
done.store(true, std::memory_order_release);
});
while (!done.load(std::memory_order_acquire)) {
dlg.Pulse();
wxYield(); // keep painting; app-modal blocks input to the rest of the UI
wxMilliSleep(30);
}
worker.join();
}
void DesignPanel::on_import_step()
{
wxFileDialog dlg(this, _L("Import STEP"), wxEmptyString, wxEmptyString,
@@ -2380,7 +2405,17 @@ void DesignPanel::on_import_step()
std::string err;
// Keep the OCCT B-rep (don't mesh it like the slicer importer): each top-level solid
// becomes a coexisting CadBody, fully editable by the on-face/edge feature tools.
const std::vector<TopoDS_Shape> solids = GeometryEngine::read_step_solids(path, err);
std::vector<TopoDS_Shape> solids;
run_off_ui_thread(this, _L("Reading STEP…"), [&]() {
try {
solids = GeometryEngine::read_step_solids(path, err);
} catch (const Standard_Failure& e) {
const char* what = e.GetMessageString();
err = (what != nullptr && *what != '\0') ? what : "OCCT failure";
} catch (const std::exception& e) {
err = e.what();
}
});
if (solids.empty()) {
m_status->SetForegroundColour(wxColour(235, 110, 110));
m_status->SetLabel(err.empty() ? _L("No solids found in STEP")
@@ -2398,7 +2433,18 @@ void DesignPanel::on_import_step()
f.mode = BooleanMode::New; // each solid is its own coexisting body
m_doc.features.push_back(f);
}
if (!m_doc.recompute()) {
bool rebuilt = false;
run_off_ui_thread(this, _L("Rebuilding model…"), [&]() {
try {
rebuilt = m_doc.recompute();
} catch (const Standard_Failure& e) {
const char* what = e.GetMessageString();
m_doc.error = (what != nullptr && *what != '\0') ? what : "OCCT failure";
} catch (const std::exception& e) {
m_doc.error = e.what();
}
});
if (!rebuilt) {
m_status->SetForegroundColour(wxColour(235, 110, 110));
m_status->SetLabel(_L("STEP import failed: ") + wxString::FromUTF8(m_doc.error));
m_status->Refresh();
+15 -10
View File
@@ -3267,7 +3267,10 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt)
m_last_frame_start_time = now;
}
_refresh_if_shown_on_screen();
// Not on screen yet (e.g. the notebook is still showing this page): nothing was rendered,
// so keep the frame pending instead of dropping it — clearing m_dirty here leaves the
// canvas blank until some later event happens to dirty it again.
const bool rendered = _refresh_if_shown_on_screen();
#if ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT
if (m_extra_frame_requested || mouse3d_controller_applied || imgui_requires_extra_frame || wxGetApp().imgui()->requires_extra_frame()) {
@@ -3279,7 +3282,7 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt)
evt.RequestMore();
}
else
m_dirty = false;
m_dirty = !rendered;
}
void GLCanvas3D::on_char(wxKeyEvent& evt)
@@ -7251,16 +7254,18 @@ void GLCanvas3D::_update_camera_zoom(double zoom)
m_dirty = true;
}
void GLCanvas3D::_refresh_if_shown_on_screen()
bool GLCanvas3D::_refresh_if_shown_on_screen()
{
if (_is_shown_on_screen()) {
const Size& cnv_size = get_canvas_size();
_resize((unsigned int)cnv_size.get_width(), (unsigned int)cnv_size.get_height());
if (!_is_shown_on_screen())
return false;
// Because of performance problems on macOS, where PaintEvents are not delivered
// frequently enough, we call render() here directly when we can.
render();
}
const Size& cnv_size = get_canvas_size();
_resize((unsigned int)cnv_size.get_width(), (unsigned int)cnv_size.get_height());
// Because of performance problems on macOS, where PaintEvents are not delivered
// frequently enough, we call render() here directly when we can.
render();
return true;
}
void GLCanvas3D::_picking_pass()
+3 -1
View File
@@ -1256,7 +1256,9 @@ private:
void _zoom_to_box(const BoundingBoxf3& box, double margin_factor = DefaultCameraZoomToBoxMarginFactor);
void _update_camera_zoom(double zoom);
void _refresh_if_shown_on_screen();
// Returns false when the canvas is not on screen yet and therefore nothing was rendered,
// so callers can keep it dirty instead of dropping the pending frame.
bool _refresh_if_shown_on_screen();
void _picking_pass();
void _rectangular_selection_picking_pass();