From 20006547922629be3950ebe8b5f84f7ead4362d3 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Mon, 7 Sep 2026 04:20:54 +0800 Subject: [PATCH] [fix] prevent Windows startup crashes from OpenGL failures, fixes #97 --- apps/desktop/src/app/main.cpp | 5 + .../src/gui/application/gui_application.cpp | 10 ++ .../platform/windows/gui/slint_backend.cpp | 109 ++++++++++++++++++ .../src/platform/windows/gui/slint_backend.h | 33 ++++++ .../windows/gui/slint_renderer_probe.cpp | 59 ++++++++++ apps/desktop/xmake/targets.lua | 2 + 6 files changed, 218 insertions(+) create mode 100644 apps/desktop/src/platform/windows/gui/slint_backend.cpp create mode 100644 apps/desktop/src/platform/windows/gui/slint_backend.h create mode 100644 apps/desktop/src/platform/windows/gui/slint_renderer_probe.cpp diff --git a/apps/desktop/src/app/main.cpp b/apps/desktop/src/app/main.cpp index f38f92a3..6e2d3e1b 100644 --- a/apps/desktop/src/app/main.cpp +++ b/apps/desktop/src/app/main.cpp @@ -15,6 +15,7 @@ #ifdef _WIN32 #include +#include "platform/windows/gui/slint_backend.h" #include "service_host.h" #endif @@ -175,6 +176,10 @@ int HandleServiceCliCommand(const std::string& command) { int main(int argc, char* argv[]) { #ifdef _WIN32 + if (argc == 2 && + std::strcmp(argv[1], crossdesk::kSlintRendererProbeArgument) == 0) { + return crossdesk::RunSlintRendererProbe(); + } if (argc > 1 && IsServiceCliCommand(argv[1])) { return HandleServiceCliCommand(argv[1]); } diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index d2fcdce9..e47648a4 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -37,6 +37,7 @@ #if _WIN32 #include +#include "platform/windows/gui/slint_backend.h" #include "platform/windows/gui/tray/win_tray.h" #elif defined(__APPLE__) #include "platform/macos/gui/tray/mac_tray.h" @@ -992,6 +993,15 @@ int GuiApplication::Run() { if (!InitializeSDL()) { return -1; } +#if _WIN32 + const auto backend = ConfigureWindowsSlintBackend(); + if (!backend.success) { + LOG_ERROR("{}", backend.diagnostic); + SDL_Quit(); + return -1; + } + LOG_INFO("Slint backend: {}; {}", backend.backend, backend.diagnostic); +#endif InitializeModules(); InitializeUi(); diff --git a/apps/desktop/src/platform/windows/gui/slint_backend.cpp b/apps/desktop/src/platform/windows/gui/slint_backend.cpp new file mode 100644 index 00000000..3e53e514 --- /dev/null +++ b/apps/desktop/src/platform/windows/gui/slint_backend.cpp @@ -0,0 +1,109 @@ +#include "platform/windows/gui/slint_backend.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace crossdesk { +namespace { + +struct HandleCloser { + void operator()(HANDLE handle) const noexcept { + if (handle != INVALID_HANDLE_VALUE) { + CloseHandle(handle); + } + } +}; +using ScopedHandle = std::unique_ptr; + +std::string ErrorCode(DWORD code) { + std::ostringstream text; + text << "0x" << std::hex << std::uppercase << code; + return text.str(); +} + +std::string ProbeSlintRenderer(DWORD timeout_ms) { + std::wstring executable(32768, L'\0'); + const DWORD length = GetModuleFileNameW( + nullptr, executable.data(), static_cast(executable.size())); + if (length == 0 || length >= executable.size()) { + return "Unable to locate renderer probe executable"; + } + executable.resize(length); + + // A job also cleans up the probe if the application is closed while the + // graphics driver is stuck. Create it suspended so assignment cannot race. + ScopedHandle job(CreateJobObjectW(nullptr, nullptr)); + JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = {}; + limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + if (!job || + !SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation, + &limits, sizeof(limits))) { + return "Unable to create renderer probe job: " + ErrorCode(GetLastError()); + } + + constexpr std::string_view argument = kSlintRendererProbeArgument; + std::wstring command = L"\"" + executable + L"\" "; + command.append(argument.begin(), argument.end()); + STARTUPINFOW startup = {sizeof(startup)}; + PROCESS_INFORMATION process = {}; + if (!CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, + FALSE, CREATE_NO_WINDOW | CREATE_SUSPENDED, nullptr, + nullptr, &startup, &process)) { + return "Unable to start renderer probe: " + ErrorCode(GetLastError()); + } + ScopedHandle process_handle(process.hProcess); + ScopedHandle thread_handle(process.hThread); + if (!AssignProcessToJobObject(job.get(), process.hProcess) || + ResumeThread(process.hThread) == static_cast(-1)) { + const DWORD error = GetLastError(); + TerminateProcess(process.hProcess, 1); + return "Unable to run renderer probe: " + ErrorCode(error); + } + + const DWORD wait = WaitForSingleObject(process.hProcess, timeout_ms); + if (wait != WAIT_OBJECT_0) { + // Closing the job kills only our probe, including any stuck driver thread. + return wait == WAIT_TIMEOUT ? "Slint renderer probe timed out" + : "Waiting for renderer probe failed: " + + ErrorCode(GetLastError()); + } + DWORD exit_code = 0; + if (!GetExitCodeProcess(process.hProcess, &exit_code)) { + return "Unable to read renderer probe result: " + ErrorCode(GetLastError()); + } + return exit_code == 0 + ? std::string{} + : "Slint renderer probe exited with " + ErrorCode(exit_code); +} + +} // namespace + +SlintBackendSelection ConfigureWindowsSlintBackend(uint32_t probe_timeout_ms) { + if (const char* requested = std::getenv("SLINT_BACKEND"); + requested && requested[0] != '\0') { + return {true, requested, "Using explicitly requested Slint backend"}; + } + + // Slint creates its own GL context. A successful SDL/WGL capability check + // cannot validate it, and a Rust panic cannot be caught by C++ exceptions. + // Exercise Slint in a separate process without touching GL in this process. + const std::string error = ProbeSlintRenderer(probe_timeout_ms); + if (error.empty()) { + return {true, "default", "Slint renderer probe rendered successfully"}; + } + + // _putenv_s updates the CRT and the OS environment read by slint_cpp.dll. + // This is process-local so a later launch can retry hardware rendering. + if (_putenv_s("SLINT_BACKEND", "winit-software") != 0) { + return {false, {}, "Unable to select Slint software rendering: " + error}; + } + return {true, "winit-software", error}; +} + +} // namespace crossdesk diff --git a/apps/desktop/src/platform/windows/gui/slint_backend.h b/apps/desktop/src/platform/windows/gui/slint_backend.h new file mode 100644 index 00000000..e9540202 --- /dev/null +++ b/apps/desktop/src/platform/windows/gui/slint_backend.h @@ -0,0 +1,33 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-09-07 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _SLINT_BACKEND_H_ +#define _SLINT_BACKEND_H_ + +#include +#include + +namespace crossdesk { + +struct SlintBackendSelection { + bool success = true; + std::string backend; + std::string diagnostic; +}; + +inline constexpr char kSlintRendererProbeArgument[] = "--slint-renderer-probe"; + +// Call before creating any Slint components. The executable's main() must +// dispatch kSlintRendererProbeArgument before services, settings or the daemon. +SlintBackendSelection ConfigureWindowsSlintBackend( + uint32_t probe_timeout_ms = 10000); + +// Internal child-process entry point; never call in the main application. +int RunSlintRendererProbe(); + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/platform/windows/gui/slint_renderer_probe.cpp b/apps/desktop/src/platform/windows/gui/slint_renderer_probe.cpp new file mode 100644 index 00000000..58d8d53c --- /dev/null +++ b/apps/desktop/src/platform/windows/gui/slint_renderer_probe.cpp @@ -0,0 +1,59 @@ +#include +#include + +#include +#include + +#include "crossdesk_ui.h" +#include "platform/windows/gui/slint_backend.h" + +namespace crossdesk { + +int RunSlintRendererProbe() { + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + if (!SDL_Init(SDL_INIT_VIDEO)) { + return 1; + } + + int result = 2; + { + // Reuse the application's single AOT entry point. Independently generated + // headers can contain colliding internal component names across TUs. + auto probe = ui::MainWindow::create(); + probe->set_custom_titlebar(true); + // AfterRendering confirms that the real GL context, shader initialization + // and first draw all succeeded. GPU readback from an off-screen window can + // be black even when rendering works, so do not use its pixels as a test. + const auto notifier_error = probe->window().set_rendering_notifier( + [&](slint::RenderingState state, slint::GraphicsAPI) { + if (state == slint::RenderingState::AfterRendering) { + result = 0; + } + }); + probe->window().set_position( + slint::PhysicalPosition(slint::Point{-32000, -32000})); + probe->show(); + slint::Timer timer; + timer.start(slint::TimerMode::SingleShot, std::chrono::milliseconds(100), + [&] { + // Renderers without a rendering notifier (software) support + // snapshots. + if (notifier_error) { + const auto pixels = probe->window().take_snapshot(); + if (pixels && pixels->width() > 0 && pixels->height() > 0) { + result = 0; + } else { + std::cerr << "Slint probe could not read rendered pixels" + << std::endl; + } + } + slint::quit_event_loop(); + }); + slint::run_event_loop(); + probe->hide(); + } + SDL_Quit(); + return result; +} + +} // namespace crossdesk diff --git a/apps/desktop/xmake/targets.lua b/apps/desktop/xmake/targets.lua index b36ae608..5a241f32 100644 --- a/apps/desktop/xmake/targets.lua +++ b/apps/desktop/xmake/targets.lua @@ -329,6 +329,8 @@ function setup_targets() add_cxxflags("/bigobj") add_links("opengl32") add_files("apps/desktop/src/platform/windows/gui/tray/win_tray.cpp", + "apps/desktop/src/platform/windows/gui/slint_backend.cpp", + "apps/desktop/src/platform/windows/gui/slint_renderer_probe.cpp", "apps/desktop/src/platform/common/gui/opengl_video_renderer.cpp", "apps/desktop/src/platform/common/gui/video_renderer_factory_opengl.cpp", "apps/desktop/src/platform/windows/gui/runtime/windows_service_runtime.cpp",