From 01499e536ed68cad6d1eabe80bbc672bfb42201a Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 13 Oct 2026 15:00:18 +0800 Subject: [PATCH] fix(gui): stop the UV editor pane from hijacking the shared GL context The texture-displacement gizmo pushes pane state to the UV editor from the preview rebuild that runs on every frame of a slider drag. Tile size is the only one of the layer card's sliders whose value appears in that pane state, so dragging it changes the "N.N mm tile" label, the panel relays out, and the UV canvas receives a synchronous wxEVT_SIZE. Its on_size() flushes a paint with Update(), and render() then makes the app's single shared wxGLContext current on the UV drawable in the middle of the 3D canvas's frame. On X11/GLX that out-of-turn context activation is fatal: the drawable the 3D canvas later swaps no longer belongs to the current context, the server returns BadMatch, and GDK turns the unhandled X error into g_error(). EGL on Wayland reports only a status, so the same theft silently corrupts the frame instead of aborting - which is why the guard is not conditioned on the display backend. Add GLCanvas3D::is_frame_in_progress(), backed by a counter that _render_frame() maintains through its existing scope guard, and let the UV canvas skip the synchronous paint while a frame is on the stack, re-queueing it with Refresh() so nothing is dropped. Rebind the context with _set_current() before SwapBuffers() as well, so a surface that steals it by some other route - a modal GL dialog a gizmo opens from its panel - cannot turn the swap into the same fatal error. --- src/slic3r/GUI/GLCanvas3D.cpp | 25 ++++++++++++++++++++++--- src/slic3r/GUI/GLCanvas3D.hpp | 7 +++++++ src/slic3r/GUI/UVEditorCanvas.cpp | 18 +++++++++++++++++- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index c509e5d6f8..2e429dda20 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1514,6 +1514,8 @@ wxDEFINE_EVENT(EVT_GLCANVAS_PRINTABLE, SimpleEvent); const double GLCanvas3D::DefaultCameraZoomToBoxMarginFactor = 1.25; const double GLCanvas3D::DefaultCameraZoomToBedMarginFactor = 2.00; const double GLCanvas3D::DefaultCameraZoomToPlateMarginFactor = 1.25; +// Main-thread only: _render_frame never runs off it, so no atomic is needed. +int GLCanvas3D::s_frames_in_progress = 0; void GLCanvas3D::load_arrange_settings() { @@ -2468,7 +2470,8 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init) } m_in_render = true; - Slic3r::ScopeGuard in_render_guard([this]() { m_in_render = false; }); + ++s_frames_in_progress; + Slic3r::ScopeGuard in_render_guard([this]() { m_in_render = false; --s_frames_in_progress; }); (void)in_render_guard; if (m_canvas == nullptr) @@ -2713,15 +2716,31 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init) // On Wayland, eglSwapBuffers blocks when the canvas is hidden or // occluded. Skip the swap to avoid stalling the render loop. - if (m_canvas->IsShownOnScreen()) { + // Code run from inside this frame - a modal dialog a gizmo opened from its ImGui panel, a + // synchronous paint of another canvas - may have made a different surface current. Bind this one + // again before presenting, so the swap is not issued against a window the context no longer + // belongs to: on Mesa/X11 that is a fatal BadMatch, not the no-op it is on other platforms. Draws + // already issued before this point cannot be redirected - only this swap is protected. + if (m_canvas->IsShownOnScreen() && _set_current()) { m_canvas->SwapBuffers(); m_frame_profiler.mark("swap"); if (!overlay_tick) m_render_stats.increment_fps_counter(); m_presented_signature = signature; } - else + else { + if (m_canvas->IsShownOnScreen()) { + // Logged once: if the rebind fails it does so every frame, and a warning per frame would + // bury everything else. + static bool rebind_failure_logged = false; + if (!rebind_failure_logged) { + rebind_failure_logged = true; + BOOST_LOG_TRIVIAL(warning) << "GLCanvas3D: could not rebind the GL context to this canvas before " + "SwapBuffers(); the frame is not presented"; + } + } m_presented_signature.reset(); + } } // Everything drawn into the 3D scene, from the clear to the post processing passes, ending in the diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index bb148234d1..2cb6bcea4f 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -643,6 +643,8 @@ private: #endif unsigned int m_last_w, m_last_h; bool m_in_render; + // How many canvases are inside _render_frame() right now - see is_frame_in_progress(). + static int s_frames_in_progress; wxTimer m_timer; wxTimer m_timer_set_color; // Armed by each frame that draws the FPS overlay; its tick requests an overlay-only frame. @@ -974,6 +976,11 @@ public: void on_change_color_mode(bool is_dark, bool reinit = true); const bool get_dark_mode_status() { return m_is_dark; } void set_as_dirty() { m_dirty = true; } + // True while any GLCanvas3D is inside _render_frame(). The other surfaces that share the app's one + // GL context (the UV editor canvas) check this before making themselves current: a frame interrupted + // by another surface finishes drawing into that surface and then presents a window the context no + // longer belongs to, which on Mesa/X11 is a fatal X error (BadMatch) rather than a no-op. + static bool is_frame_in_progress() { return s_frames_in_progress > 0; } void requires_check_outside_state() { m_requires_check_outside_state = true; } unsigned int get_volumes_count() const { return (unsigned int)m_volumes.volumes.size(); } diff --git a/src/slic3r/GUI/UVEditorCanvas.cpp b/src/slic3r/GUI/UVEditorCanvas.cpp index a4a9901822..87f629816f 100644 --- a/src/slic3r/GUI/UVEditorCanvas.cpp +++ b/src/slic3r/GUI/UVEditorCanvas.cpp @@ -36,6 +36,7 @@ #include "3DScene.hpp" #include "BitmapCache.hpp" +#include "GLCanvas3D.hpp" #include "GLShader.hpp" #include "GUI_App.hpp" #include "I18N.hpp" @@ -1324,8 +1325,13 @@ void UVEditorCanvas::on_size(wxSizeEvent &evt) // Update() flushes it immediately - still through the normal paint path (unlike calling // render() directly), which matters because this canvas shares the app's one GL context with // the 3D view and must not hijack it outside its own paint. + // Not while a 3D canvas frame is in progress, though: wxGTK delivers this size event synchronously + // when the texture displacement gizmo relays out the pane from its ImGui panel, i.e. from inside + // that frame, and flushing the paint there would take the shared context away from it. The Refresh() + // then runs the paint once the frame is over. Refresh(); - Update(); + if (!GLCanvas3D::is_frame_in_progress()) + Update(); } void UVEditorCanvas::on_leave(wxMouseEvent &evt) @@ -1346,6 +1352,16 @@ void UVEditorCanvas::render() { if (m_context == nullptr || !IsShownOnScreen()) return; + // Painted from inside a 3D canvas frame - a modal dialog the gizmo opened from its ImGui panel runs + // this paint, as does any other synchronous event raised there. Making this surface current now + // would take the shared context away from that canvas mid-frame: its remaining draws would land + // here and, on Mesa/X11, its SwapBuffers() would be a fatal BadMatch. Skip, but ask for another + // paint so the skipped one is not simply lost. The pane stays stale for as long as a nested modal + // loop keeps a frame on the stack, which is behind the dialog anyway. + if (GLCanvas3D::is_frame_in_progress()) { + Refresh(); + return; + } // wxGLCanvas::SetCurrent() genuinely fails (returns false) on a canvas that isn't shown on // screen, and can also fail on a pixel-format mismatch with the context - and every GL call