From a167702038496b97e8495cc79f0c62660063ac87 Mon Sep 17 00:00:00 2001 From: Thomas Henauer Date: Tue, 12 May 2026 18:01:10 +0200 Subject: [PATCH] Linux: use GTK sink for Wayland Bambu liveview (#13432) * Use GTK sink for Wayland liveview Keep native Wayland sessions on the GTK backend and use a GTK widget based GStreamer sink for Bambu liveview instead of the Wayland video overlay path, which can render black on NVIDIA/Hyprland. Keep the existing wxMediaCtrl path for X11 and continue preferring software H.264 decoding while demoting GL and hardware decoder paths that caused liveview crashes. * Narrow Linux liveview fix to native Wayland * Tighten native Wayland liveview setup * Tighten Wayland liveview teardown and rank setup * Package GStreamer gtksink for Wayland liveview --------- Co-authored-by: SoftFever --- scripts/flatpak/com.orcaslicer.OrcaSlicer.yml | 3 +- scripts/linux.d/arch | 1 + scripts/linux.d/cachyos | 1 + scripts/linux.d/debian | 1 + scripts/linux.d/fedora | 1 + scripts/linux.d/gentoo | 1 + scripts/linux.d/suse | 1 + src/slic3r/GUI/MediaPlayCtrl.cpp | 10 +- src/slic3r/GUI/wxMediaCtrl2.cpp | 371 +++++++++++++++++- src/slic3r/GUI/wxMediaCtrl2.h | 22 ++ 10 files changed, 397 insertions(+), 15 deletions(-) diff --git a/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml b/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml index 772c59b17a..bb42b48607 100644 --- a/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml +++ b/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml @@ -35,12 +35,13 @@ finish-args: modules: - # JPEG codec for the liveview + # JPEG codec and GTK video sink (Wayland liveview) for the liveview - name: gst-plugins-good buildsystem: meson config-opts: - -Dauto_features=disabled - -Djpeg=enabled + - -Dgtk3=enabled - -Ddoc=disabled - -Dexamples=disabled - -Dtests=disabled diff --git a/scripts/linux.d/arch b/scripts/linux.d/arch index 68e8873552..c9342f561e 100644 --- a/scripts/linux.d/arch +++ b/scripts/linux.d/arch @@ -12,6 +12,7 @@ export REQUIRED_DEV_PACKAGES=( gettext git glew + gst-plugins-good gstreamer gstreamermm gtk3 diff --git a/scripts/linux.d/cachyos b/scripts/linux.d/cachyos index 9557c55e62..d491747ace 100644 --- a/scripts/linux.d/cachyos +++ b/scripts/linux.d/cachyos @@ -12,6 +12,7 @@ export REQUIRED_DEV_PACKAGES=( gettext git glew + gst-plugins-good gstreamer gtk3 libmspack diff --git a/scripts/linux.d/debian b/scripts/linux.d/debian index b3169718c8..f4d500760e 100644 --- a/scripts/linux.d/debian +++ b/scripts/linux.d/debian @@ -10,6 +10,7 @@ REQUIRED_DEV_PACKAGES=( g++ gettext git + gstreamer1.0-gtk3 libcurl4-openssl-dev libdbus-1-dev libglew-dev diff --git a/scripts/linux.d/fedora b/scripts/linux.d/fedora index 92e4c6155c..ca4eceaee3 100644 --- a/scripts/linux.d/fedora +++ b/scripts/linux.d/fedora @@ -13,6 +13,7 @@ REQUIRED_DEV_PACKAGES=( gettext git gstreamer1-devel + gstreamer1-plugins-good-gtk gstreamermm-devel gtk3-devel libmspack-devel diff --git a/scripts/linux.d/gentoo b/scripts/linux.d/gentoo index 017c9c6254..f172ac92c3 100644 --- a/scripts/linux.d/gentoo +++ b/scripts/linux.d/gentoo @@ -21,6 +21,7 @@ REQUIRED_DEV_PACKAGES=( media-libs/glew media-libs/gst-plugins-base:1.0 media-libs/gstreamer:1.0 + media-plugins/gst-plugins-gtk:1.0 net-misc/curl net-misc/wget sys-apps/dbus diff --git a/scripts/linux.d/suse b/scripts/linux.d/suse index a0a1da605a..1f4db45f46 100644 --- a/scripts/linux.d/suse +++ b/scripts/linux.d/suse @@ -13,6 +13,7 @@ REQUIRED_DEV_PACKAGES=( gettext git gstreamer-devel + gstreamer-plugins-good-gtk gtk3-devel libmspack-devel libquadmath-devel diff --git a/src/slic3r/GUI/MediaPlayCtrl.cpp b/src/slic3r/GUI/MediaPlayCtrl.cpp index 98b9fd797b..0a6902e11d 100644 --- a/src/slic3r/GUI/MediaPlayCtrl.cpp +++ b/src/slic3r/GUI/MediaPlayCtrl.cpp @@ -32,7 +32,8 @@ static std::map error_messages = { {100, L("The player is not loaded, please click \"play\" button to retry.")}, {101, L("The player is not loaded, please click \"play\" button to retry.")}, {102, L("The player is not loaded, please click \"play\" button to retry.")}, - {103, L("The player is not loaded, please click \"play\" button to retry.")} + {103, L("The player is not loaded, please click \"play\" button to retry.")}, + {104, L("The player is not loaded because the GStreamer GTK video sink is missing or failed to initialize.")} }; namespace Slic3r { @@ -838,6 +839,12 @@ void wxMediaCtrl2::DoSetSize(int x, int y, int width, int height, int sizeFlags) wxWindow::DoSetSize(x, y, width, height, sizeFlags); #else wxMediaCtrl::DoSetSize(x, y, width, height, sizeFlags); +#endif +#if defined(__LINUX__) && defined(__WXGTK__) + if (m_gtk_video_window) { + const wxSize client_size = GetClientSize(); + m_gtk_video_window->SetSize(0, 0, client_size.GetWidth(), client_size.GetHeight()); + } #endif if (sizeFlags & wxSIZE_USE_EXISTING) return; wxSize size = m_video_size; @@ -853,4 +860,3 @@ void wxMediaCtrl2::DoSetSize(int x, int y, int width, int height, int sizeFlags) }); } } - diff --git a/src/slic3r/GUI/wxMediaCtrl2.cpp b/src/slic3r/GUI/wxMediaCtrl2.cpp index bae815ae0f..2c4b789e12 100644 --- a/src/slic3r/GUI/wxMediaCtrl2.cpp +++ b/src/slic3r/GUI/wxMediaCtrl2.cpp @@ -2,7 +2,9 @@ #include "libslic3r/Time.hpp" #include "I18N.hpp" #include "GUI_App.hpp" +#include "LinuxDisplayBackend.hpp" #include +#include #ifdef __WIN32__ #include #include @@ -13,6 +15,78 @@ #ifdef __LINUX__ #include "Printer/gstbambusrc.h" #include // main gstreamer header +#endif + +#if defined(__LINUX__) && defined(__WXGTK__) +#include +#include + +namespace { +bool ensure_gstreamer_initialized_for_liveview() +{ + GError* error = nullptr; + if (!gst_init_check(nullptr, nullptr, &error)) { + BOOST_LOG_TRIVIAL(error) << "wxMediaCtrl2: gst_init_check failed before native Wayland liveview setup" + << (error ? std::string(": ") + error->message : std::string()); + if (error) + g_error_free(error); + return false; + } + + return true; +} + +bool is_gstreamer_feature_available(const char* feature) +{ + if (!ensure_gstreamer_initialized_for_liveview()) + return false; + + GstElementFactory* factory = gst_element_factory_find(feature); + if (!factory) + return false; + + gst_object_unref(factory); + return true; +} + +void set_gstreamer_feature_rank(const char* feature, guint rank) +{ + GstElementFactory* factory = gst_element_factory_find(feature); + if (!factory) + return; + + gst_plugin_feature_set_rank(GST_PLUGIN_FEATURE(factory), rank); + gst_object_unref(factory); +} + +void configure_wayland_gstreamer_liveview_path() +{ + static bool configured = false; + if (configured) + return; + configured = true; + + if (!ensure_gstreamer_initialized_for_liveview()) + return; + + // Prefer software decode for Bambu liveview on Wayland/NVIDIA, where + // zero-copy GL/DMABUF display paths can be fragile. Keep hardware + // decoders available as lower-ranked fallbacks for VAAPI/NVDEC/V4L2-only + // installations instead of passing preflight and then blocking autoplug. + set_gstreamer_feature_rank("avdec_h264", GST_RANK_PRIMARY + 300); + set_gstreamer_feature_rank("openh264dec", GST_RANK_PRIMARY + 100); + set_gstreamer_feature_rank("nvh264dec", GST_RANK_MARGINAL); + set_gstreamer_feature_rank("vaapih264dec", GST_RANK_MARGINAL); + set_gstreamer_feature_rank("vah264dec", GST_RANK_MARGINAL); + set_gstreamer_feature_rank("v4l2h264dec", GST_RANK_MARGINAL); +} +} + +#endif // defined(__LINUX__) && defined(__WXGTK__) + +#ifdef __LINUX__ +extern "C" int gst_bambu_last_error; + class WXDLLIMPEXP_MEDIA wxGStreamerMediaBackend : public wxMediaBackendCommonBase { @@ -25,6 +99,15 @@ wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent); wxMediaCtrl2::wxMediaCtrl2(wxWindow *parent) { +#if defined(__LINUX__) && defined(__WXGTK__) + m_native_wayland = Slic3r::GUI::is_running_on_wayland(); + if (m_native_wayland && is_gstreamer_feature_available("gtksink")) + configure_wayland_gstreamer_liveview_path(); + else if (m_native_wayland) { + m_gtk_sink_error = _L("Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer."); + BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: native Wayland liveview disabled because GStreamer gtksink is unavailable"; + } +#endif #ifdef __WIN32__ auto hModExe = GetModuleHandle(NULL); // BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl2: GetModuleHandle " << hModExe; @@ -39,20 +122,202 @@ wxMediaCtrl2::wxMediaCtrl2(wxWindow *parent) *AmdPowerXpressRequestHighPerformance = 0; } #endif - wxMediaCtrl::Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxMEDIACTRLPLAYERCONTROLS_NONE); +#if defined(__LINUX__) && defined(__WXGTK__) + if (m_native_wayland) + wxControl::Create(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize); + else +#endif + wxMediaCtrl::Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxMEDIACTRLPLAYERCONTROLS_NONE); #ifdef __LINUX__ - /* Register only after we have created the wxMediaCtrl, since only then are we guaranteed to have fired up Gstreamer's plugin registry. */ - auto playbin = reinterpret_cast(m_imp)->m_playbin; - g_object_set (G_OBJECT (playbin), - "audio-sink", NULL, - NULL); gstbambusrc_register(); +#ifdef __WXGTK__ + if (m_native_wayland && m_gtk_sink_error.empty()) + m_use_gtk_sink = CreateGtkSinkPlayer(); + if (m_native_wayland && !m_use_gtk_sink && m_gtk_sink_error.empty()) + m_gtk_sink_error = _L("Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation."); +#endif + if (!m_use_gtk_sink && m_imp) { + auto playbin = reinterpret_cast(m_imp)->m_playbin; + g_object_set(G_OBJECT(playbin), + "audio-sink", nullptr, + nullptr); + } else if (!m_use_gtk_sink) { + BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: wxMediaCtrl backend is unavailable"; + } Bind(wxEVT_MEDIA_LOADED, [this](auto & e) { m_loaded = true; + wxMediaEvent event(wxEVT_MEDIA_STATECHANGED); + event.SetId(0); + event.SetEventObject(this); + wxPostEvent(this, event); }); #endif } +wxMediaCtrl2::~wxMediaCtrl2() +{ +#if defined(__LINUX__) && defined(__WXGTK__) + DestroyGtkSinkPlayer(); +#endif +} + +#if defined(__LINUX__) && defined(__WXGTK__) +bool wxMediaCtrl2::CreateGtkSinkPlayer() +{ + GstElement *playbin = gst_element_factory_make("playbin", "orca-wayland-gtk-playbin"); + if (!playbin) + return false; + + GError *error = nullptr; + GstElement *video_sink = gst_parse_bin_from_description( + "videoconvert ! videoscale ! video/x-raw,format=BGRx ! gtksink name=orca_wayland_gtksink sync=false", + TRUE, + &error); + if (!video_sink) { + BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: failed to create gtksink video bin" + << (error ? std::string(": ") + error->message : std::string()); + if (error) + g_error_free(error); + gst_object_unref(playbin); + return false; + } + + GstElement *gtk_sink = gst_bin_get_by_name(GST_BIN(video_sink), "orca_wayland_gtksink"); + if (!gtk_sink) { + BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: failed to find gtksink in video bin"; + gst_object_unref(video_sink); + gst_object_unref(playbin); + return false; + } + + GtkWidget *gtk_widget = nullptr; + g_object_get(G_OBJECT(gtk_sink), "widget", >k_widget, nullptr); + if (!gtk_widget) { + BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: gtksink did not expose a GtkWidget"; + gst_object_unref(gtk_sink); + gst_object_unref(video_sink); + gst_object_unref(playbin); + return false; + } + + gtk_widget_show(gtk_widget); + m_gtk_video_window = new wxNativeWindow(this, wxID_ANY, gtk_widget); + m_gtk_video_window->Show(); + g_object_unref(gtk_widget); + + g_object_set(G_OBJECT(playbin), + "video-sink", video_sink, + "audio-sink", nullptr, + nullptr); + gst_object_unref(video_sink); + + m_gtk_playbin = playbin; + m_gtk_sink = gtk_sink; + + GstBus *bus = gst_element_get_bus(playbin); + m_gtk_bus_watch_id = gst_bus_add_watch(bus, [](GstBus *, GstMessage *message, gpointer data) -> gboolean { + auto *self = static_cast(data); + if (!self || !self->m_gtk_playbin) + return G_SOURCE_REMOVE; + + switch (GST_MESSAGE_TYPE(message)) { + case GST_MESSAGE_ERROR: + { + GError *error = nullptr; + gchar *debug = nullptr; + gst_message_parse_error(message, &error, &debug); + BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: gtksink pipeline error" + << (error ? std::string(": ") + error->message : std::string()) + << (debug ? std::string(" debug: ") + debug : std::string()); + if (error) + g_error_free(error); + if (debug) + g_free(debug); + + self->m_error = gst_bambu_last_error ? gst_bambu_last_error : 2; + self->m_loaded = false; + self->m_gtk_state = wxMEDIASTATE_STOPPED; + self->PostGtkSinkStateEvent(self->GetId()); + break; + } + case GST_MESSAGE_EOS: + self->m_loaded = false; + self->m_gtk_state = wxMEDIASTATE_STOPPED; + self->PostGtkSinkStateEvent(self->GetId()); + break; + case GST_MESSAGE_STATE_CHANGED: + if (GST_MESSAGE_SRC(message) == GST_OBJECT(self->m_gtk_playbin)) { + GstState old_state; + GstState new_state; + GstState pending_state; + gst_message_parse_state_changed(message, &old_state, &new_state, &pending_state); + + if (new_state == GST_STATE_PLAYING) { + self->m_loaded = true; + self->m_gtk_state = wxMEDIASTATE_PLAYING; + self->PostGtkSinkStateEvent(); + } else if (new_state == GST_STATE_PAUSED && old_state < GST_STATE_PAUSED) { + // Treat only upward READY/NULL -> PAUSED as load completion. + // PLAYING -> PAUSED is a normal teardown step before NULL. + self->m_loaded = true; + self->m_gtk_state = wxMEDIASTATE_PAUSED; + self->PostGtkSinkStateEvent(); + } else if (new_state <= GST_STATE_READY && old_state >= GST_STATE_PAUSED) { + self->m_loaded = false; + self->m_gtk_state = wxMEDIASTATE_STOPPED; + self->PostGtkSinkStateEvent(); + } + } + break; + default: + break; + } + + return G_SOURCE_CONTINUE; + }, this); + gst_object_unref(bus); + + BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl2: using GTK native Wayland video sink"; + return true; +} + +void wxMediaCtrl2::DestroyGtkSinkPlayer() +{ + if (m_gtk_bus_watch_id) { + g_source_remove(m_gtk_bus_watch_id); + m_gtk_bus_watch_id = 0; + } + + if (m_gtk_playbin) { + gst_element_set_state(m_gtk_playbin, GST_STATE_NULL); + } + + if (m_gtk_video_window) { + m_gtk_video_window->Destroy(); + m_gtk_video_window = nullptr; + } + + if (m_gtk_playbin) { + gst_object_unref(m_gtk_playbin); + m_gtk_playbin = nullptr; + } + + if (m_gtk_sink) { + gst_object_unref(m_gtk_sink); + m_gtk_sink = nullptr; + } + +} + +void wxMediaCtrl2::PostGtkSinkStateEvent(int id) +{ + wxMediaEvent event(wxEVT_MEDIA_STATECHANGED); + event.SetId(id); + event.SetEventObject(this); + wxPostEvent(this, event); +} +#endif // defined(__LINUX__) && defined(__WXGTK__) + #define CLSID_BAMBU_SOURCE L"{233E64FB-2041-4A6C-AFAB-FF9BCF83E7AA}" void wxMediaCtrl2::Load(wxURI url) @@ -178,6 +443,15 @@ void wxMediaCtrl2::Load(wxURI url) if (!factory) { factory = gst_element_factory_find("vaapih264dec"); } + if (!factory) { + factory = gst_element_factory_find("vah264dec"); + } + if (!factory) { + factory = gst_element_factory_find("nvh264dec"); + } + if (!factory) { + factory = gst_element_factory_find("v4l2h264dec"); + } if (!factory) { hasplugins = 0; } else { @@ -199,30 +473,103 @@ void wxMediaCtrl2::Load(wxURI url) #endif m_error = 0; m_loaded = false; - wxMediaCtrl::Load(url); - -#ifdef __WXGTK3__ +#if defined(__LINUX__) && defined(__WXGTK__) + if (m_use_gtk_sink && m_gtk_playbin) { + const std::string uri = std::string(url.BuildURI().ToUTF8().data()); + gst_element_set_state(m_gtk_playbin, GST_STATE_NULL); + g_object_set(G_OBJECT(m_gtk_playbin), "uri", uri.c_str(), nullptr); + m_gtk_state = wxMEDIASTATE_STOPPED; + GstStateChangeReturn state = gst_element_set_state(m_gtk_playbin, GST_STATE_PAUSED); + if (state == GST_STATE_CHANGE_FAILURE) { + m_error = gst_bambu_last_error ? gst_bambu_last_error : 2; + PostGtkSinkStateEvent(GetId()); + } + return; + } + if (!m_imp) { + m_error = m_native_wayland && !m_gtk_sink_error.empty() ? 104 : 100; + m_loaded = false; + if (m_native_wayland && !m_gtk_sink_error.empty() && !m_gtk_sink_error_notified) { + m_gtk_sink_error_notified = true; + const wxString message = m_gtk_sink_error; + CallAfter([message] { + wxMessageBox(message, _L("Error"), wxOK); + }); + } wxMediaEvent event(wxEVT_MEDIA_STATECHANGED); event.SetId(GetId()); event.SetEventObject(this); wxPostEvent(this, event); + return; + } #endif + wxMediaCtrl::Load(url); } -void wxMediaCtrl2::Play() { wxMediaCtrl::Play(); } +void wxMediaCtrl2::Play() +{ +#if defined(__LINUX__) && defined(__WXGTK__) + if (m_use_gtk_sink && m_gtk_playbin) { + GstStateChangeReturn state = gst_element_set_state(m_gtk_playbin, GST_STATE_PLAYING); + if (state == GST_STATE_CHANGE_FAILURE) { + m_error = gst_bambu_last_error ? gst_bambu_last_error : 2; + m_gtk_state = wxMEDIASTATE_STOPPED; + PostGtkSinkStateEvent(GetId()); + } + return; + } + if (!m_imp) { + m_error = m_native_wayland && !m_gtk_sink_error.empty() ? 104 : 100; + if (m_native_wayland && !m_gtk_sink_error.empty() && !m_gtk_sink_error_notified) { + m_gtk_sink_error_notified = true; + const wxString message = m_gtk_sink_error; + CallAfter([message] { + wxMessageBox(message, _L("Error"), wxOK); + }); + } + PostGtkSinkStateEvent(GetId()); + return; + } +#endif + wxMediaCtrl::Play(); +} void wxMediaCtrl2::Stop() { +#if defined(__LINUX__) && defined(__WXGTK__) + if (m_use_gtk_sink && m_gtk_playbin) { + gst_element_set_state(m_gtk_playbin, GST_STATE_NULL); + m_gtk_state = wxMEDIASTATE_STOPPED; + m_loaded = false; + PostGtkSinkStateEvent(0); + return; + } + if (!m_imp) + return; +#endif wxMediaCtrl::Stop(); } -#ifdef __LINUX__ -extern "C" int gst_bambu_last_error; +wxMediaState wxMediaCtrl2::GetState() +{ +#if defined(__LINUX__) && defined(__WXGTK__) + if (m_use_gtk_sink && m_gtk_playbin) + return m_gtk_state; + if (!m_imp) + return wxMEDIASTATE_STOPPED; #endif + return wxMediaCtrl::GetState(); +} int wxMediaCtrl2::GetLastError() const { #ifdef __LINUX__ +#ifdef __WXGTK__ + if (m_use_gtk_sink && m_error) + return m_error; +#endif + if (m_error) + return m_error; return gst_bambu_last_error; #else return m_error; diff --git a/src/slic3r/GUI/wxMediaCtrl2.h b/src/slic3r/GUI/wxMediaCtrl2.h index c84ad7a833..4f37b5cd1e 100644 --- a/src/slic3r/GUI/wxMediaCtrl2.h +++ b/src/slic3r/GUI/wxMediaCtrl2.h @@ -13,6 +13,10 @@ wxDECLARE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent); +#if defined(__LINUX__) && defined(__WXGTK__) +typedef struct _GstElement GstElement; +#endif + #ifdef __WXMAC__ class wxMediaCtrl2 : public wxWindow @@ -59,6 +63,7 @@ class wxMediaCtrl2 : public wxMediaCtrl { public: wxMediaCtrl2(wxWindow *parent); + ~wxMediaCtrl2(); void Load(wxURI url); @@ -68,6 +73,8 @@ public: void SetIdleImage(wxString const & image); + wxMediaState GetState(); + int GetLastError() const; wxSize GetVideoSize() const; @@ -84,6 +91,21 @@ protected: #endif private: +#if defined(__LINUX__) && defined(__WXGTK__) + bool CreateGtkSinkPlayer(); + void DestroyGtkSinkPlayer(); + void PostGtkSinkStateEvent(int id = 0); + + bool m_native_wayland = false; + bool m_use_gtk_sink = false; + wxString m_gtk_sink_error; + bool m_gtk_sink_error_notified = false; + GstElement *m_gtk_playbin = nullptr; + GstElement *m_gtk_sink = nullptr; + unsigned int m_gtk_bus_watch_id = 0; + wxWindow *m_gtk_video_window = nullptr; + wxMediaState m_gtk_state = wxMEDIASTATE_STOPPED; +#endif wxString m_idle_image; int m_error = 0; bool m_loaded = false;