diff --git a/.github/workflows/rust-multiplatform.yml b/.github/workflows/rust-multiplatform.yml index 6968a3508..3d281af16 100644 --- a/.github/workflows/rust-multiplatform.yml +++ b/.github/workflows/rust-multiplatform.yml @@ -156,11 +156,12 @@ jobs: - uses: Swatinem/rust-cache@v2 with: key: mobile-${{ matrix.target }} - # Step 1a kill-spike scope: - # - shell-core MUST compile on iOS / Android / WASM (spec §11 mobile invariant 1). - # - shell-native is desktop-only until Step 1f wires real EaglProvider / - # AndroidEglProvider impls + sdk linking. Mobile cargo check on shell-native - # is deferred to Step 1f; the spec §11 contract is verified via shell-core - # wasm/ios/android compile + API surface (GlContextProvider trait public, - # on_pause cfg(android) surface.take(), TouchForce in ShellEvent — Phase B). + # Step 1a spec §11 mobile invariants verify on iOS / Android cargo check: + # - shell-core wasm32/ios/android-clean (no platform deps). + # - shell-native compiles on mobile targets with EaglProvider / + # AndroidEglProvider stubs (`unimplemented!("Step 1f")`); desktop GL + # stack (glutin / winit) is target-gated to desktop in Cargo.toml + + # GlutinProvider source is cfg-gated to desktop OS only. Real SDK + # linking and iOS/Android runtime is Step 1f. - run: cargo check -p openpencil-shell-core --target ${{ matrix.target }} + - run: cargo check -p openpencil-shell-native --target ${{ matrix.target }} diff --git a/crates/openpencil-shell-native/Cargo.toml b/crates/openpencil-shell-native/Cargo.toml index 4239eb439..2d5c8c910 100644 --- a/crates/openpencil-shell-native/Cargo.toml +++ b/crates/openpencil-shell-native/Cargo.toml @@ -28,47 +28,60 @@ openpencil-shell-core = { path = "../openpencil-shell-core", version = "0.1.0" } # OP runs its own GPU event loop and does not call jian_host_desktop::run (softbuffer # raster present is not needed). # -# winit features note: on Linux you MUST explicitly enable `x11` and/or `wayland`, -# otherwise `platform_impl/mod.rs` triggers `compile_error!`. macOS / Windows backends -# are auto-enabled via cfg(target_os) and need no feature flag. Step 1a runs CI on all -# three desktop OSes, so enabling both x11 + wayland Linux backends is sufficient. +# Desktop GL stack — target-gated to macOS / Linux / Windows. iOS / Android +# pull EaglProvider / AndroidEglProvider stubs (Step 1f) which don't need +# glutin / winit / desktop skia-safe gl bindings; spec §11 invariant 1 says +# shell-native must compile on mobile cargo check (verified by CI mobile-check +# job in rust-multiplatform.yml). winit features note: on Linux you MUST +# explicitly enable `x11` and/or `wayland`, otherwise `platform_impl/mod.rs` +# triggers `compile_error!`. macOS / Windows backends are auto-enabled via +# cfg(target_os) and need no feature flag. +# Cross-platform abstraction deps — pulled for ALL non-wasm targets including +# iOS / Android. The `GlContextProvider` trait (spec §3.1) references +# `glow::Context` in its method signatures and Step 1f Eagl / AndroidEgl +# stubs reference `raw_window_handle` for `on_resume`; both must be importable +# on mobile per spec §11 invariant 2. `glow` and `raw-window-handle` are +# pure-Rust thin bindings — no native build steps on iOS / Android. +# `jian-core` is wasm32-clean per P0.5 and platform-neutral on mobile. [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +jian-core = { path = "../../vendor/jian/crates/jian-core", version = "0.0.1" } +glow = "0.17.0" +raw-window-handle = "0.6.2" + +# Desktop GL stack — target-gated to macOS / Linux / Windows. iOS / Android +# pull only the cross-platform `glow` + `raw-window-handle` above for the +# `GlContextProvider` trait surface; the actual `GlutinProvider` desktop +# implementation, `SharedSkiaContext`, `NativeBackend`, and +# `CanvasViewportStub` are cfg-gated out of the mobile build (see +# src/lib.rs module-level `#[cfg(...)]`). spec §11 invariant 1 says +# shell-native must compile on mobile cargo check (verified by CI +# mobile-check job in rust-multiplatform.yml). winit features note: on +# Linux you MUST explicitly enable `x11` and/or `wayland`, otherwise +# `platform_impl/mod.rs` triggers `compile_error!`. macOS / Windows +# backends are auto-enabled via cfg(target_os) and need no feature flag. +[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies] skia-safe = { version = "0.97.0", features = ["gl"] } glutin = "0.32.3" glutin-winit = "0.5.0" -glow = "0.17.0" winit = { version = "0.30.13", default-features = false, features = [ "x11", "wayland", "wayland-csd-adwaita", "rwh_06", ] } -raw-window-handle = "0.6.2" scopeguard = "1.2" - -# Jian path deps — both path + version per spec §12.2. -# - jian-core: exposes DrawOp / Paint / TextRun / geometry / scene::Color. `shell-core` -# also pulls jian-core; shell-native uses it directly so the NativeBackend translation -# path can construct `jian_core::render::DrawOp::*` without bouncing through the -# shell-core re-export each frame. -# - jian-skia: provides SkiaBackend (RenderBackend impl) + skia textlayout (the textlayout -# feature pulls ICU + harfbuzz, ~15MB; P0.5 already bumped skia-safe 0.78→0.97 and -# added a public draw_on_canvas). -jian-core = { path = "../../vendor/jian/crates/jian-core", version = "0.0.1" } jian-skia = { path = "../../vendor/jian/crates/jian-skia", version = "0.0.1", features = [ "textlayout", ] } - -# jian-host-desktop: target-gated desktop only (Linux/macOS/Windows); not pulled into -# android/ios metadata (verified by Task 1 Step 26 boundary check). -# - default-features = false: Jian's default features include `run = ["dep:softbuffer"]` -# for raster present; OP runs its own GPU event loop and doesn't need softbuffer. -# - features = ["textlayout"]: aligns the text path with jian-skia. -[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies] jian-host-desktop = { path = "../../vendor/jian/crates/jian-host-desktop", version = "0.0.1", default-features = false, features = [ "textlayout", ] } +# jian-skia + jian-host-desktop are now in the desktop-only `[target...]` +# block above (merged to avoid duplicate table headers). Per spec §11 + +# §12.3 boundary invariants 2 & 3 they're not pulled into iOS / Android +# cargo check (verified by check-jian-boundaries.sh). + # tracing for span instrumentation across SharedSkiaContext / NativeBackend # (spec §3.3 / §5.2.1; Task 2 Step 15 dictates per-method spans). [dependencies.tracing] diff --git a/crates/openpencil-shell-native/src/context/mod.rs b/crates/openpencil-shell-native/src/context/mod.rs index 9ae62049c..a0657038a 100644 --- a/crates/openpencil-shell-native/src/context/mod.rs +++ b/crates/openpencil-shell-native/src/context/mod.rs @@ -1,13 +1,32 @@ //! Shared GL + Skia context module (spec v19 §3). //! -//! - [`provider`] — `GlContextProvider` trait + `GlutinProvider` (desktop) -//! + iOS / Android stubs (Step 1f). +//! - [`provider`] — `GlContextProvider` trait (cross-platform) + +//! `GlutinProvider` (desktop) + iOS / Android stubs (Step 1f). The +//! trait is exposed on every non-wasm target per spec §11 invariant 2. //! - [`shared`] — `SharedSkiaContext` owning the GL stack + //! `skia_safe::DirectContext` + `skia_safe::Surface`. Frame-scoped -//! `with_frame` callback + idempotent teardown. +//! `with_frame` callback + idempotent teardown. Desktop-only — the +//! skia-safe / jian-skia deps that back it aren't fetched on +//! iOS / Android (Cargo.toml target-gates them). pub mod provider; -pub mod shared; -pub use provider::{GlContextProvider, GlutinProvider, ProviderError, ProviderResult}; +// Cross-platform: trait + error types, importable on every non-wasm target. +pub use provider::{GlContextProvider, ProviderError, ProviderResult}; + +// Per-platform provider implementations. +#[cfg(target_os = "android")] +pub use provider::AndroidEglProvider; +#[cfg(target_os = "ios")] +pub use provider::EaglProvider; +#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] +pub use provider::GlutinProvider; + +// `SharedSkiaContext` is desktop-only — depends on `skia_safe` + `winit` +// (cfg-gated dependencies). Step 1f mobile wiring will introduce a mobile +// twin (or generalize this one) once iOS / Android providers go from +// `unimplemented!()` placeholders to real GL contexts. +#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] +mod shared; +#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] pub use shared::{SharedSkiaContext, SharedSkiaError, SharedSkiaResult, SurfaceConfig}; diff --git a/crates/openpencil-shell-native/src/context/provider.rs b/crates/openpencil-shell-native/src/context/provider.rs index 0a9f2a3da..13183dee9 100644 --- a/crates/openpencil-shell-native/src/context/provider.rs +++ b/crates/openpencil-shell-native/src/context/provider.rs @@ -10,9 +10,8 @@ //! exist as compile-time placeholders so the public API surface is frozen //! before Step 1f real implementations land. +#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] use std::error::Error; -use std::ffi::CString; -use std::num::NonZeroU32; use std::sync::Arc; /// Errors raised by GL context providers. @@ -25,6 +24,11 @@ pub enum ProviderError { } impl ProviderError { + /// Wrap any `Error` impl into a `ProviderError::Failure`. Used by the + /// desktop `GlutinProvider` to convert glutin / glutin-winit errors; + /// `cfg(any(...))`-gated to silence `dead_code` on iOS / Android where + /// no in-tree caller exists yet (Step 1f mobile providers will use it). + #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] pub(crate) fn from_error(err: E) -> Self { Self::Failure(err.to_string()) } @@ -98,7 +102,9 @@ pub trait GlContextProvider { } // ──────────────────────────────────────────────────────────────────────────── -// Desktop: GlutinProvider +// Desktop: GlutinProvider (cfg-gated to macOS / Linux / Windows; the +// glutin / winit / skia-safe dep stack is desktop-only per spec §11 +// invariant 1). // ──────────────────────────────────────────────────────────────────────────── /// Desktop GL provider built on top of `glutin 0.32` + `glutin-winit 0.5`. @@ -106,6 +112,7 @@ pub trait GlContextProvider { /// All non-`Send` glutin handles live in [`Option`]s so `release` can /// drop them in a defined order (surface → context → display) without /// requiring `&mut self` to consume `self`. +#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] pub struct GlutinProvider { /// Currently-current context. `None` after `release`. context: Option, @@ -117,6 +124,7 @@ pub struct GlutinProvider { glow: Arc, } +#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] impl GlutinProvider { /// Construct from an existing winit window. Builds a glutin display /// directly from the window's display handle (bypassing the sealed @@ -134,6 +142,7 @@ impl GlutinProvider { use glutin::prelude::*; use glutin_winit::GlWindow; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; + use std::ffi::CString; let raw_window_handle = window .window_handle() @@ -235,13 +244,7 @@ fn pick_display_api( glutin::display::DisplayApiPreference::Egl } -#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] -fn pick_display_api( - _raw: raw_window_handle::RawWindowHandle, -) -> glutin::display::DisplayApiPreference { - glutin::display::DisplayApiPreference::Egl -} - +#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] impl GlContextProvider for GlutinProvider { #[tracing::instrument(skip(self))] fn make_current(&mut self) -> ProviderResult<()> { @@ -285,6 +288,7 @@ impl GlContextProvider for GlutinProvider { #[tracing::instrument(skip(self))] fn resize(&mut self, width: u32, height: u32) -> ProviderResult<()> { use glutin::prelude::*; + use std::num::NonZeroU32; let ctx = self .context .as_ref() diff --git a/crates/openpencil-shell-native/src/lib.rs b/crates/openpencil-shell-native/src/lib.rs index 38673d31f..29c6f4e9c 100644 --- a/crates/openpencil-shell-native/src/lib.rs +++ b/crates/openpencil-shell-native/src/lib.rs @@ -26,17 +26,44 @@ compile_error!( Use openpencil-shell-web for browser builds (spec v19 §1.2)." ); -pub mod backend; -pub mod canvas_view_stub; +// Cross-platform context module: re-exports the `GlContextProvider` trait +// + `ProviderError` / `ProviderResult` on every (non-wasm) target so spec +// §11 invariant 2 holds — mobile callers can name the trait. Internal +// cfg-gates select between `GlutinProvider` (desktop), `EaglProvider` (iOS) +// and `AndroidEglProvider` (Android), and `SharedSkiaContext` is only +// compiled in on desktop where the GL + Skia stack is available. pub mod context; +// Desktop-only modules — pull `skia_safe` / `jian_skia` / `glutin` types +// that aren't fetched on iOS / Android (see Cargo.toml target-gated deps). +// Spec §11 invariants 1 & 3: mobile builds compile shell-native without +// these modules at all; mobile widget rendering lands in Step 1f. +#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] +pub mod backend; +#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] +pub mod canvas_view_stub; + +#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] pub use backend::{to_jian_color, to_jian_rect, NativeBackend}; +#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] pub use canvas_view_stub::CanvasViewportStub; + +// Cross-platform re-exports — visible on every (non-wasm) target. +pub use context::{GlContextProvider, ProviderError, ProviderResult}; + +// Desktop-only re-exports. +#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] pub use context::{ - GlContextProvider, GlutinProvider, ProviderError, ProviderResult, SharedSkiaContext, - SharedSkiaError, SharedSkiaResult, SurfaceConfig, + GlutinProvider, SharedSkiaContext, SharedSkiaError, SharedSkiaResult, SurfaceConfig, }; +// Mobile stub re-exports — Step 1f real impls; today they're zero-sized +// placeholder structs whose `GlContextProvider` impls `unimplemented!()`. +#[cfg(target_os = "android")] +pub use context::AndroidEglProvider; +#[cfg(target_os = "ios")] +pub use context::EaglProvider; + // `placeholder()` from Task 1 was removed by Codex Phase A Gate round 1 // NIT 1 — Task 2's full re-export chain (`SharedSkiaContext`, // `NativeBackend`, etc.) already proves the shell-core ↔ shell-native