fix(renderer): defer desktop gl startup

This commit is contained in:
Fini 2026-05-30 03:08:58 +08:00
parent 1b97bdea45
commit 4249721d98
3 changed files with 159 additions and 28 deletions

View file

@ -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(&not_ready));
assert!(!render_surface_not_ready(
&op_host_native::SharedSkiaError::GlInterface
));
}
}

View file

@ -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}");

View file

@ -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::<glutin::surface::WindowSurface>::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<u32>,
) -> 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);
}
}