fix(native): unblock linux startup — provider GL loader + wayland-dlopen

Two Linux startup blockers:

1. Skia's Interface::new_native() dlopens libGL/GLX, which fails on
   the EGL contexts this project always creates on Linux — GL init
   errored and the app exited (the tracked LINUX_GPU_SKIA_LOADER_TBD
   gap). GlContextProvider now exposes gl_proc_address; the glutin
   provider resolves through its display (eglGetProcAddress on EGL)
   and SharedSkiaContext builds the interface via new_load_with,
   falling back to new_native for providers without a loader. The
   Linux GPU smoke tests are un-ignored (they soft-skip without a
   working EGL stack unless STEP1A_REQUIRE_GPU=1).

2. Dropping casement's default features also dropped wayland-dlopen,
   making libwayland-client a DT_NEEDED hard dependency — the binary
   could not even load on X11-only hosts. Restore the feature so
   libwayland loads at runtime when present.
This commit is contained in:
Kayshen-X 2026-07-03 00:08:20 +08:00
parent 061296c779
commit bde00122d5
7 changed files with 69 additions and 11 deletions

View file

@ -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",
] }

View file

@ -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",
] }

View file

@ -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::*;

View file

@ -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> {
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

View file

@ -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.

View file

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

View file

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