From 6441fbb6dda23c7cc5bf168778f5aca4273349fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?micha=C5=82=20targiel?= Date: Wed, 7 Oct 2026 04:20:20 +0200 Subject: [PATCH 1/3] Fix printer metadata staging and integer line-radius queries --- src/libslic3r/AABBTreeLines.hpp | 12 ++--- src/slic3r/GUI/PartPlate.cpp | 4 ++ src/slic3r/Utils/PresetUpdater.cpp | 11 +++++ .../Utils/PrinterMetadataValidation.hpp | 24 ++++++++++ tests/crash_regressions/CMakeLists.txt | 23 ++++++++++ tests/crash_regressions/LineRadius_test.cpp | 18 ++++++++ tests/slic3rutils/CMakeLists.txt | 7 +++ .../PrinterMetadataValidation_test.cpp | 45 +++++++++++++++++++ 8 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 src/slic3r/Utils/PrinterMetadataValidation.hpp create mode 100644 tests/crash_regressions/CMakeLists.txt create mode 100644 tests/crash_regressions/LineRadius_test.cpp create mode 100644 tests/slic3rutils/PrinterMetadataValidation_test.cpp diff --git a/src/libslic3r/AABBTreeLines.hpp b/src/libslic3r/AABBTreeLines.hpp index 55cf328f57fd..98ccafc1048a 100644 --- a/src/libslic3r/AABBTreeLines.hpp +++ b/src/libslic3r/AABBTreeLines.hpp @@ -32,7 +32,8 @@ namespace AABBTreeLines { { Vec nearest_point; const LineType& line = lines[primitive_index]; - squared_distance = line_alg::distance_to_squared(line, origin.template cast(), &nearest_point); + squared_distance = line_alg::distance_to_squared( + line, origin.template cast(), &nearest_point); return nearest_point.template cast(); } }; @@ -351,10 +352,11 @@ namespace AABBTreeLines { return dist; } - std::vector all_lines_in_radius(const Vec<2, Scalar> &point, Floating radius) - { - return AABBTreeLines::all_lines_in_radius(this->lines, this->tree, point.template cast(), radius * radius); - } + std::vector all_lines_in_radius(const Vec<2, Scalar> &point, Floating radius) + { + // Store a vector value, not Eigen's lazy cast expression, in the distancer. + return AABBTreeLines::all_lines_in_radius(this->lines, this->tree, point.template cast().eval(), radius * radius); + } template std::vector, size_t>> intersections_with_line(const LineType& line) const diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index eee744434309..cb9cc406b6ee 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -749,6 +749,10 @@ void PartPlate::calc_exclude_triangles(const ExPolygon &poly) { m_exclude_triangles.reset(); + // A single 0x0 point denotes no excluded area in printer profiles. + if (poly.contour.points.size() < 3) + return; + if (!init_model_from_poly(m_exclude_triangles, poly, GROUND_Z)) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ":Unable to create exclude triangles\n"; } diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index c10a09895d47..c6111fd7d12b 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -1,4 +1,5 @@ #include "PresetUpdater.hpp" +#include "PrinterMetadataValidation.hpp" #include #include @@ -117,6 +118,8 @@ void copy_file_fix(const fs::path &source, const fs::path &target) struct Update { + enum class DirectoryKind { PresetBundle, PrinterMetadata }; + DirectoryKind directory_kind { DirectoryKind::PresetBundle }; fs::path source; fs::path target; @@ -167,6 +170,13 @@ struct Update { if (is_directory) { auto validate_staging = [this](const fs::path &staging) -> bool { + if (directory_kind == DirectoryKind::PrinterMetadata) { + if (!validate_printer_metadata_directory(staging)) { + BOOST_LOG_TRIVIAL(error) << "Printer metadata staging is missing version.txt or model JSON files"; + return false; + } + return true; + } if (vendor == "flutter_web") { if (!fs::exists(staging / "version.json")) { BOOST_LOG_TRIVIAL(error) << "Web resource staging is missing version.json"; @@ -1702,6 +1712,7 @@ Updates PresetUpdater::priv::get_printer_config_updates(bool update) const } catch (...) {} } updates.updates.emplace_back(std::move(resc_folder), std::move(config_folder), version, "bbl", change_log, version.comment, false, true); + updates.updates.back().directory_kind = Update::DirectoryKind::PrinterMetadata; return updates; } diff --git a/src/slic3r/Utils/PrinterMetadataValidation.hpp b/src/slic3r/Utils/PrinterMetadataValidation.hpp new file mode 100644 index 000000000000..48036903e368 --- /dev/null +++ b/src/slic3r/Utils/PrinterMetadataValidation.hpp @@ -0,0 +1,24 @@ +#ifndef slic3r_PrinterMetadataValidation_hpp_ +#define slic3r_PrinterMetadataValidation_hpp_ + +#include + +namespace Slic3r { + +// resources/printers contains flat model metadata, not preset bundles. +inline bool validate_printer_metadata_directory(const boost::filesystem::path& directory) +{ + namespace fs = boost::filesystem; + if (!fs::is_directory(directory) || !fs::is_regular_file(directory / "version.txt")) + return false; + + for (const auto& entry : fs::directory_iterator(directory)) { + if (fs::is_regular_file(entry.path()) && entry.path().extension() == ".json") + return true; + } + return false; +} + +} // namespace Slic3r + +#endif diff --git a/tests/crash_regressions/CMakeLists.txt b/tests/crash_regressions/CMakeLists.txt new file mode 100644 index 000000000000..ecd6f2ccbc5f --- /dev/null +++ b/tests/crash_regressions/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.16) +project(SnapmakerCrashRegressions LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +get_filename_component(ORCA_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE) +enable_testing() +include("${ORCA_SOURCE_DIR}/cmake/catch2.cmake") +find_package(Boost 1.84 REQUIRED COMPONENTS filesystem) + +add_executable(printer_metadata_validation_tests + "${ORCA_SOURCE_DIR}/tests/slic3rutils/PrinterMetadataValidation_test.cpp") +target_include_directories(printer_metadata_validation_tests PRIVATE "${ORCA_SOURCE_DIR}/src") +target_compile_definitions(printer_metadata_validation_tests PRIVATE TEST_RESOURCES_DIR="${ORCA_SOURCE_DIR}/resources") +target_link_libraries(printer_metadata_validation_tests PRIVATE Catch2::Catch2WithMain Boost::filesystem) +catch_discover_tests(printer_metadata_validation_tests) + +add_executable(line_radius_tests LineRadius_test.cpp) +target_include_directories(line_radius_tests PRIVATE + "${ORCA_SOURCE_DIR}/src" + "${ORCA_SOURCE_DIR}/deps_src" + "${ORCA_SOURCE_DIR}/deps_src/eigen" + "${ORCA_SOURCE_DIR}/deps_src/libigl") +target_link_libraries(line_radius_tests PRIVATE Catch2::Catch2WithMain Boost::filesystem) +catch_discover_tests(line_radius_tests) diff --git a/tests/crash_regressions/LineRadius_test.cpp b/tests/crash_regressions/LineRadius_test.cpp new file mode 100644 index 000000000000..44d0a1c28455 --- /dev/null +++ b/tests/crash_regressions/LineRadius_test.cpp @@ -0,0 +1,18 @@ +#include + +#include "libslic3r/AABBTreeLines.hpp" + +TEST_CASE("Integer-coordinate line queries find only segments within the radius", "[geometry][line-radius]") +{ + using namespace Slic3r; + AABBTreeLines::LinesDistancer distancer(Lines{Line(Point(0, 0), Point(100, 0)), Line(Point(0, 100), Point(100, 100))}); + const auto nearby = distancer.all_lines_in_radius(Point(50, 3), 5.0); + REQUIRE(nearby == std::vector{0}); + REQUIRE(distancer.all_lines_in_radius(Point(50, 50), 5.0).empty()); +} + +TEST_CASE("Empty line queries return no segments", "[geometry][line-radius]") +{ + Slic3r::AABBTreeLines::LinesDistancer distancer; + REQUIRE(distancer.all_lines_in_radius(Slic3r::Point(0, 0), 5.0).empty()); +} diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index 861ea88cf004..1026f987e194 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -4,6 +4,13 @@ if (NOT SLIC3R_GUI) return() endif() +add_executable(printer_metadata_validation_tests PrinterMetadataValidation_test.cpp) +target_include_directories(printer_metadata_validation_tests PRIVATE ${PROJECT_SOURCE_DIR}/src) +target_compile_definitions(printer_metadata_validation_tests PRIVATE + TEST_RESOURCES_DIR="${PROJECT_SOURCE_DIR}/resources") +target_link_libraries(printer_metadata_validation_tests PRIVATE Catch2::Catch2WithMain Boost::filesystem) +catch_discover_tests(printer_metadata_validation_tests TEST_PREFIX "printer_metadata: ") + get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp diff --git a/tests/slic3rutils/PrinterMetadataValidation_test.cpp b/tests/slic3rutils/PrinterMetadataValidation_test.cpp new file mode 100644 index 000000000000..ac1eef3f07bc --- /dev/null +++ b/tests/slic3rutils/PrinterMetadataValidation_test.cpp @@ -0,0 +1,45 @@ +#include + +#include "slic3r/Utils/PrinterMetadataValidation.hpp" + +#include + +namespace fs = boost::filesystem; + +namespace { +struct StagingDirectory +{ + fs::path path = fs::temp_directory_path() / fs::unique_path("orca-metadata-%%%%-%%%%"); + StagingDirectory() { fs::create_directories(path); } + ~StagingDirectory() { fs::remove_all(path); } + void write(const char* name, const char* content) { std::ofstream(path.string() + "/" + name) << content; } +}; +} // namespace + +TEST_CASE("Shipped flat printer metadata passes staging validation", "[updater][printer-metadata]") +{ REQUIRE(Slic3r::validate_printer_metadata_directory(fs::path(TEST_RESOURCES_DIR) / "printers")); } + +TEST_CASE("Incomplete printer metadata cannot replace the installed directory", "[updater][printer-metadata]") +{ + StagingDirectory staging; + REQUIRE_FALSE(Slic3r::validate_printer_metadata_directory(staging.path)); + staging.write("C11.json", "{}"); + REQUIRE_FALSE(Slic3r::validate_printer_metadata_directory(staging.path)); + staging.write("version.txt", "01.10.00.01"); + REQUIRE(Slic3r::validate_printer_metadata_directory(staging.path)); + fs::remove(staging.path / "C11.json"); + REQUIRE_FALSE(Slic3r::validate_printer_metadata_directory(staging.path)); +} + +TEST_CASE("Preset trees and directories named like JSON files are not printer metadata", "[updater][printer-metadata]") +{ + StagingDirectory staging; + staging.write("version.txt", "01.10.00.01"); + fs::create_directories(staging.path / "machine"); + fs::create_directories(staging.path / "C11.json"); + REQUIRE_FALSE(Slic3r::validate_printer_metadata_directory(staging.path)); + fs::remove(staging.path / "version.txt"); + fs::create_directories(staging.path / "version.txt"); + staging.write("C12.json", "{}"); + REQUIRE_FALSE(Slic3r::validate_printer_metadata_directory(staging.path)); +} From c514c6f6d97e3c470b45bb3175a23367b69cfd93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?micha=C5=82=20targiel?= Date: Wed, 7 Oct 2026 04:20:20 +0200 Subject: [PATCH 2/3] Fix GTK popup teardown and no-result WebKit bridge scripts --- src/slic3r/GUI/HttpServer.cpp | 6 +- src/slic3r/GUI/SliceModePopup.cpp | 9 ++- src/slic3r/GUI/SliceModePopup.hpp | 2 + src/slic3r/GUI/Widgets/PopupWindow.cpp | 7 ++- src/slic3r/GUI/Widgets/PopupWindow.hpp | 2 + src/slic3r/GUI/Widgets/WebView.cpp | 36 +++++++++-- .../PopupLifecycle_smoke.cpp | 40 +++++++++++++ .../WebKitCompletion_smoke.cpp | 59 +++++++++++++++++++ 8 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 tests/crash_regressions/PopupLifecycle_smoke.cpp create mode 100644 tests/crash_regressions/WebKitCompletion_smoke.cpp diff --git a/src/slic3r/GUI/HttpServer.cpp b/src/slic3r/GUI/HttpServer.cpp index 5c35b0528ca9..60045be5ce93 100644 --- a/src/slic3r/GUI/HttpServer.cpp +++ b/src/slic3r/GUI/HttpServer.cpp @@ -479,7 +479,7 @@ boost::asio::ip::port_type HttpServer::find_available_port(boost::asio::ip::port // 尝试从起始端口开始查找可用端口 for (boost::asio::ip::port_type p = start_port; p < start_port + 1000; ++p) { if (is_port_available(p)) { - BOOST_LOG_TRIVIAL(error) << "use new port for start server:"< lock(m_health_check_mutex); if (!m_health_check_enabled) { - BOOST_LOG_TRIVIAL(error) << "Health check is not running"; + BOOST_LOG_TRIVIAL(debug) << "Health check is already stopped"; return; } @@ -1176,4 +1176,4 @@ void HttpServer::ResponseFile::write_response(std::stringstream& ssOut) ssOut << fileContent; // 响应体(长度必须与Content-Length一致) } -}} // namespace Slic3r::GUI \ No newline at end of file +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/SliceModePopup.cpp b/src/slic3r/GUI/SliceModePopup.cpp index c5171c5a7545..94c3b413168a 100644 --- a/src/slic3r/GUI/SliceModePopup.cpp +++ b/src/slic3r/GUI/SliceModePopup.cpp @@ -63,7 +63,9 @@ SliceModePopup::SliceModePopup(wxWindow *parent) Bind(wxEVT_LEFT_UP, &SliceModePopup::on_left_up, this); Bind(wxEVT_TIMER, &SliceModePopup::on_timer, this); #ifdef __WXGTK__ - wxGetTopLevelParent(parent)->Bind(wxEVT_ACTIVATE, &SliceModePopup::on_top_window_activate, this); + m_activation_source = wxGetTopLevelParent(parent); + if (m_activation_source) + m_activation_source->Bind(wxEVT_ACTIVATE, &SliceModePopup::on_top_window_activate, this); #endif update_metrics(); } @@ -71,7 +73,10 @@ SliceModePopup::SliceModePopup(wxWindow *parent) SliceModePopup::~SliceModePopup() { #ifdef __WXGTK__ - wxGetTopLevelParent(GetParent())->Unbind(wxEVT_ACTIVATE, &SliceModePopup::on_top_window_activate, this); + // DestroyChildren() can detach the popup before its destructor runs. + // Unbind from the original emitter, if it is still alive. + if (m_activation_source) + m_activation_source->Unbind(wxEVT_ACTIVATE, &SliceModePopup::on_top_window_activate, this); #endif } diff --git a/src/slic3r/GUI/SliceModePopup.hpp b/src/slic3r/GUI/SliceModePopup.hpp index 506bfcf28c92..bebd13029b34 100644 --- a/src/slic3r/GUI/SliceModePopup.hpp +++ b/src/slic3r/GUI/SliceModePopup.hpp @@ -3,6 +3,7 @@ #include #include +#include #include @@ -41,6 +42,7 @@ class SliceModePopup : public wxPopupWindow void on_timer(wxTimerEvent &evt); void update_metrics(); #ifdef __WXGTK__ + wxWeakRef m_activation_source; // The popup is an override-redirect window that stays on top of everything, // so hide it when the application loses activation (mirrors Widgets/PopupWindow). void on_top_window_activate(wxActivateEvent &evt); diff --git a/src/slic3r/GUI/Widgets/PopupWindow.cpp b/src/slic3r/GUI/Widgets/PopupWindow.cpp index 135802c83e1c..90fb491a1d60 100644 --- a/src/slic3r/GUI/Widgets/PopupWindow.cpp +++ b/src/slic3r/GUI/Widgets/PopupWindow.cpp @@ -16,7 +16,9 @@ bool PopupWindow::Create(wxWindow *parent, int style) if (!wxPopupTransientWindow::Create(parent, style)) return false; #ifdef __WXGTK__ - GetTopParent(parent)->Bind(wxEVT_ACTIVATE, &PopupWindow::topWindowActiavate, this); + m_activation_source = GetTopParent(parent); + if (m_activation_source) + m_activation_source->Bind(wxEVT_ACTIVATE, &PopupWindow::topWindowActiavate, this); #endif return true; } @@ -24,7 +26,8 @@ bool PopupWindow::Create(wxWindow *parent, int style) PopupWindow::~PopupWindow() { #ifdef __WXGTK__ - GetTopParent(this)->Unbind(wxEVT_ACTIVATE, &PopupWindow::topWindowActiavate, this); + if (m_activation_source) + m_activation_source->Unbind(wxEVT_ACTIVATE, &PopupWindow::topWindowActiavate, this); #endif } diff --git a/src/slic3r/GUI/Widgets/PopupWindow.hpp b/src/slic3r/GUI/Widgets/PopupWindow.hpp index 88993de88ef4..5f7d9d7bdc5e 100644 --- a/src/slic3r/GUI/Widgets/PopupWindow.hpp +++ b/src/slic3r/GUI/Widgets/PopupWindow.hpp @@ -2,6 +2,7 @@ #define slic3r_GUI_PopupWindow_hpp_ #include +#include class PopupWindow : public wxPopupTransientWindow { @@ -17,6 +18,7 @@ class PopupWindow : public wxPopupTransientWindow private: #ifdef __WXGTK__ + wxWeakRef m_activation_source; void topWindowActiavate(wxActivateEvent &event); #endif }; diff --git a/src/slic3r/GUI/Widgets/WebView.cpp b/src/slic3r/GUI/Widgets/WebView.cpp index a821ac30eca3..e5976dede30e 100644 --- a/src/slic3r/GUI/Widgets/WebView.cpp +++ b/src/slic3r/GUI/Widgets/WebView.cpp @@ -11,6 +11,8 @@ #include #elif defined(__WXMAC__) #include +#elif defined(__WXGTK__) +#include #endif #include #if defined(__WIN32__) || defined(__WXMAC__) @@ -30,7 +32,6 @@ #elif defined __linux__ #include #define WEBKIT_API -struct WebKitWebView; struct WebKitJavascriptResult; extern "C" { WEBKIT_API void @@ -291,6 +292,22 @@ class WebViewWebKit : public wxWebViewWebKit wxString m_queuedUrl; }; +#elif defined(__WXGTK__) + +class WebViewWebKit : public wxWebViewWebKit +{ +public: + bool RunScript(const wxString &javascript, wxString *output = nullptr) const override + { + if (output) + return wxWebViewWebKit::RunScript(javascript, output); + + // Bridge setup scripts can return native WebKit objects. There is no + // result to serialize when the caller only wants to execute a script. + return WebView::RunScript(const_cast(this), javascript); + } +}; + #endif class FakeWebView : public wxWebView @@ -377,6 +394,8 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url) wxWebView* webView = new WebViewEdge; #elif defined(__WXOSX__) wxWebView *webView = new WebViewWebKit(url2); +#elif defined(__WXGTK__) + wxWebView *webView = new WebViewWebKit; #else auto webView = wxWebView::New(); #endif @@ -491,6 +510,8 @@ void WebView::LoadUrl(wxWebView * webView, wxString const &url) bool WebView::RunScript(wxWebView *webView, wxString const &javascript) { + if (!webView || !webView->GetNativeBackend()) + return false; if (Slic3r::GUI::wxGetApp().app_config->get("internal_developer_mode") == "true" && javascript.find("studio_userlogin") == wxString::npos) wxLogMessage("Running JavaScript:\n%s\n", javascript); @@ -507,14 +528,19 @@ bool WebView::RunScript(wxWebView *webView, wxString const &javascript) return true; #else WebKitWebView *wkWebView = (WebKitWebView *) webView->GetNativeBackend(); + // Discard native message-handler objects that WebKit cannot return + // to the UI process when the caller does not request a result. + const wxString script_without_result = javascript + "\n;void 0;"; webkit_web_view_run_javascript( - wkWebView, javascript.utf8_str(), NULL, + wkWebView, script_without_result.utf8_str(), NULL, [](GObject *wkWebView, GAsyncResult *res, void *) { GError * error = NULL; auto result = webkit_web_view_run_javascript_finish((WebKitWebView*)wkWebView, res, &error); - if (!result) - g_error_free (error); - else + if (error) { + wxLogWarning("Error running JavaScript: %s", wxString::FromUTF8(error->message)); + g_error_free(error); + } + if (result) webkit_javascript_result_unref (result); }, NULL); return true; diff --git a/tests/crash_regressions/PopupLifecycle_smoke.cpp b/tests/crash_regressions/PopupLifecycle_smoke.cpp new file mode 100644 index 000000000000..e4c577970c52 --- /dev/null +++ b/tests/crash_regressions/PopupLifecycle_smoke.cpp @@ -0,0 +1,40 @@ +#include + +#include "slic3r/GUI/Widgets/PopupWindow.hpp" + +#include + +class LifecycleApp : public wxApp +{ +public: + bool OnInit() override { return true; } +}; +wxIMPLEMENT_APP_NO_MAIN(LifecycleApp); + +int main(int argc, char **argv) +{ + if (!wxEntryStart(argc, argv) || !wxTheApp->CallOnInit()) + return 1; + + // Normal parent destruction and detached children in both lifetime orders. + for (int order = 0; order < 3; ++order) { + auto *frame = new wxFrame(nullptr, wxID_ANY, "Popup lifecycle regression"); + auto *popup = new PopupWindow(frame); + if (order == 0) { + delete frame; + } else { + frame->RemoveChild(popup); + if (order == 1) { + delete popup; + delete frame; + } else { + delete frame; + delete popup; + } + } + } + std::puts("Parent destruction and both detached-popup lifetime orders: PASS"); + wxTheApp->OnExit(); + wxEntryCleanup(); + return 0; +} diff --git a/tests/crash_regressions/WebKitCompletion_smoke.cpp b/tests/crash_regressions/WebKitCompletion_smoke.cpp new file mode 100644 index 000000000000..6a65d03048c3 --- /dev/null +++ b/tests/crash_regressions/WebKitCompletion_smoke.cpp @@ -0,0 +1,59 @@ +#include +#include + +#include + +static bool passed = false; + +static void fixed_script_finished(GObject *source, GAsyncResult *result, gpointer) +{ + GError *error = nullptr; + auto *value = webkit_web_view_run_javascript_finish(WEBKIT_WEB_VIEW(source), result, &error); + passed = value && !error; + std::printf("Discarded completion value: %s\n", passed ? "PASS" : "FAIL"); + if (error) { + std::printf("%s\n", error->message); + g_error_free(error); + } + if (value) + webkit_javascript_result_unref(value); + gtk_main_quit(); +} + +static void original_script_finished(GObject *source, GAsyncResult *result, gpointer) +{ + GError *error = nullptr; + auto *value = webkit_web_view_run_javascript_finish(WEBKIT_WEB_VIEW(source), result, &error); + std::printf("Original native-object completion: %s\n", error ? error->message : "returned a value"); + if (error) + g_error_free(error); + if (value) + webkit_javascript_result_unref(value); + webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(source), + "window.crashTest = window.webkit.messageHandlers.crashTest;\n;void 0;", + nullptr, fixed_script_finished, nullptr); +} + +static void loaded(WebKitWebView *view, WebKitLoadEvent event, gpointer) +{ + if (event == WEBKIT_LOAD_FINISHED) + webkit_web_view_run_javascript(view, "window.crashTest = window.webkit.messageHandlers.crashTest;", + nullptr, original_script_finished, nullptr); +} + +int main(int argc, char **argv) +{ + gtk_init(&argc, &argv); + auto *window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + auto *view = WEBKIT_WEB_VIEW(webkit_web_view_new()); + webkit_user_content_manager_register_script_message_handler(webkit_web_view_get_user_content_manager(view), "crashTest"); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + gtk_window_set_default_size(GTK_WINDOW(window), 320, 200); + g_signal_connect(view, "load-changed", G_CALLBACK(loaded), nullptr); + g_timeout_add_seconds(15, [](gpointer) -> gboolean { gtk_main_quit(); return G_SOURCE_REMOVE; }, nullptr); + webkit_web_view_load_html(view, "WebKit script completion regression check", nullptr); + gtk_widget_show_all(window); + gtk_main(); + gtk_widget_destroy(window); + return passed ? 0 : 1; +} From 27b1df8f6d0f78f6138d353262eeb34d0c7a1dac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?micha=C5=82=20targiel?= Date: Wed, 7 Oct 2026 04:20:20 +0200 Subject: [PATCH 3/3] Add opt-in Linux rendering compatibility to shared launchers --- doc/Linux-render-compatibility.md | 84 +++++++++++++++++++++++++++++++ gitlab_pack_linux.sh | 4 +- pack_linux.sh | 4 +- resources/linux-launch-env.sh | 24 +++++++++ scripts/run-local-linux.sh | 7 +++ 5 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 doc/Linux-render-compatibility.md create mode 100644 resources/linux-launch-env.sh create mode 100755 scripts/run-local-linux.sh diff --git a/doc/Linux-render-compatibility.md b/doc/Linux-render-compatibility.md new file mode 100644 index 000000000000..691956b39ad4 --- /dev/null +++ b/doc/Linux-render-compatibility.md @@ -0,0 +1,84 @@ +# Linux startup and rendering compatibility + +The packaged Linux launchers and `scripts/run-local-linux.sh` share +`resources/linux-launch-env.sh`. It supplies the distribution CA bundle when +available and not already configured, and replaces the POSIX C locale with +`en_US.UTF-8` because Flutter requires a browser language tag. + +## Blank model canvas or NVIDIA EGL crashes + +An optional compatibility mode uses Mesa/Zink over Vulkan, the OpenGL 2.1 +rendering path, X11 (XWayland in a Wayland session), and disables WebKit's +DMA-BUF renderer. Install Mesa, its Zink driver, a working Vulkan driver, and +XWayland before enabling it. On Arch Linux the Mesa EGL vendor descriptor is +`/usr/share/glvnd/egl_vendor.d/50_mesa.json`; other distributions must provide +that descriptor as well for this mode. + +For a packaged build: + +```sh +ORCA_LINUX_RENDER_COMPAT=1 ./Snapmaker_Orca.AppImage +``` + +For a source build in the usual `build/` directory: + +```sh +ORCA_LINUX_RENDER_COMPAT=1 ./scripts/run-local-linux.sh project.3mf +``` + +Leave the variable unset to retain the normal rendering path and the existing +automatic NVIDIA/Wayland Zink detection in the packaged launchers. Compatibility +mode is a workaround, not a root-cause fix for the modern OpenGL path. It may +reduce performance or disable features requiring newer OpenGL versions. + +## Validation and limits + +Observed with Snapmaker Orca 2.4.1 (`c0e6987`), Arch Linux, wxGTK3, +WebKitGTK 4.1, NVIDIA RTX 3090 / driver 610.57.04, and Mesa 26.2.2: + +- WebKitWebProcess crashed with SIGSEGV inside `libnvidia-eglcore`. +- Default OpenGL contexts produced a blank model canvas with both Zink and + software llvmpipe, while the native toolbar remained visible. +- OpenGL 2.1 displayed model geometry and all seven plates of the same 3MF with + both llvmpipe and Zink. The Zink configuration was then checked using the normal + user profile. +- Model visibility was checked; sliced toolpaths, printing, other GPUs, and + other distributions were not validated. + +The portable launcher changes accompany GTK popup lifetime fixes, no-result +WebKit script execution, correct validation of flat printer metadata, and +materialized Eigen casts in integer-coordinate line-radius queries. + +## Focused regression tests + +```sh +cmake -S tests/crash_regressions -B regression-build \ + -DCMAKE_PREFIX_PATH=/path/to/dependency/prefix +cmake --build regression-build +ctest --test-dir regression-build --output-on-failure +``` + +This suite contains three metadata-validation cases and two line-radius cases. +The metadata cases are also registered with the main `tests/slic3rutils` suite. +Catch2 is fetched by the repository's existing CMake helper; a local Catch2 +checkout may be passed as `FETCHCONTENT_SOURCE_DIR_CATCH2` for offline builds. + +Two standalone GUI smoke programs in `tests/crash_regressions` exercise popup +destruction and WebKit native-object completion. They require a graphical +session and can be built with the dependency prefix's `wx-config` and system +WebKitGTK: + +```sh +c++ -std=c++17 -Isrc $(wx-config --cxxflags) \ + tests/crash_regressions/PopupLifecycle_smoke.cpp \ + src/slic3r/GUI/Widgets/PopupWindow.cpp $(wx-config --libs) -o popup-smoke +./popup-smoke +c++ tests/crash_regressions/WebKitCompletion_smoke.cpp \ + $(pkg-config --cflags --libs webkit2gtk-4.1) -o webkit-smoke +ORCA_LINUX_RENDER_COMPAT=1 sh -c \ + '. ./resources/linux-launch-env.sh; exec ./webkit-smoke' +``` + +The WebKit smoke compares the unsupported native-object completion with a +discarded completion value. It exercises the API behavior, not the complete +application bridge or the driver's crash path. diff --git a/gitlab_pack_linux.sh b/gitlab_pack_linux.sh index 366c9e5deb3b..64d5df7f18ff 100644 --- a/gitlab_pack_linux.sh +++ b/gitlab_pack_linux.sh @@ -477,9 +477,9 @@ pack_appimage() { #!/bin/bash DIR=\$(readlink -f "\$0" | xargs dirname) export LD_LIBRARY_PATH="\$DIR/bin:\$LD_LIBRARY_PATH" -export LC_ALL=C +. "\$DIR/resources/linux-launch-env.sh" -if [ "\$XDG_SESSION_TYPE" = "wayland" ] && [ "\$ZINK_DISABLE_OVERRIDE" != "1" ]; then +if [ "\$XDG_SESSION_TYPE" = "wayland" ] && [ "\$ZINK_DISABLE_OVERRIDE" != "1" ] && [ "\$ORCA_LINUX_RENDER_COMPAT" != "1" ]; then if command -v glxinfo >/dev/null 2>&1; then RENDERER=\$(glxinfo | grep "OpenGL renderer string:" | sed 's/.*: //') if echo "\$RENDERER" | grep -qi "NVIDIA"; then diff --git a/pack_linux.sh b/pack_linux.sh index 5e6b5294548f..245f59667522 100644 --- a/pack_linux.sh +++ b/pack_linux.sh @@ -458,9 +458,9 @@ pack_appimage() { #!/bin/bash DIR=\$(readlink -f "\$0" | xargs dirname) export LD_LIBRARY_PATH="\$DIR/bin:\$LD_LIBRARY_PATH" -export LC_ALL=C +. "\$DIR/resources/linux-launch-env.sh" -if [ "\$XDG_SESSION_TYPE" = "wayland" ] && [ "\$ZINK_DISABLE_OVERRIDE" != "1" ]; then +if [ "\$XDG_SESSION_TYPE" = "wayland" ] && [ "\$ZINK_DISABLE_OVERRIDE" != "1" ] && [ "\$ORCA_LINUX_RENDER_COMPAT" != "1" ]; then if command -v glxinfo >/dev/null 2>&1; then RENDERER=\$(glxinfo | grep "OpenGL renderer string:" | sed 's/.*: //') if echo "\$RENDERER" | grep -qi "NVIDIA"; then diff --git a/resources/linux-launch-env.sh b/resources/linux-launch-env.sh new file mode 100644 index 000000000000..45c8d8dbac68 --- /dev/null +++ b/resources/linux-launch-env.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# Shared environment for packaged Linux launchers and source builds. +# Keep user-provided certificate settings; source builds may use an OpenSSL +# prefix that does not contain the distribution's CA bundle. +if [ -z "${SSL_CERT_FILE:-}" ] && [ -f /etc/ssl/cert.pem ]; then + export SSL_CERT_FILE=/etc/ssl/cert.pem +fi + +# Flutter expects a browser language tag rather than the POSIX C locale. +case "${LC_ALL:-${LC_MESSAGES:-${LANG:-C}}}" in + C|C.*|POSIX) export LC_ALL=en_US.UTF-8 ;; +esac + +# Explicit fallback for blank model canvases or NVIDIA EGL crashes. This is +# intentionally opt-in: the OpenGL 2.1 path disables newer rendering features. +if [ "${ORCA_LINUX_RENDER_COMPAT:-0}" = 1 ]; then + export __GLX_VENDOR_LIBRARY_NAME=mesa + export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json + export MESA_LOADER_DRIVER_OVERRIDE=zink + export GALLIUM_DRIVER=zink + export MESA_GL_VERSION_OVERRIDE=2.1 + export GDK_BACKEND=x11 + export WEBKIT_DISABLE_DMABUF_RENDERER=1 +fi diff --git a/scripts/run-local-linux.sh b/scripts/run-local-linux.sh new file mode 100755 index 000000000000..bd6a0a00b269 --- /dev/null +++ b/scripts/run-local-linux.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -eu + +orca_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +. "$orca_root/resources/linux-launch-env.sh" +export DESKTOPINTEGRATION=false +exec "$orca_root/build/src/snapmaker-orca" "$@"