From 4249721d98d0a2045beb1f87fdd5c09c5d2875b4 Mon Sep 17 00:00:00 2001 From: Fini Date: Sat, 30 May 2026 03:08:58 +0800 Subject: [PATCH] fix(renderer): defer desktop gl startup --- crates/op-host-desktop/src/app_handler.rs | 121 ++++++++++++++---- crates/op-host-desktop/src/main.rs | 8 +- crates/op-host-native/src/context/provider.rs | 58 ++++++++- 3 files changed, 159 insertions(+), 28 deletions(-) diff --git a/crates/op-host-desktop/src/app_handler.rs b/crates/op-host-desktop/src/app_handler.rs index 0d029e293..102e0b4e6 100644 --- a/crates/op-host-desktop/src/app_handler.rs +++ b/crates/op-host-desktop/src/app_handler.rs @@ -6,7 +6,7 @@ use crate::{ git_jobs, menu, persistence, settings_io, window_state, DesktopApp, INITIAL_VIEWPORT_H, INITIAL_VIEWPORT_W, }; -use op_host_native::{NativeBackend, SharedSkiaContext}; +use op_host_native::{NativeBackend, ProviderError, SharedSkiaContext, SharedSkiaError}; use std::time::{Duration, Instant}; use winit::application::ApplicationHandler; use winit::event::{ @@ -46,7 +46,8 @@ impl ApplicationHandler for DesktopApp { .with_inner_size(winit::dpi::LogicalSize::new( INITIAL_VIEWPORT_W as u32, INITIAL_VIEWPORT_H as u32, - )); + )) + .with_min_inner_size(winit::dpi::LogicalSize::new(640.0f64, 400.0f64)); // Hide the title bar but keep the platform's own window // controls — the Electron `titleBarStyle: 'hidden'` recipe. // @@ -95,18 +96,6 @@ impl ApplicationHandler for DesktopApp { let dpi = window.scale_factor() as f32; self.dpi = dpi; - match SharedSkiaContext::new_desktop(&window) { - Ok(ctx) => { - self.ctx = Some(ctx); - self.backend = Some(NativeBackend::with_dpi(dpi)); - } - Err(err) => { - eprintln!("openpencil-desktop: SharedSkiaContext::new_desktop failed: {err}"); - self.error = Some(err); - event_loop.exit(); - return; - } - } // Build the curved-arrow rotate cursor once and cache it. let (rgba, w, h, hx, hy) = cursor_icon::make_rotate_cursor_rgba(); match winit::window::CustomCursor::from_rgba(rgba, w, h, hx, hy) { @@ -223,15 +212,17 @@ impl ApplicationHandler for DesktopApp { // first paint so the launch document shows immediately. self.drain_opened_files(); - if let (Some(ctx), Some(backend)) = (self.ctx.as_mut(), self.backend.as_mut()) { - frame::paint( - ctx, - backend, - &mut self.host, - self.viewport_width, - self.viewport_height, - self.dpi, - ); + if self.try_init_render_context(event_loop) { + if let (Some(ctx), Some(backend)) = (self.ctx.as_mut(), self.backend.as_mut()) { + frame::paint( + ctx, + backend, + &mut self.host, + self.viewport_width, + self.viewport_height, + self.dpi, + ); + } } // The auto-update probe spawned in `new()` is likely still @@ -279,6 +270,8 @@ impl ApplicationHandler for DesktopApp { self.error = Some(err); event_loop.exit(); } + } else { + self.try_init_render_context(event_loop); } self.viewport_width = size.width as f32 / self.dpi; self.viewport_height = size.height as f32 / self.dpi; @@ -355,6 +348,13 @@ impl ApplicationHandler for DesktopApp { self.request_redraw(true); } WindowEvent::RedrawRequested => { + if (self.ctx.is_none() || self.backend.is_none()) + && !self.try_init_render_context(event_loop) + { + self.redraw_pending = false; + self.redraw_dirty = true; + return; + } if chat_session::drain_new_chat_request( &mut self.host, &mut self.current_chat, @@ -874,3 +874,78 @@ impl ApplicationHandler for DesktopApp { self.window.take(); } } + +fn render_surface_not_ready(err: &SharedSkiaError) -> bool { + matches!( + err, + SharedSkiaError::Provider(ProviderError::SurfaceNotReady { .. }) + ) +} + +impl DesktopApp { + fn try_init_render_context(&mut self, event_loop: &ActiveEventLoop) -> bool { + if self.ctx.is_some() && self.backend.is_some() { + return true; + } + let Some(window) = self.window.as_ref() else { + return false; + }; + let size = window.inner_size(); + if size.width <= 1 || size.height <= 1 { + self.defer_render_context_init(event_loop); + return false; + } + + let dpi = window.scale_factor() as f32; + match SharedSkiaContext::new_desktop(window) { + Ok(ctx) => { + self.dpi = dpi; + self.ctx = Some(ctx); + self.backend = Some(NativeBackend::with_dpi(dpi)); + true + } + Err(err) if render_surface_not_ready(&err) => { + self.defer_render_context_init(event_loop); + false + } + Err(err) => { + eprintln!("openpencil-desktop: SharedSkiaContext::new_desktop failed: {err}"); + self.error = Some(err); + event_loop.exit(); + false + } + } + } + + fn defer_render_context_init(&self, event_loop: &ActiveEventLoop) { + if let Some(window) = self.window.as_ref() { + let _ = window.request_inner_size(winit::dpi::LogicalSize::new( + INITIAL_VIEWPORT_W as f64, + INITIAL_VIEWPORT_H as f64, + )); + } + event_loop.set_control_flow(ControlFlow::WaitUntil( + Instant::now() + Duration::from_millis(50), + )); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_init_retries_only_surface_not_ready() { + let not_ready = op_host_native::SharedSkiaError::Provider( + op_host_native::ProviderError::SurfaceNotReady { + width: 1, + height: 1, + }, + ); + assert!(render_surface_not_ready(¬_ready)); + + assert!(!render_surface_not_ready( + &op_host_native::SharedSkiaError::GlInterface + )); + } +} diff --git a/crates/op-host-desktop/src/main.rs b/crates/op-host-desktop/src/main.rs index 07a6fed37..3a7015804 100644 --- a/crates/op-host-desktop/src/main.rs +++ b/crates/op-host-desktop/src/main.rs @@ -825,7 +825,13 @@ fn main() { return; } let initial_file = initial_file_from_argv(); - let event_loop = match EventLoop::new() { + let mut event_loop_builder = EventLoop::builder(); + #[cfg(target_os = "macos")] + { + use winit::platform::macos::{ActivationPolicy, EventLoopBuilderExtMacOS}; + event_loop_builder.with_activation_policy(ActivationPolicy::Regular); + } + let event_loop = match event_loop_builder.build() { Ok(el) => el, Err(err) => { eprintln!("openpencil-desktop: EventLoop::new failed: {err}"); diff --git a/crates/op-host-native/src/context/provider.rs b/crates/op-host-native/src/context/provider.rs index 91a15f7b6..b41527ec0 100644 --- a/crates/op-host-native/src/context/provider.rs +++ b/crates/op-host-native/src/context/provider.rs @@ -21,6 +21,12 @@ pub enum ProviderError { /// cross-platform uniformity (each backend uses different error types). #[error("GL context provider failed: {0}")] Failure(String), + /// The platform has created a window, but its content surface is + /// still the placeholder startup size. Retrying after the first + /// real resize/redraw avoids entering native GL with an invalid + /// drawable. + #[error("GL window surface is not ready: {width}x{height}")] + SurfaceNotReady { width: u32, height: u32 }, } impl ProviderError { @@ -194,11 +200,11 @@ impl GlutinProvider { // `glutin_winit::GlWindow::build_surface_attributes` does // internally; we inline it so the crate stays off // `glutin-winit` (which is pinned to the upstream `winit` - // package). Sizes are clamped to ≥ 1 — a zero-area surface - // is rejected by every GL backend. + // package). During macOS startup, winit can briefly expose a + // placeholder 1×1 drawable; reject that state before entering + // native GL so the caller can retry after the real resize. let inner = window.inner_size(); - let surface_width = std::num::NonZeroU32::new(inner.width.max(1)).unwrap(); - let surface_height = std::num::NonZeroU32::new(inner.height.max(1)).unwrap(); + let (surface_width, surface_height) = window_surface_size(inner)?; let surface_attrs = glutin::surface::SurfaceAttributesBuilder::::new() .build(raw_window_handle, surface_width, surface_height); @@ -230,6 +236,21 @@ impl GlutinProvider { } } +#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] +fn window_surface_size( + inner: winit::dpi::PhysicalSize, +) -> ProviderResult<(std::num::NonZeroU32, std::num::NonZeroU32)> { + if inner.width <= 1 || inner.height <= 1 { + return Err(ProviderError::SurfaceNotReady { + width: inner.width, + height: inner.height, + }); + } + let width = std::num::NonZeroU32::new(inner.width).expect("surface width is > 1"); + let height = std::num::NonZeroU32::new(inner.height).expect("surface height is > 1"); + Ok((width, height)) +} + #[cfg(target_os = "macos")] fn pick_display_api( _raw: raw_window_handle::RawWindowHandle, @@ -389,3 +410,32 @@ impl GlContextProvider for AndroidEglProvider { unimplemented!("Step 1f") } } + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] + #[test] + fn window_surface_size_rejects_tiny_startup_extents() { + let err = window_surface_size(winit::dpi::PhysicalSize::new(1, 1)).unwrap_err(); + + assert!(matches!( + err, + ProviderError::SurfaceNotReady { + width: 1, + height: 1 + } + )); + } + + #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] + #[test] + fn window_surface_size_keeps_usable_extents() { + let (width, height) = + window_surface_size(winit::dpi::PhysicalSize::new(1440, 900)).unwrap(); + + assert_eq!(width.get(), 1440); + assert_eq!(height.get(), 900); + } +}