diff --git a/crates/op-host-desktop/Cargo.toml b/crates/op-host-desktop/Cargo.toml index 475d4043f..8c58dcaf4 100644 --- a/crates/op-host-desktop/Cargo.toml +++ b/crates/op-host-desktop/Cargo.toml @@ -164,6 +164,10 @@ skia-safe = { version = "0.97.0", default-features = false, features = [ winit = { package = "casement", path = "../../vendor/casement", default-features = false, features = [ "x11", "wayland", + # dlopen libwayland at runtime so X11-only hosts can still start the + # binary (without this, libwayland-client becomes a DT_NEEDED hard + # dependency and the dynamic linker refuses to load on X11-only boxes). + "wayland-dlopen", "wayland-csd-adwaita", "rwh_06", ] } diff --git a/crates/op-host-native/Cargo.toml b/crates/op-host-native/Cargo.toml index 39364f61b..fffb9cf70 100644 --- a/crates/op-host-native/Cargo.toml +++ b/crates/op-host-native/Cargo.toml @@ -165,6 +165,10 @@ glutin = { version = "0.32.3", optional = true } winit = { package = "casement", path = "../../vendor/casement", default-features = false, optional = true, features = [ "x11", "wayland", + # dlopen libwayland at runtime so X11-only hosts can still start the + # binary (without this, libwayland-client becomes a DT_NEEDED hard + # dependency and the dynamic linker refuses to load on X11-only boxes). + "wayland-dlopen", "wayland-csd-adwaita", "rwh_06", ] } diff --git a/crates/op-host-native/src/context/provider.rs b/crates/op-host-native/src/context/provider.rs index dcbf85e3b..85cd160c9 100644 --- a/crates/op-host-native/src/context/provider.rs +++ b/crates/op-host-native/src/context/provider.rs @@ -107,6 +107,18 @@ pub trait GlContextProvider { /// asynchronously, so an explicit release path makes the /// 100-iteration RSS sanity test deterministic. fn release(&mut self) -> ProviderResult<()>; + + /// Resolve a GL symbol through the API that created this context + /// (EGL / GLX / WGL / CGL). Skia consumes this via + /// `Interface::new_load_with` so symbol resolution matches the + /// context's API — `Interface::new_native()` dlopens libGL/GLX on + /// Linux, which fails when the current context is EGL (the + /// LINUX_GPU_SKIA_LOADER_TBD gap). `None` = this provider has no + /// loader; `SharedSkiaContext::new` falls back to `new_native()`. + fn gl_proc_address(&self, symbol: &std::ffi::CStr) -> Option<*const std::ffi::c_void> { + let _ = symbol; + None + } } // ──────────────────────────────────────────────────────────────────────────── @@ -324,6 +336,17 @@ impl GlContextProvider for GlutinProvider { 0 } + /// Route Skia's symbol lookups through the same glutin display the + /// glow function table was loaded from, so EGL contexts resolve via + /// eglGetProcAddress instead of `new_native()`'s GLX path. + fn gl_proc_address(&self, symbol: &std::ffi::CStr) -> Option<*const std::ffi::c_void> { + use glutin::display::GetGlDisplay; + use glutin::prelude::*; + self.context + .as_ref() + .map(|ctx| ctx.display().get_proc_address(symbol)) + } + #[tracing::instrument(skip(self))] fn resize(&mut self, width: u32, height: u32) -> ProviderResult<()> { use glutin::prelude::*; diff --git a/crates/op-host-native/src/context/shared.rs b/crates/op-host-native/src/context/shared.rs index bf6e34053..8cceee948 100644 --- a/crates/op-host-native/src/context/shared.rs +++ b/crates/op-host-native/src/context/shared.rs @@ -97,8 +97,7 @@ impl SharedSkiaContext { let glow_handle = provider.glow(); - let interface = - skia_safe::gpu::gl::Interface::new_native().ok_or(SharedSkiaError::GlInterface)?; + let interface = provider_gl_interface(&provider).ok_or(SharedSkiaError::GlInterface)?; let mut direct_context = skia_safe::gpu::direct_contexts::make_gl(interface, None) .ok_or(SharedSkiaError::DirectContext)?; @@ -325,6 +324,25 @@ impl Drop for SharedSkiaContext { } } +/// Build the Skia GL interface for a provider. Prefer the provider's +/// own symbol loader so resolution matches the API that created the +/// context (EGL vs GLX vs WGL vs CGL) — `new_native()` dlopens +/// libGL/GLX on Linux and fails on EGL-current contexts (the +/// LINUX_GPU_SKIA_LOADER_TBD gap). Providers without a loader return +/// `None` per symbol, the load comes back empty, and the chain falls +/// through to `new_native()`. +fn provider_gl_interface( + provider: &dyn GlContextProvider, +) -> Option { + skia_safe::gpu::gl::Interface::new_load_with(|name| { + std::ffi::CString::new(name) + .ok() + .and_then(|sym| provider.gl_proc_address(&sym)) + .unwrap_or(std::ptr::null()) + }) + .or_else(skia_safe::gpu::gl::Interface::new_native) +} + /// Query GL state for the current viewport + sample/stencil bits. /// /// Phase A Gate round 2 BLOCK 1 fix: `SharedSkiaContext::new` is now diff --git a/crates/op-host-native/tests/common/mod.rs b/crates/op-host-native/tests/common/mod.rs index 7b154b7a0..ccb1e734e 100644 --- a/crates/op-host-native/tests/common/mod.rs +++ b/crates/op-host-native/tests/common/mod.rs @@ -213,6 +213,15 @@ pub mod egl_pbuffer { 0 } + /// Resolve symbols via eglGetProcAddress — the loader path that + /// unblocks Skia on EGL pbuffer + llvmpipe (previously the + /// LINUX_GPU_SKIA_LOADER_TBD gap: `new_native()` went through + /// GLX and found nothing). + fn gl_proc_address(&self, symbol: &std::ffi::CStr) -> Option<*const c_void> { + let name = symbol.to_str().ok()?; + self.egl.get_proc_address(name).map(|p| p as *const c_void) + } + fn resize(&mut self, _w: u32, _h: u32) -> ProviderResult<()> { // Pbuffer can't be resized once created — tests build the // surface at the size they need. diff --git a/crates/op-host-native/tests/gpu_chrome_stub_composition.rs b/crates/op-host-native/tests/gpu_chrome_stub_composition.rs index fa6bc7a65..e032c48f5 100644 --- a/crates/op-host-native/tests/gpu_chrome_stub_composition.rs +++ b/crates/op-host-native/tests/gpu_chrome_stub_composition.rs @@ -288,11 +288,11 @@ fn gpu_chrome_stub_composition() { platform::run(); } -// Linux GPU chrome+stub composition deferred: same root cause as -// LINUX_GPU_SKIA_LOADER_TBD in gpu_smoke.rs. +// Linux GPU chrome+stub composition: unblocked by the +// `GlContextProvider::gl_proc_address` loader path (see gpu_smoke.rs); +// soft-skips without a working EGL stack unless STEP1A_REQUIRE_GPU=1. #[cfg(target_os = "linux")] #[test] -#[ignore = "LINUX_GPU_SKIA_LOADER_TBD: skia-safe Interface::new_native cannot resolve GL syms from EGL pbuffer + llvmpipe (see gpu_smoke.rs)"] fn gpu_chrome_stub_composition() { platform::run(); } diff --git a/crates/op-host-native/tests/gpu_smoke.rs b/crates/op-host-native/tests/gpu_smoke.rs index 7c7bf1b77..cfe670f9b 100644 --- a/crates/op-host-native/tests/gpu_smoke.rs +++ b/crates/op-host-native/tests/gpu_smoke.rs @@ -290,14 +290,14 @@ fn gpu_smoke() { platform::run(); } -// Linux GPU smoke deferred: skia-safe `Interface::new_native()` dlopens -// libGL.so + glXGetProcAddress, which fails on EGL pbuffer + llvmpipe. -// Wiring `Interface::new_load_with(eglGetProcAddress)` requires a new -// `GlContextProvider::get_proc_address` method (spec §3.1 mini-patch -// follow-up). Tracked LINUX_GPU_SKIA_LOADER_TBD. +// Linux GPU smoke: Skia now loads its GL interface through +// `GlContextProvider::gl_proc_address` (eglGetProcAddress for the EGL +// pbuffer provider), closing the old LINUX_GPU_SKIA_LOADER_TBD gap +// where `Interface::new_native()` went through GLX and found nothing. +// On hosts without a working EGL/Mesa stack the test soft-skips +// (INCONCLUSIVE eprintln) unless `STEP1A_REQUIRE_GPU=1`. #[cfg(target_os = "linux")] #[test] -#[ignore = "LINUX_GPU_SKIA_LOADER_TBD: skia-safe Interface::new_native cannot resolve GL syms from EGL pbuffer + llvmpipe; needs new_load_with(eglGetProcAddress) loader path (spec §3.1 follow-up)"] fn gpu_smoke() { platform::run(); }