From 6c3fe5e2a5037e1575688881ad3379d09b923d04 Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Mon, 14 Sep 2026 15:13:47 +0200 Subject: [PATCH] winit: Probe for OpenGL 2.0 before selecting the FemtoVG renderer Windows always has an opengl32.dll, but in virtual machines it often provides only OpenGL 1.1. Creating a context through glutin succeeds there and only the missing glCreateShader gives it away. That check ran in resume(), after the window adapter was created with the FemtoVG renderer, so the backend could no longer fall back. Probe for glCreateShader with a throwaway window in new_suspended() instead. The renderer creation then fails early enough for the fallback chain to reach the software renderer. Only a WGL context that lacks glCreateShader proves that the driver is a GL 1.1 stub. If the probe itself can't run, glutin may still reach a working driver through EGL, so leave the choice to it. Closes #12706 ChangeLog: Windows: fall back to another renderer when the OpenGL driver doesn't support OpenGL 2.0 --- internal/backends/winit/Cargo.toml | 7 +- internal/backends/winit/renderer/femtovg.rs | 11 +- .../winit/renderer/femtovg/glcontext.rs | 13 -- .../winit/renderer/femtovg/glprobe.rs | 142 ++++++++++++++++++ 4 files changed, 158 insertions(+), 15 deletions(-) create mode 100644 internal/backends/winit/renderer/femtovg/glprobe.rs diff --git a/internal/backends/winit/Cargo.toml b/internal/backends/winit/Cargo.toml index b45985ba340..fef473a168c 100644 --- a/internal/backends/winit/Cargo.toml +++ b/internal/backends/winit/Cargo.toml @@ -42,7 +42,12 @@ x11 = [ "softbuffer?/x11", "softbuffer?/x11-dlopen", ] -renderer-femtovg = ["i-slint-renderer-femtovg/opengl", "dep:glutin", "dep:glutin-winit"] +renderer-femtovg = [ + "i-slint-renderer-femtovg/opengl", + "dep:glutin", + "dep:glutin-winit", + "windows/Win32_Graphics_OpenGL", +] renderer-femtovg-wgpu = ["i-slint-renderer-femtovg/wgpu", "dep:i-slint-renderer-femtovg", "unstable-wgpu-30"] renderer-skia = ["i-slint-renderer-skia"] renderer-vello = ["dep:i-slint-renderer-anyrender", "i-slint-renderer-anyrender/vello", "unstable-wgpu-29"] diff --git a/internal/backends/winit/renderer/femtovg.rs b/internal/backends/winit/renderer/femtovg.rs index b15596df278..609616a0b17 100644 --- a/internal/backends/winit/renderer/femtovg.rs +++ b/internal/backends/winit/renderer/femtovg.rs @@ -1,7 +1,7 @@ // Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -// cSpell: ignore glcontext webglcontextlost webglcontextrestored +// cSpell: ignore glcontext glprobe webglcontextlost webglcontextrestored use std::rc::Rc; #[cfg(supports_opengl)] use std::rc::Weak; @@ -21,6 +21,8 @@ use super::WinitCompatibleRenderer; #[cfg(all(supports_opengl, not(target_arch = "wasm32")))] mod glcontext; +#[cfg(all(supports_opengl, target_os = "windows"))] +mod glprobe; #[cfg(supports_opengl)] pub struct GlutinFemtoVGRenderer { @@ -34,6 +36,13 @@ impl GlutinFemtoVGRenderer { pub fn new_suspended( shared_backend_data: &Rc, ) -> Result, PlatformError> { + // Bail out before a window is created, so that the backend can still fall back to + // another renderer. + #[cfg(target_os = "windows")] + if !glprobe::opengl_2_available() { + return Err("The FemtoVG renderer requires an OpenGL 2.0 driver".into()); + } + Ok(Box::new(Self { renderer: FemtoVGRenderer::new_suspended(), _requested_graphics_api: shared_backend_data.requested_graphics_api.clone(), diff --git a/internal/backends/winit/renderer/femtovg/glcontext.rs b/internal/backends/winit/renderer/femtovg/glcontext.rs index bfb91bb1647..7252283d53a 100644 --- a/internal/backends/winit/renderer/femtovg/glcontext.rs +++ b/internal/backends/winit/renderer/femtovg/glcontext.rs @@ -205,19 +205,6 @@ impl OpenGLContext { ns_view.setLayerContentsPlacement(objc2_app_kit::NSViewLayerContentsPlacement::TopLeft); } - // Sanity check, as all this might succeed on Windows without working GL drivers, but this will fail: - if context - .display() - .get_proc_address(&std::ffi::CString::new("glCreateShader").unwrap()) - .is_null() - { - return Err( - "Failed to initialize OpenGL driver: Could not locate glCreateShader symbol" - .to_string() - .into(), - ); - } - // Try to default to vsync and ignore if the driver doesn't support it. surface .set_swap_interval( diff --git a/internal/backends/winit/renderer/femtovg/glprobe.rs b/internal/backends/winit/renderer/femtovg/glprobe.rs new file mode 100644 index 00000000000..3063dbe6d5b --- /dev/null +++ b/internal/backends/winit/renderer/femtovg/glprobe.rs @@ -0,0 +1,142 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +// cSpell: ignore clipchildren clipsiblings doublebuffer owndc +// cSpell: ignore pixelformatdescriptor wndclassw + +use std::sync::OnceLock; + +use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM}; +use windows::Win32::Graphics::Gdi::{GetDC, HDC, ReleaseDC}; +use windows::Win32::Graphics::OpenGL::{ + ChoosePixelFormat, PFD_DOUBLEBUFFER, PFD_DRAW_TO_WINDOW, PFD_MAIN_PLANE, PFD_SUPPORT_OPENGL, + PFD_TYPE_RGBA, PIXELFORMATDESCRIPTOR, SetPixelFormat, wglCreateContext, wglDeleteContext, + wglGetCurrentContext, wglGetCurrentDC, wglGetProcAddress, wglMakeCurrent, +}; +use windows::Win32::UI::WindowsAndMessaging::{ + CS_OWNDC, CreateWindowExW, DefWindowProcW, DestroyWindow, RegisterClassW, UnregisterClassW, + WINDOW_EX_STYLE, WNDCLASSW, WS_CLIPCHILDREN, WS_CLIPSIBLINGS, WS_OVERLAPPED, +}; +use windows::core::{s, w}; + +pub fn opengl_2_available() -> bool { + static AVAILABLE: OnceLock = OnceLock::new(); + // A probe that couldn't run says nothing about the driver, and glutin may still + // reach a working one through EGL. + *AVAILABLE.get_or_init(|| unsafe { probe() }.unwrap_or(true)) +} + +/// Every Windows installation has an opengl32.dll, but in virtual machines it often provides +/// only OpenGL 1.1, without the shader entry points. Creating a context still succeeds there, +/// so probe with a throwaway window and context up-front. +/// +/// Returns `None` if the probe couldn't run, which says nothing about the driver. +unsafe fn probe() -> Option { + unsafe { + let class_name = w!("SlintOpenGLProbe"); + let window_class = WNDCLASSW { + style: CS_OWNDC, + lpfnWndProc: Some(window_proc), + lpszClassName: class_name, + ..Default::default() + }; + if RegisterClassW(&window_class) == 0 { + return None; + } + + // WGL requires a window that clips its children and siblings. + let available = CreateWindowExW( + WINDOW_EX_STYLE::default(), + class_name, + w!(""), + WS_OVERLAPPED | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, + 0, + 0, + 1, + 1, + None, + None, + None, + None, + ) + .ok() + .and_then(|window| { + let available = probe_window(window); + let _ = DestroyWindow(window); + available + }); + + let _ = UnregisterClassW(class_name, None); + + available + } +} + +unsafe extern "system" fn window_proc( + window: HWND, + message: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + unsafe { DefWindowProcW(window, message, wparam, lparam) } +} + +unsafe fn probe_window(window: HWND) -> Option { + unsafe { + let hdc = GetDC(Some(window)); + if hdc.is_invalid() { + return None; + } + + let available = probe_device_context(hdc); + + ReleaseDC(Some(window), hdc); + + available + } +} + +unsafe fn probe_device_context(hdc: HDC) -> Option { + unsafe { + let pixel_format_descriptor = PIXELFORMATDESCRIPTOR { + nSize: std::mem::size_of::() as u16, + nVersion: 1, + dwFlags: PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, + iPixelType: PFD_TYPE_RGBA, + cColorBits: 32, + cDepthBits: 24, + iLayerType: PFD_MAIN_PLANE.0 as u8, + ..Default::default() + }; + + let pixel_format = ChoosePixelFormat(hdc, &pixel_format_descriptor); + if pixel_format == 0 { + return None; + } + SetPixelFormat(hdc, pixel_format, &pixel_format_descriptor).ok()?; + + let context = wglCreateContext(hdc).ok()?; + + // Restore whatever was current before, so that probing from a thread that already + // renders through WGL is harmless. + let previous_context = wglGetCurrentContext(); + let previous_hdc = wglGetCurrentDC(); + + let available = wglMakeCurrent(hdc, context).is_ok().then(|| { + let address = wglGetProcAddress(s!("glCreateShader")).map_or(0, |entry| entry as usize); + let _ = wglMakeCurrent(previous_hdc, previous_context); + // Besides null, some drivers report a missing entry point as 1, 2, 3 or -1. + !matches!(address, 0 | 1 | 2 | 3 | usize::MAX) + }); + + let _ = wglDeleteContext(context); + + available + } +} + +#[test] +fn probe_runs() { + // The answer depends on the machine, so this only covers the probe running to completion. + opengl_2_available(); +}