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(); +}