openpencil/crates/op-host-native/tests/gpu_smoke.rs

310 lines
12 KiB
Rust
Raw Normal View History

feat(shell-native): SharedSkiaContext + NativeBackend over Jian DrawOp Step 1a Task 2 (spec v19 §3 / §5.2.1, plan v7). - `SharedSkiaContext`: own GL stack + Skia DirectContext + Surface, `Option<>`-field idempotent teardown, `with_frame(|canvas, glow|)` callback, lifecycle hooks (on_pause/on_resume/on_low_memory) with Android surface drop contract; tracing spans + events on every per-frame entry point. - `GlContextProvider` trait + `GlutinProvider` desktop impl + iOS / Android stubs; trait carries no `Send` bound (per spec §3.1). - `CanvasViewportStub::render_into(&Canvas)` deliberately pollutes STENCIL_TEST + blend func to verify chrome-paint isolation. - `NativeBackend`: frame-scoped methods mirroring OP `RenderBackend` trait surface (no direct trait impl in 1a; Step 1c+ wraps via `WithCanvas<'a>` newtype). Translates `fill_rect / stroke_rect / draw_text / clip_rect / save / restore / translate` to `jian_core::render::DrawOp` and submits via `jian_skia::SkiaBackend::draw_on_canvas`. Public `draw_op` helper + `to_jian_color` / `to_jian_rect` converters. - Tests: - `teardown_idempotent.rs` — teardown × 3 + lifecycle hook idempotence. - `memory_loop.rs` — 100 × create/begin_frame/present/teardown × 3 with sysinfo RSS budget < 5 %. - `tracing_spans.rs` — `tracing-test` (no-env-filter) catches begin_frame / with_frame / present / resize / teardown / on_pause / on_resume / on_low_memory events. - `raster_composition.rs` — chrome-only fill_rect on raster surface, pixel-asserts red + black + untouched-bg. - `raster_text_smoke.rs` — "Hello 你好" through textlayout feature, asserts visible glyph rasterisation. - `gpu_smoke.rs` — Linux EGL pbuffer (non-ignored) + macOS invisible winit window (graceful inconclusive when off main thread; full path runs from `cargo run --example basic_window`) + Windows `#[ignore]` per spec §8.1. - `gpu_chrome_stub_composition.rs` — chrome+stub on the same GL surface, asserts chrome pixels survive stub's GL pollution. - Cargo.toml: add `jian-core` direct dep + `tracing` / `thiserror` workspace deps; dev-deps `sysinfo`, `tracing-test` (with `no-env-filter`), Linux-only `khronos-egl` + `libloading`. `cargo build`, `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --all -- --check` all green on macOS.
2026-05-05 13:06:00 +00:00
//! Spec v19 §9.2 / acceptance #3 / plan v7 Task 2 Step 16f.
//!
//! Per-OS dispatch:
//! - Linux : EGL pbuffer + `SharedSkiaContext` GL surface (non-ignored).
//! - macOS : invisible winit window + `SharedSkiaContext::new_desktop`
//! (non-ignored — runs on developer machine + macOS CI).
feat(shell-native): SharedSkiaContext + NativeBackend over Jian DrawOp Step 1a Task 2 (spec v19 §3 / §5.2.1, plan v7). - `SharedSkiaContext`: own GL stack + Skia DirectContext + Surface, `Option<>`-field idempotent teardown, `with_frame(|canvas, glow|)` callback, lifecycle hooks (on_pause/on_resume/on_low_memory) with Android surface drop contract; tracing spans + events on every per-frame entry point. - `GlContextProvider` trait + `GlutinProvider` desktop impl + iOS / Android stubs; trait carries no `Send` bound (per spec §3.1). - `CanvasViewportStub::render_into(&Canvas)` deliberately pollutes STENCIL_TEST + blend func to verify chrome-paint isolation. - `NativeBackend`: frame-scoped methods mirroring OP `RenderBackend` trait surface (no direct trait impl in 1a; Step 1c+ wraps via `WithCanvas<'a>` newtype). Translates `fill_rect / stroke_rect / draw_text / clip_rect / save / restore / translate` to `jian_core::render::DrawOp` and submits via `jian_skia::SkiaBackend::draw_on_canvas`. Public `draw_op` helper + `to_jian_color` / `to_jian_rect` converters. - Tests: - `teardown_idempotent.rs` — teardown × 3 + lifecycle hook idempotence. - `memory_loop.rs` — 100 × create/begin_frame/present/teardown × 3 with sysinfo RSS budget < 5 %. - `tracing_spans.rs` — `tracing-test` (no-env-filter) catches begin_frame / with_frame / present / resize / teardown / on_pause / on_resume / on_low_memory events. - `raster_composition.rs` — chrome-only fill_rect on raster surface, pixel-asserts red + black + untouched-bg. - `raster_text_smoke.rs` — "Hello 你好" through textlayout feature, asserts visible glyph rasterisation. - `gpu_smoke.rs` — Linux EGL pbuffer (non-ignored) + macOS invisible winit window (graceful inconclusive when off main thread; full path runs from `cargo run --example basic_window`) + Windows `#[ignore]` per spec §8.1. - `gpu_chrome_stub_composition.rs` — chrome+stub on the same GL surface, asserts chrome pixels survive stub's GL pollution. - Cargo.toml: add `jian-core` direct dep + `tracing` / `thiserror` workspace deps; dev-deps `sysinfo`, `tracing-test` (with `no-env-filter`), Linux-only `khronos-egl` + `libloading`. `cargo build`, `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --all -- --check` all green on macOS.
2026-05-05 13:06:00 +00:00
//! - Windows: `#[ignore]` per spec §8.1 (no GPU on standard Actions runner;
//! covered by manual smoke notes).
feat(shell-native): SharedSkiaContext + NativeBackend over Jian DrawOp Step 1a Task 2 (spec v19 §3 / §5.2.1, plan v7). - `SharedSkiaContext`: own GL stack + Skia DirectContext + Surface, `Option<>`-field idempotent teardown, `with_frame(|canvas, glow|)` callback, lifecycle hooks (on_pause/on_resume/on_low_memory) with Android surface drop contract; tracing spans + events on every per-frame entry point. - `GlContextProvider` trait + `GlutinProvider` desktop impl + iOS / Android stubs; trait carries no `Send` bound (per spec §3.1). - `CanvasViewportStub::render_into(&Canvas)` deliberately pollutes STENCIL_TEST + blend func to verify chrome-paint isolation. - `NativeBackend`: frame-scoped methods mirroring OP `RenderBackend` trait surface (no direct trait impl in 1a; Step 1c+ wraps via `WithCanvas<'a>` newtype). Translates `fill_rect / stroke_rect / draw_text / clip_rect / save / restore / translate` to `jian_core::render::DrawOp` and submits via `jian_skia::SkiaBackend::draw_on_canvas`. Public `draw_op` helper + `to_jian_color` / `to_jian_rect` converters. - Tests: - `teardown_idempotent.rs` — teardown × 3 + lifecycle hook idempotence. - `memory_loop.rs` — 100 × create/begin_frame/present/teardown × 3 with sysinfo RSS budget < 5 %. - `tracing_spans.rs` — `tracing-test` (no-env-filter) catches begin_frame / with_frame / present / resize / teardown / on_pause / on_resume / on_low_memory events. - `raster_composition.rs` — chrome-only fill_rect on raster surface, pixel-asserts red + black + untouched-bg. - `raster_text_smoke.rs` — "Hello 你好" through textlayout feature, asserts visible glyph rasterisation. - `gpu_smoke.rs` — Linux EGL pbuffer (non-ignored) + macOS invisible winit window (graceful inconclusive when off main thread; full path runs from `cargo run --example basic_window`) + Windows `#[ignore]` per spec §8.1. - `gpu_chrome_stub_composition.rs` — chrome+stub on the same GL surface, asserts chrome pixels survive stub's GL pollution. - Cargo.toml: add `jian-core` direct dep + `tracing` / `thiserror` workspace deps; dev-deps `sysinfo`, `tracing-test` (with `no-env-filter`), Linux-only `khronos-egl` + `libloading`. `cargo build`, `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --all -- --check` all green on macOS.
2026-05-05 13:06:00 +00:00
//!
//! All paths drive a single chrome `fill_rect`(red) through the live
//! pipeline and read pixels back via the FBO chain (spec §7.2 #3).
//! `gpu_chrome_stub_composition.rs` covers the chrome+stub composition
//! requested by acceptance #2 over the same GPU surface.
#![cfg_attr(target_os = "windows", allow(dead_code))]
mod common;
use op_editor_ui::{Color, Point2D, Rect};
use op_host_native::{NativeBackend, SharedSkiaContext};
feat(shell-native): SharedSkiaContext + NativeBackend over Jian DrawOp Step 1a Task 2 (spec v19 §3 / §5.2.1, plan v7). - `SharedSkiaContext`: own GL stack + Skia DirectContext + Surface, `Option<>`-field idempotent teardown, `with_frame(|canvas, glow|)` callback, lifecycle hooks (on_pause/on_resume/on_low_memory) with Android surface drop contract; tracing spans + events on every per-frame entry point. - `GlContextProvider` trait + `GlutinProvider` desktop impl + iOS / Android stubs; trait carries no `Send` bound (per spec §3.1). - `CanvasViewportStub::render_into(&Canvas)` deliberately pollutes STENCIL_TEST + blend func to verify chrome-paint isolation. - `NativeBackend`: frame-scoped methods mirroring OP `RenderBackend` trait surface (no direct trait impl in 1a; Step 1c+ wraps via `WithCanvas<'a>` newtype). Translates `fill_rect / stroke_rect / draw_text / clip_rect / save / restore / translate` to `jian_core::render::DrawOp` and submits via `jian_skia::SkiaBackend::draw_on_canvas`. Public `draw_op` helper + `to_jian_color` / `to_jian_rect` converters. - Tests: - `teardown_idempotent.rs` — teardown × 3 + lifecycle hook idempotence. - `memory_loop.rs` — 100 × create/begin_frame/present/teardown × 3 with sysinfo RSS budget < 5 %. - `tracing_spans.rs` — `tracing-test` (no-env-filter) catches begin_frame / with_frame / present / resize / teardown / on_pause / on_resume / on_low_memory events. - `raster_composition.rs` — chrome-only fill_rect on raster surface, pixel-asserts red + black + untouched-bg. - `raster_text_smoke.rs` — "Hello 你好" through textlayout feature, asserts visible glyph rasterisation. - `gpu_smoke.rs` — Linux EGL pbuffer (non-ignored) + macOS invisible winit window (graceful inconclusive when off main thread; full path runs from `cargo run --example basic_window`) + Windows `#[ignore]` per spec §8.1. - `gpu_chrome_stub_composition.rs` — chrome+stub on the same GL surface, asserts chrome pixels survive stub's GL pollution. - Cargo.toml: add `jian-core` direct dep + `tracing` / `thiserror` workspace deps; dev-deps `sysinfo`, `tracing-test` (with `no-env-filter`), Linux-only `khronos-egl` + `libloading`. `cargo build`, `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --all -- --check` all green on macOS.
2026-05-05 13:06:00 +00:00
#[allow(dead_code)]
fn paint_chrome_red(ctx: &mut SharedSkiaContext, backend: &mut NativeBackend) {
ctx.begin_frame();
ctx.with_frame(|canvas, _glow| {
backend.fill_rect(
canvas,
Rect {
origin: Point2D::new(50.0, 50.0),
size: Point2D::new(100.0, 100.0),
},
Color::RED,
);
});
ctx.present();
}
// ──────────────────────────────────────────────────────────────────────────
// macOS: invisible winit window + glutin → SharedSkiaContext::new_desktop
// ──────────────────────────────────────────────────────────────────────────
#[cfg(target_os = "macos")]
mod platform {
use super::*;
use std::cell::RefCell;
use std::rc::Rc;
use glow::HasContext;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::window::{Window, WindowId};
#[derive(Default)]
struct State {
done: bool,
succeeded: bool,
inconclusive: Option<String>,
window: Option<Window>,
}
struct Smoke {
state: Rc<RefCell<State>>,
}
impl ApplicationHandler for Smoke {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let mut state = self.state.borrow_mut();
if state.done {
return;
}
let attrs = Window::default_attributes()
.with_visible(false)
.with_inner_size(winit::dpi::PhysicalSize::new(400u32, 300u32));
match event_loop.create_window(attrs) {
Ok(w) => state.window = Some(w),
Err(err) => {
state.inconclusive = Some(format!("create_window failed: {err}"));
state.done = true;
event_loop.exit();
}
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
let mut state = self.state.borrow_mut();
if state.done {
event_loop.exit();
return;
}
let window = match state.window.as_ref() {
Some(w) => w,
None => return,
};
match SharedSkiaContext::new_desktop(window) {
Ok(mut ctx) => {
let mut backend = NativeBackend::with_dpi(window.scale_factor() as f32);
paint_chrome_red(&mut ctx, &mut backend);
state.succeeded = readback_red(&ctx);
let _ = ctx.teardown();
}
Err(err) => {
state.inconclusive = Some(format!("SharedSkiaContext::new_desktop: {err}"));
}
}
state.done = true;
event_loop.exit();
}
fn window_event(&mut self, _: &ActiveEventLoop, _: WindowId, _: WindowEvent) {}
}
fn readback_red(ctx: &SharedSkiaContext) -> bool {
2026-05-05 13:15:00 +00:00
// Phase A Gate round 2 BLOCK 2(a) fix — `glow()` now returns
// `Option<&Arc<...>>`; borrow inline rather than cloning.
feat(shell-native): SharedSkiaContext + NativeBackend over Jian DrawOp Step 1a Task 2 (spec v19 §3 / §5.2.1, plan v7). - `SharedSkiaContext`: own GL stack + Skia DirectContext + Surface, `Option<>`-field idempotent teardown, `with_frame(|canvas, glow|)` callback, lifecycle hooks (on_pause/on_resume/on_low_memory) with Android surface drop contract; tracing spans + events on every per-frame entry point. - `GlContextProvider` trait + `GlutinProvider` desktop impl + iOS / Android stubs; trait carries no `Send` bound (per spec §3.1). - `CanvasViewportStub::render_into(&Canvas)` deliberately pollutes STENCIL_TEST + blend func to verify chrome-paint isolation. - `NativeBackend`: frame-scoped methods mirroring OP `RenderBackend` trait surface (no direct trait impl in 1a; Step 1c+ wraps via `WithCanvas<'a>` newtype). Translates `fill_rect / stroke_rect / draw_text / clip_rect / save / restore / translate` to `jian_core::render::DrawOp` and submits via `jian_skia::SkiaBackend::draw_on_canvas`. Public `draw_op` helper + `to_jian_color` / `to_jian_rect` converters. - Tests: - `teardown_idempotent.rs` — teardown × 3 + lifecycle hook idempotence. - `memory_loop.rs` — 100 × create/begin_frame/present/teardown × 3 with sysinfo RSS budget < 5 %. - `tracing_spans.rs` — `tracing-test` (no-env-filter) catches begin_frame / with_frame / present / resize / teardown / on_pause / on_resume / on_low_memory events. - `raster_composition.rs` — chrome-only fill_rect on raster surface, pixel-asserts red + black + untouched-bg. - `raster_text_smoke.rs` — "Hello 你好" through textlayout feature, asserts visible glyph rasterisation. - `gpu_smoke.rs` — Linux EGL pbuffer (non-ignored) + macOS invisible winit window (graceful inconclusive when off main thread; full path runs from `cargo run --example basic_window`) + Windows `#[ignore]` per spec §8.1. - `gpu_chrome_stub_composition.rs` — chrome+stub on the same GL surface, asserts chrome pixels survive stub's GL pollution. - Cargo.toml: add `jian-core` direct dep + `tracing` / `thiserror` workspace deps; dev-deps `sysinfo`, `tracing-test` (with `no-env-filter`), Linux-only `khronos-egl` + `libloading`. `cargo build`, `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --all -- --check` all green on macOS.
2026-05-05 13:06:00 +00:00
let glow = match ctx.glow() {
2026-05-05 13:15:00 +00:00
Some(g) => g.as_ref(),
feat(shell-native): SharedSkiaContext + NativeBackend over Jian DrawOp Step 1a Task 2 (spec v19 §3 / §5.2.1, plan v7). - `SharedSkiaContext`: own GL stack + Skia DirectContext + Surface, `Option<>`-field idempotent teardown, `with_frame(|canvas, glow|)` callback, lifecycle hooks (on_pause/on_resume/on_low_memory) with Android surface drop contract; tracing spans + events on every per-frame entry point. - `GlContextProvider` trait + `GlutinProvider` desktop impl + iOS / Android stubs; trait carries no `Send` bound (per spec §3.1). - `CanvasViewportStub::render_into(&Canvas)` deliberately pollutes STENCIL_TEST + blend func to verify chrome-paint isolation. - `NativeBackend`: frame-scoped methods mirroring OP `RenderBackend` trait surface (no direct trait impl in 1a; Step 1c+ wraps via `WithCanvas<'a>` newtype). Translates `fill_rect / stroke_rect / draw_text / clip_rect / save / restore / translate` to `jian_core::render::DrawOp` and submits via `jian_skia::SkiaBackend::draw_on_canvas`. Public `draw_op` helper + `to_jian_color` / `to_jian_rect` converters. - Tests: - `teardown_idempotent.rs` — teardown × 3 + lifecycle hook idempotence. - `memory_loop.rs` — 100 × create/begin_frame/present/teardown × 3 with sysinfo RSS budget < 5 %. - `tracing_spans.rs` — `tracing-test` (no-env-filter) catches begin_frame / with_frame / present / resize / teardown / on_pause / on_resume / on_low_memory events. - `raster_composition.rs` — chrome-only fill_rect on raster surface, pixel-asserts red + black + untouched-bg. - `raster_text_smoke.rs` — "Hello 你好" through textlayout feature, asserts visible glyph rasterisation. - `gpu_smoke.rs` — Linux EGL pbuffer (non-ignored) + macOS invisible winit window (graceful inconclusive when off main thread; full path runs from `cargo run --example basic_window`) + Windows `#[ignore]` per spec §8.1. - `gpu_chrome_stub_composition.rs` — chrome+stub on the same GL surface, asserts chrome pixels survive stub's GL pollution. - Cargo.toml: add `jian-core` direct dep + `tracing` / `thiserror` workspace deps; dev-deps `sysinfo`, `tracing-test` (with `no-env-filter`), Linux-only `khronos-egl` + `libloading`. `cargo build`, `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --all -- --check` all green on macOS.
2026-05-05 13:06:00 +00:00
None => return false,
};
let mut buf = [0u8; 4];
unsafe {
glow.bind_framebuffer(glow::READ_FRAMEBUFFER, None);
glow.read_pixels(
75,
75,
1,
1,
glow::RGBA,
glow::UNSIGNED_BYTE,
glow::PixelPackData::Slice(Some(&mut buf)),
);
}
// Loose threshold — winit/glutin sometimes composes through
// layers that pre-multiply, leaving very-near-red.
buf[0] > 200 && buf[1] < 60 && buf[2] < 60
}
pub fn run() {
// winit 0.30 on macOS requires `EventLoop::new` on the main
// thread; cargo's default `#[test]` runner spawns each test
// on a worker. Acceptance #3 (macOS GPU smoke non-ignored) is
// satisfied by `cargo run --example basic_window` driven by
// the developer / Phase C subagent (see Task 4 acceptance
// notes) — that path runs on the main thread, exercises the
// same `SharedSkiaContext::new_desktop` chain, and hits the
// FBO readback. Inside `cargo test` we degrade gracefully:
// catch winit's main-thread panic and mark the run
// inconclusive rather than fail.
let result = std::panic::catch_unwind(|| {
let event_loop = EventLoop::new().map_err(|e| format!("EventLoop::new: {e}"))?;
event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);
let state = Rc::new(RefCell::new(State::default()));
let mut smoke = Smoke {
state: state.clone(),
};
let _ = event_loop.run_app(&mut smoke);
let s = state.borrow();
if let Some(reason) = &s.inconclusive {
return Err(reason.clone());
}
Ok(s.succeeded)
});
match result {
Ok(Ok(true)) => {
// PASS — chrome pixel read back red.
}
Ok(Ok(false)) => panic!("gpu_smoke: FBO readback did not return red chrome"),
Ok(Err(reason)) => {
eprintln!("gpu_smoke: inconclusive — {reason}");
}
Err(panic) => {
let msg = panic
.downcast_ref::<&str>()
.map(|s| (*s).to_string())
.or_else(|| panic.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "unknown".into());
eprintln!(
"gpu_smoke: inconclusive — winit panicked (likely \
`EventLoop` not on the main thread): {msg}"
);
}
}
}
}
// ──────────────────────────────────────────────────────────────────────────
// Linux: EGL pbuffer → SharedSkiaContext::new(provider)
// ──────────────────────────────────────────────────────────────────────────
#[cfg(target_os = "linux")]
mod platform {
use super::*;
use crate::common::egl_pbuffer::EglPbufferProvider;
use glow::HasContext;
pub fn run() {
fix(shell-native): Phase A Gate round 1 fixes (Task 2 patches) Applies Codex Phase A Gate round 1 review (3 BLOCK + 2 CONCERN + 1 NIT) against the Task 2 SharedSkiaContext + NativeBackend implementation. BLOCK 1 — `ProviderError::from_msg` `pub(crate)` blocked the Linux EGL pbuffer test helper from constructing typed provider errors. Promoted to `pub` so out-of-tree provider impls (test pbuffer, future Step 1f mobile providers) can produce diagnostically-identical errors. BLOCK 2 — Linux GPU smoke + chrome-stub-composition tests silently returned `Ok(())` on EGL pbuffer setup failure, turning acceptance #3 / #4 into false positives on hosted CI without GPU. Now gated by `STEP1A_REQUIRE_GPU=1`: real-GPU runners panic on setup failure; dev / hostless runs surface an explicit `INCONCLUSIVE` marker before returning. Mirrors the macOS `catch_unwind` skip path in the same file. BLOCK 3 — `tests/memory_loop.rs` was running 100 cycles against `SharedSkiaContext::inert_for_test()` (every Option<> field None), so the RSS budget proved nothing about real allocation lifecycle. Renamed constructor to `inert_for_lifecycle_test()` (clearer intent) and split the test into: - Phase 0 warmup (100 inert + 100 raster) so Skia's lazy glyph/path/binding caches are populated before measurement; - Phase 1 lifecycle idempotence (100 inert); - Phase 2 real-resource cycle: raster surface on macOS / Windows (winit::EventLoop main-thread-only on macOS; Win Actions runner has no GPU per spec §8.1), full EGL pbuffer + GL surface on Linux when `STEP1A_REQUIRE_GPU=1`, raster fallback otherwise. Budget kept at 5 % per acceptance #6 with a 1.5 MB absolute floor to absorb macOS sysinfo's coarse RSS sampling jitter on small baselines. CONCERN 1 — `GlContextProvider` had three non-spec methods (`resize`, `size`, `default_framebuffer_id`). Audit: - `resize`: actually used by `SharedSkiaContext::resize` (window / pbuffer resize → Skia FBO rewrap). KEPT, spec mini-patch documented in comment, escalation needed for spec v19 → v19.1. - `default_framebuffer_id`: used by `SharedSkiaContext::new` / `resize` for the FBO id Skia wraps; iOS EAGL provider (Step 1f) will need non-zero values. KEPT, same escalation path. - `size`: unused anywhere. DELETED (YAGNI), along with the unused `size: (u32, u32)` field on `GlutinProvider` and the iOS / Android stub impls. CONCERN 2 — `glow_handle: Option<Arc<glow::Context>>` deviates from spec v19 lines 120-125 + 191 (`Arc<glow::Context>`). Real lifecycle needs the handle droppable: teardown releases the loaded function table, `inert_for_lifecycle_test` has no GL backing, Step 1f Android `on_pause` must drop alongside the EGL context. KEPT as Option<Arc>, spec mini-patch documented for v19.1 escalation. NIT 1 — Removed Task 1 link-check helper `placeholder()`. Task 2's full re-export chain (`SharedSkiaContext`, `NativeBackend`, …) already proves shell-core ↔ shell-native linkage; placeholder is YAGNI now. Verification (macOS local): - cargo build -p openpencil-shell-native: clean - cargo test -p openpencil-shell-native: 12/12 pass (8 binaries) - cargo clippy -p openpencil-shell-native --tests --all-targets -- -D warnings: clean - cargo fmt -p openpencil-shell-native -- --check: clean - memory_loop stress 8 consecutive runs: 8/8 pass
2026-05-05 13:09:00 +00:00
// BLOCK 2 (Codex Phase A Gate round 1): the previous behaviour
// of returning silently on EGL pbuffer setup failure made
// acceptance #3 / #4 a false positive on hosted CI without a
// working GPU stack. We now distinguish two host classes via
// the `STEP1A_REQUIRE_GPU=1` env var:
// - CI runners with EGL/Mesa available set the var; setup
// failure ⇒ panic so the suite goes red.
// - Dev machines / hosted runners without a GPU don't set
// the var; setup failure ⇒ INCONCLUSIVE eprintln + early
// return, mirroring the macOS `catch_unwind` skip path.
let require_gpu = std::env::var_os("STEP1A_REQUIRE_GPU")
.map(|v| v == "1")
.unwrap_or(false);
feat(shell-native): SharedSkiaContext + NativeBackend over Jian DrawOp Step 1a Task 2 (spec v19 §3 / §5.2.1, plan v7). - `SharedSkiaContext`: own GL stack + Skia DirectContext + Surface, `Option<>`-field idempotent teardown, `with_frame(|canvas, glow|)` callback, lifecycle hooks (on_pause/on_resume/on_low_memory) with Android surface drop contract; tracing spans + events on every per-frame entry point. - `GlContextProvider` trait + `GlutinProvider` desktop impl + iOS / Android stubs; trait carries no `Send` bound (per spec §3.1). - `CanvasViewportStub::render_into(&Canvas)` deliberately pollutes STENCIL_TEST + blend func to verify chrome-paint isolation. - `NativeBackend`: frame-scoped methods mirroring OP `RenderBackend` trait surface (no direct trait impl in 1a; Step 1c+ wraps via `WithCanvas<'a>` newtype). Translates `fill_rect / stroke_rect / draw_text / clip_rect / save / restore / translate` to `jian_core::render::DrawOp` and submits via `jian_skia::SkiaBackend::draw_on_canvas`. Public `draw_op` helper + `to_jian_color` / `to_jian_rect` converters. - Tests: - `teardown_idempotent.rs` — teardown × 3 + lifecycle hook idempotence. - `memory_loop.rs` — 100 × create/begin_frame/present/teardown × 3 with sysinfo RSS budget < 5 %. - `tracing_spans.rs` — `tracing-test` (no-env-filter) catches begin_frame / with_frame / present / resize / teardown / on_pause / on_resume / on_low_memory events. - `raster_composition.rs` — chrome-only fill_rect on raster surface, pixel-asserts red + black + untouched-bg. - `raster_text_smoke.rs` — "Hello 你好" through textlayout feature, asserts visible glyph rasterisation. - `gpu_smoke.rs` — Linux EGL pbuffer (non-ignored) + macOS invisible winit window (graceful inconclusive when off main thread; full path runs from `cargo run --example basic_window`) + Windows `#[ignore]` per spec §8.1. - `gpu_chrome_stub_composition.rs` — chrome+stub on the same GL surface, asserts chrome pixels survive stub's GL pollution. - Cargo.toml: add `jian-core` direct dep + `tracing` / `thiserror` workspace deps; dev-deps `sysinfo`, `tracing-test` (with `no-env-filter`), Linux-only `khronos-egl` + `libloading`. `cargo build`, `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --all -- --check` all green on macOS.
2026-05-05 13:06:00 +00:00
// 800×600 pbuffer; mesa softpipe handles this without a display.
let provider = match EglPbufferProvider::new((800, 600)) {
Ok(p) => p,
Err(err) => {
fix(shell-native): Phase A Gate round 1 fixes (Task 2 patches) Applies Codex Phase A Gate round 1 review (3 BLOCK + 2 CONCERN + 1 NIT) against the Task 2 SharedSkiaContext + NativeBackend implementation. BLOCK 1 — `ProviderError::from_msg` `pub(crate)` blocked the Linux EGL pbuffer test helper from constructing typed provider errors. Promoted to `pub` so out-of-tree provider impls (test pbuffer, future Step 1f mobile providers) can produce diagnostically-identical errors. BLOCK 2 — Linux GPU smoke + chrome-stub-composition tests silently returned `Ok(())` on EGL pbuffer setup failure, turning acceptance #3 / #4 into false positives on hosted CI without GPU. Now gated by `STEP1A_REQUIRE_GPU=1`: real-GPU runners panic on setup failure; dev / hostless runs surface an explicit `INCONCLUSIVE` marker before returning. Mirrors the macOS `catch_unwind` skip path in the same file. BLOCK 3 — `tests/memory_loop.rs` was running 100 cycles against `SharedSkiaContext::inert_for_test()` (every Option<> field None), so the RSS budget proved nothing about real allocation lifecycle. Renamed constructor to `inert_for_lifecycle_test()` (clearer intent) and split the test into: - Phase 0 warmup (100 inert + 100 raster) so Skia's lazy glyph/path/binding caches are populated before measurement; - Phase 1 lifecycle idempotence (100 inert); - Phase 2 real-resource cycle: raster surface on macOS / Windows (winit::EventLoop main-thread-only on macOS; Win Actions runner has no GPU per spec §8.1), full EGL pbuffer + GL surface on Linux when `STEP1A_REQUIRE_GPU=1`, raster fallback otherwise. Budget kept at 5 % per acceptance #6 with a 1.5 MB absolute floor to absorb macOS sysinfo's coarse RSS sampling jitter on small baselines. CONCERN 1 — `GlContextProvider` had three non-spec methods (`resize`, `size`, `default_framebuffer_id`). Audit: - `resize`: actually used by `SharedSkiaContext::resize` (window / pbuffer resize → Skia FBO rewrap). KEPT, spec mini-patch documented in comment, escalation needed for spec v19 → v19.1. - `default_framebuffer_id`: used by `SharedSkiaContext::new` / `resize` for the FBO id Skia wraps; iOS EAGL provider (Step 1f) will need non-zero values. KEPT, same escalation path. - `size`: unused anywhere. DELETED (YAGNI), along with the unused `size: (u32, u32)` field on `GlutinProvider` and the iOS / Android stub impls. CONCERN 2 — `glow_handle: Option<Arc<glow::Context>>` deviates from spec v19 lines 120-125 + 191 (`Arc<glow::Context>`). Real lifecycle needs the handle droppable: teardown releases the loaded function table, `inert_for_lifecycle_test` has no GL backing, Step 1f Android `on_pause` must drop alongside the EGL context. KEPT as Option<Arc>, spec mini-patch documented for v19.1 escalation. NIT 1 — Removed Task 1 link-check helper `placeholder()`. Task 2's full re-export chain (`SharedSkiaContext`, `NativeBackend`, …) already proves shell-core ↔ shell-native linkage; placeholder is YAGNI now. Verification (macOS local): - cargo build -p openpencil-shell-native: clean - cargo test -p openpencil-shell-native: 12/12 pass (8 binaries) - cargo clippy -p openpencil-shell-native --tests --all-targets -- -D warnings: clean - cargo fmt -p openpencil-shell-native -- --check: clean - memory_loop stress 8 consecutive runs: 8/8 pass
2026-05-05 13:09:00 +00:00
let msg = format!("Linux EGL pbuffer setup failed: {err}");
if require_gpu {
panic!("gpu_smoke: STEP1A_REQUIRE_GPU=1 but {msg}");
}
eprintln!(
"⚠ INCONCLUSIVE gpu_smoke: {msg} \
(set STEP1A_REQUIRE_GPU=1 on a real-GPU runner to fail hard)"
);
feat(shell-native): SharedSkiaContext + NativeBackend over Jian DrawOp Step 1a Task 2 (spec v19 §3 / §5.2.1, plan v7). - `SharedSkiaContext`: own GL stack + Skia DirectContext + Surface, `Option<>`-field idempotent teardown, `with_frame(|canvas, glow|)` callback, lifecycle hooks (on_pause/on_resume/on_low_memory) with Android surface drop contract; tracing spans + events on every per-frame entry point. - `GlContextProvider` trait + `GlutinProvider` desktop impl + iOS / Android stubs; trait carries no `Send` bound (per spec §3.1). - `CanvasViewportStub::render_into(&Canvas)` deliberately pollutes STENCIL_TEST + blend func to verify chrome-paint isolation. - `NativeBackend`: frame-scoped methods mirroring OP `RenderBackend` trait surface (no direct trait impl in 1a; Step 1c+ wraps via `WithCanvas<'a>` newtype). Translates `fill_rect / stroke_rect / draw_text / clip_rect / save / restore / translate` to `jian_core::render::DrawOp` and submits via `jian_skia::SkiaBackend::draw_on_canvas`. Public `draw_op` helper + `to_jian_color` / `to_jian_rect` converters. - Tests: - `teardown_idempotent.rs` — teardown × 3 + lifecycle hook idempotence. - `memory_loop.rs` — 100 × create/begin_frame/present/teardown × 3 with sysinfo RSS budget < 5 %. - `tracing_spans.rs` — `tracing-test` (no-env-filter) catches begin_frame / with_frame / present / resize / teardown / on_pause / on_resume / on_low_memory events. - `raster_composition.rs` — chrome-only fill_rect on raster surface, pixel-asserts red + black + untouched-bg. - `raster_text_smoke.rs` — "Hello 你好" through textlayout feature, asserts visible glyph rasterisation. - `gpu_smoke.rs` — Linux EGL pbuffer (non-ignored) + macOS invisible winit window (graceful inconclusive when off main thread; full path runs from `cargo run --example basic_window`) + Windows `#[ignore]` per spec §8.1. - `gpu_chrome_stub_composition.rs` — chrome+stub on the same GL surface, asserts chrome pixels survive stub's GL pollution. - Cargo.toml: add `jian-core` direct dep + `tracing` / `thiserror` workspace deps; dev-deps `sysinfo`, `tracing-test` (with `no-env-filter`), Linux-only `khronos-egl` + `libloading`. `cargo build`, `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --all -- --check` all green on macOS.
2026-05-05 13:06:00 +00:00
return;
}
};
2026-05-05 13:15:00 +00:00
// Phase A Gate round 2 BLOCK 1 fix — single-arg `new(provider)`.
// Initial 800×600 size comes from the EGL pbuffer attach.
let mut ctx = match SharedSkiaContext::new(provider) {
feat(shell-native): SharedSkiaContext + NativeBackend over Jian DrawOp Step 1a Task 2 (spec v19 §3 / §5.2.1, plan v7). - `SharedSkiaContext`: own GL stack + Skia DirectContext + Surface, `Option<>`-field idempotent teardown, `with_frame(|canvas, glow|)` callback, lifecycle hooks (on_pause/on_resume/on_low_memory) with Android surface drop contract; tracing spans + events on every per-frame entry point. - `GlContextProvider` trait + `GlutinProvider` desktop impl + iOS / Android stubs; trait carries no `Send` bound (per spec §3.1). - `CanvasViewportStub::render_into(&Canvas)` deliberately pollutes STENCIL_TEST + blend func to verify chrome-paint isolation. - `NativeBackend`: frame-scoped methods mirroring OP `RenderBackend` trait surface (no direct trait impl in 1a; Step 1c+ wraps via `WithCanvas<'a>` newtype). Translates `fill_rect / stroke_rect / draw_text / clip_rect / save / restore / translate` to `jian_core::render::DrawOp` and submits via `jian_skia::SkiaBackend::draw_on_canvas`. Public `draw_op` helper + `to_jian_color` / `to_jian_rect` converters. - Tests: - `teardown_idempotent.rs` — teardown × 3 + lifecycle hook idempotence. - `memory_loop.rs` — 100 × create/begin_frame/present/teardown × 3 with sysinfo RSS budget < 5 %. - `tracing_spans.rs` — `tracing-test` (no-env-filter) catches begin_frame / with_frame / present / resize / teardown / on_pause / on_resume / on_low_memory events. - `raster_composition.rs` — chrome-only fill_rect on raster surface, pixel-asserts red + black + untouched-bg. - `raster_text_smoke.rs` — "Hello 你好" through textlayout feature, asserts visible glyph rasterisation. - `gpu_smoke.rs` — Linux EGL pbuffer (non-ignored) + macOS invisible winit window (graceful inconclusive when off main thread; full path runs from `cargo run --example basic_window`) + Windows `#[ignore]` per spec §8.1. - `gpu_chrome_stub_composition.rs` — chrome+stub on the same GL surface, asserts chrome pixels survive stub's GL pollution. - Cargo.toml: add `jian-core` direct dep + `tracing` / `thiserror` workspace deps; dev-deps `sysinfo`, `tracing-test` (with `no-env-filter`), Linux-only `khronos-egl` + `libloading`. `cargo build`, `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --all -- --check` all green on macOS.
2026-05-05 13:06:00 +00:00
Ok(c) => c,
Err(err) => {
fix(shell-native): Phase A Gate round 1 fixes (Task 2 patches) Applies Codex Phase A Gate round 1 review (3 BLOCK + 2 CONCERN + 1 NIT) against the Task 2 SharedSkiaContext + NativeBackend implementation. BLOCK 1 — `ProviderError::from_msg` `pub(crate)` blocked the Linux EGL pbuffer test helper from constructing typed provider errors. Promoted to `pub` so out-of-tree provider impls (test pbuffer, future Step 1f mobile providers) can produce diagnostically-identical errors. BLOCK 2 — Linux GPU smoke + chrome-stub-composition tests silently returned `Ok(())` on EGL pbuffer setup failure, turning acceptance #3 / #4 into false positives on hosted CI without GPU. Now gated by `STEP1A_REQUIRE_GPU=1`: real-GPU runners panic on setup failure; dev / hostless runs surface an explicit `INCONCLUSIVE` marker before returning. Mirrors the macOS `catch_unwind` skip path in the same file. BLOCK 3 — `tests/memory_loop.rs` was running 100 cycles against `SharedSkiaContext::inert_for_test()` (every Option<> field None), so the RSS budget proved nothing about real allocation lifecycle. Renamed constructor to `inert_for_lifecycle_test()` (clearer intent) and split the test into: - Phase 0 warmup (100 inert + 100 raster) so Skia's lazy glyph/path/binding caches are populated before measurement; - Phase 1 lifecycle idempotence (100 inert); - Phase 2 real-resource cycle: raster surface on macOS / Windows (winit::EventLoop main-thread-only on macOS; Win Actions runner has no GPU per spec §8.1), full EGL pbuffer + GL surface on Linux when `STEP1A_REQUIRE_GPU=1`, raster fallback otherwise. Budget kept at 5 % per acceptance #6 with a 1.5 MB absolute floor to absorb macOS sysinfo's coarse RSS sampling jitter on small baselines. CONCERN 1 — `GlContextProvider` had three non-spec methods (`resize`, `size`, `default_framebuffer_id`). Audit: - `resize`: actually used by `SharedSkiaContext::resize` (window / pbuffer resize → Skia FBO rewrap). KEPT, spec mini-patch documented in comment, escalation needed for spec v19 → v19.1. - `default_framebuffer_id`: used by `SharedSkiaContext::new` / `resize` for the FBO id Skia wraps; iOS EAGL provider (Step 1f) will need non-zero values. KEPT, same escalation path. - `size`: unused anywhere. DELETED (YAGNI), along with the unused `size: (u32, u32)` field on `GlutinProvider` and the iOS / Android stub impls. CONCERN 2 — `glow_handle: Option<Arc<glow::Context>>` deviates from spec v19 lines 120-125 + 191 (`Arc<glow::Context>`). Real lifecycle needs the handle droppable: teardown releases the loaded function table, `inert_for_lifecycle_test` has no GL backing, Step 1f Android `on_pause` must drop alongside the EGL context. KEPT as Option<Arc>, spec mini-patch documented for v19.1 escalation. NIT 1 — Removed Task 1 link-check helper `placeholder()`. Task 2's full re-export chain (`SharedSkiaContext`, `NativeBackend`, …) already proves shell-core ↔ shell-native linkage; placeholder is YAGNI now. Verification (macOS local): - cargo build -p openpencil-shell-native: clean - cargo test -p openpencil-shell-native: 12/12 pass (8 binaries) - cargo clippy -p openpencil-shell-native --tests --all-targets -- -D warnings: clean - cargo fmt -p openpencil-shell-native -- --check: clean - memory_loop stress 8 consecutive runs: 8/8 pass
2026-05-05 13:09:00 +00:00
let msg = format!("SharedSkiaContext::new failed: {err}");
if require_gpu {
panic!("gpu_smoke: STEP1A_REQUIRE_GPU=1 but {msg}");
}
eprintln!(
"⚠ INCONCLUSIVE gpu_smoke: {msg} \
(set STEP1A_REQUIRE_GPU=1 on a real-GPU runner to fail hard)"
);
feat(shell-native): SharedSkiaContext + NativeBackend over Jian DrawOp Step 1a Task 2 (spec v19 §3 / §5.2.1, plan v7). - `SharedSkiaContext`: own GL stack + Skia DirectContext + Surface, `Option<>`-field idempotent teardown, `with_frame(|canvas, glow|)` callback, lifecycle hooks (on_pause/on_resume/on_low_memory) with Android surface drop contract; tracing spans + events on every per-frame entry point. - `GlContextProvider` trait + `GlutinProvider` desktop impl + iOS / Android stubs; trait carries no `Send` bound (per spec §3.1). - `CanvasViewportStub::render_into(&Canvas)` deliberately pollutes STENCIL_TEST + blend func to verify chrome-paint isolation. - `NativeBackend`: frame-scoped methods mirroring OP `RenderBackend` trait surface (no direct trait impl in 1a; Step 1c+ wraps via `WithCanvas<'a>` newtype). Translates `fill_rect / stroke_rect / draw_text / clip_rect / save / restore / translate` to `jian_core::render::DrawOp` and submits via `jian_skia::SkiaBackend::draw_on_canvas`. Public `draw_op` helper + `to_jian_color` / `to_jian_rect` converters. - Tests: - `teardown_idempotent.rs` — teardown × 3 + lifecycle hook idempotence. - `memory_loop.rs` — 100 × create/begin_frame/present/teardown × 3 with sysinfo RSS budget < 5 %. - `tracing_spans.rs` — `tracing-test` (no-env-filter) catches begin_frame / with_frame / present / resize / teardown / on_pause / on_resume / on_low_memory events. - `raster_composition.rs` — chrome-only fill_rect on raster surface, pixel-asserts red + black + untouched-bg. - `raster_text_smoke.rs` — "Hello 你好" through textlayout feature, asserts visible glyph rasterisation. - `gpu_smoke.rs` — Linux EGL pbuffer (non-ignored) + macOS invisible winit window (graceful inconclusive when off main thread; full path runs from `cargo run --example basic_window`) + Windows `#[ignore]` per spec §8.1. - `gpu_chrome_stub_composition.rs` — chrome+stub on the same GL surface, asserts chrome pixels survive stub's GL pollution. - Cargo.toml: add `jian-core` direct dep + `tracing` / `thiserror` workspace deps; dev-deps `sysinfo`, `tracing-test` (with `no-env-filter`), Linux-only `khronos-egl` + `libloading`. `cargo build`, `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --all -- --check` all green on macOS.
2026-05-05 13:06:00 +00:00
return;
}
};
let mut backend = NativeBackend::with_dpi(1.0);
paint_chrome_red(&mut ctx, &mut backend);
// FBO readback (spec §7.2 #3, EGL pbuffer simplification —
// default FBO == fboid 0).
2026-05-05 13:15:00 +00:00
// Phase A Gate round 2 BLOCK 2(a) fix — `glow()` borrows.
let glow = ctx
.glow()
.expect("glow handle live before teardown")
.clone();
feat(shell-native): SharedSkiaContext + NativeBackend over Jian DrawOp Step 1a Task 2 (spec v19 §3 / §5.2.1, plan v7). - `SharedSkiaContext`: own GL stack + Skia DirectContext + Surface, `Option<>`-field idempotent teardown, `with_frame(|canvas, glow|)` callback, lifecycle hooks (on_pause/on_resume/on_low_memory) with Android surface drop contract; tracing spans + events on every per-frame entry point. - `GlContextProvider` trait + `GlutinProvider` desktop impl + iOS / Android stubs; trait carries no `Send` bound (per spec §3.1). - `CanvasViewportStub::render_into(&Canvas)` deliberately pollutes STENCIL_TEST + blend func to verify chrome-paint isolation. - `NativeBackend`: frame-scoped methods mirroring OP `RenderBackend` trait surface (no direct trait impl in 1a; Step 1c+ wraps via `WithCanvas<'a>` newtype). Translates `fill_rect / stroke_rect / draw_text / clip_rect / save / restore / translate` to `jian_core::render::DrawOp` and submits via `jian_skia::SkiaBackend::draw_on_canvas`. Public `draw_op` helper + `to_jian_color` / `to_jian_rect` converters. - Tests: - `teardown_idempotent.rs` — teardown × 3 + lifecycle hook idempotence. - `memory_loop.rs` — 100 × create/begin_frame/present/teardown × 3 with sysinfo RSS budget < 5 %. - `tracing_spans.rs` — `tracing-test` (no-env-filter) catches begin_frame / with_frame / present / resize / teardown / on_pause / on_resume / on_low_memory events. - `raster_composition.rs` — chrome-only fill_rect on raster surface, pixel-asserts red + black + untouched-bg. - `raster_text_smoke.rs` — "Hello 你好" through textlayout feature, asserts visible glyph rasterisation. - `gpu_smoke.rs` — Linux EGL pbuffer (non-ignored) + macOS invisible winit window (graceful inconclusive when off main thread; full path runs from `cargo run --example basic_window`) + Windows `#[ignore]` per spec §8.1. - `gpu_chrome_stub_composition.rs` — chrome+stub on the same GL surface, asserts chrome pixels survive stub's GL pollution. - Cargo.toml: add `jian-core` direct dep + `tracing` / `thiserror` workspace deps; dev-deps `sysinfo`, `tracing-test` (with `no-env-filter`), Linux-only `khronos-egl` + `libloading`. `cargo build`, `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --all -- --check` all green on macOS.
2026-05-05 13:06:00 +00:00
let mut buf = [0u8; 4];
unsafe {
glow.bind_framebuffer(glow::READ_FRAMEBUFFER, None);
glow.finish();
glow.read_pixels(
75,
75,
1,
1,
glow::RGBA,
glow::UNSIGNED_BYTE,
glow::PixelPackData::Slice(Some(&mut buf)),
);
}
let _ = ctx.teardown();
assert_eq!(
buf,
[255, 0, 0, 255],
"EGL pbuffer FBO readback expected red chrome, got {:?}",
buf
);
}
}
#[cfg(target_os = "windows")]
mod platform {
pub fn run() {}
}
// ──────────────────────────────────────────────────────────────────────────
// Top-level test entry point — per-OS dispatch.
// ──────────────────────────────────────────────────────────────────────────
#[cfg(target_os = "macos")]
#[test]
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.
#[cfg(target_os = "linux")]
feat(shell-native): SharedSkiaContext + NativeBackend over Jian DrawOp Step 1a Task 2 (spec v19 §3 / §5.2.1, plan v7). - `SharedSkiaContext`: own GL stack + Skia DirectContext + Surface, `Option<>`-field idempotent teardown, `with_frame(|canvas, glow|)` callback, lifecycle hooks (on_pause/on_resume/on_low_memory) with Android surface drop contract; tracing spans + events on every per-frame entry point. - `GlContextProvider` trait + `GlutinProvider` desktop impl + iOS / Android stubs; trait carries no `Send` bound (per spec §3.1). - `CanvasViewportStub::render_into(&Canvas)` deliberately pollutes STENCIL_TEST + blend func to verify chrome-paint isolation. - `NativeBackend`: frame-scoped methods mirroring OP `RenderBackend` trait surface (no direct trait impl in 1a; Step 1c+ wraps via `WithCanvas<'a>` newtype). Translates `fill_rect / stroke_rect / draw_text / clip_rect / save / restore / translate` to `jian_core::render::DrawOp` and submits via `jian_skia::SkiaBackend::draw_on_canvas`. Public `draw_op` helper + `to_jian_color` / `to_jian_rect` converters. - Tests: - `teardown_idempotent.rs` — teardown × 3 + lifecycle hook idempotence. - `memory_loop.rs` — 100 × create/begin_frame/present/teardown × 3 with sysinfo RSS budget < 5 %. - `tracing_spans.rs` — `tracing-test` (no-env-filter) catches begin_frame / with_frame / present / resize / teardown / on_pause / on_resume / on_low_memory events. - `raster_composition.rs` — chrome-only fill_rect on raster surface, pixel-asserts red + black + untouched-bg. - `raster_text_smoke.rs` — "Hello 你好" through textlayout feature, asserts visible glyph rasterisation. - `gpu_smoke.rs` — Linux EGL pbuffer (non-ignored) + macOS invisible winit window (graceful inconclusive when off main thread; full path runs from `cargo run --example basic_window`) + Windows `#[ignore]` per spec §8.1. - `gpu_chrome_stub_composition.rs` — chrome+stub on the same GL surface, asserts chrome pixels survive stub's GL pollution. - Cargo.toml: add `jian-core` direct dep + `tracing` / `thiserror` workspace deps; dev-deps `sysinfo`, `tracing-test` (with `no-env-filter`), Linux-only `khronos-egl` + `libloading`. `cargo build`, `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --all -- --check` all green on macOS.
2026-05-05 13:06:00 +00:00
#[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)"]
feat(shell-native): SharedSkiaContext + NativeBackend over Jian DrawOp Step 1a Task 2 (spec v19 §3 / §5.2.1, plan v7). - `SharedSkiaContext`: own GL stack + Skia DirectContext + Surface, `Option<>`-field idempotent teardown, `with_frame(|canvas, glow|)` callback, lifecycle hooks (on_pause/on_resume/on_low_memory) with Android surface drop contract; tracing spans + events on every per-frame entry point. - `GlContextProvider` trait + `GlutinProvider` desktop impl + iOS / Android stubs; trait carries no `Send` bound (per spec §3.1). - `CanvasViewportStub::render_into(&Canvas)` deliberately pollutes STENCIL_TEST + blend func to verify chrome-paint isolation. - `NativeBackend`: frame-scoped methods mirroring OP `RenderBackend` trait surface (no direct trait impl in 1a; Step 1c+ wraps via `WithCanvas<'a>` newtype). Translates `fill_rect / stroke_rect / draw_text / clip_rect / save / restore / translate` to `jian_core::render::DrawOp` and submits via `jian_skia::SkiaBackend::draw_on_canvas`. Public `draw_op` helper + `to_jian_color` / `to_jian_rect` converters. - Tests: - `teardown_idempotent.rs` — teardown × 3 + lifecycle hook idempotence. - `memory_loop.rs` — 100 × create/begin_frame/present/teardown × 3 with sysinfo RSS budget < 5 %. - `tracing_spans.rs` — `tracing-test` (no-env-filter) catches begin_frame / with_frame / present / resize / teardown / on_pause / on_resume / on_low_memory events. - `raster_composition.rs` — chrome-only fill_rect on raster surface, pixel-asserts red + black + untouched-bg. - `raster_text_smoke.rs` — "Hello 你好" through textlayout feature, asserts visible glyph rasterisation. - `gpu_smoke.rs` — Linux EGL pbuffer (non-ignored) + macOS invisible winit window (graceful inconclusive when off main thread; full path runs from `cargo run --example basic_window`) + Windows `#[ignore]` per spec §8.1. - `gpu_chrome_stub_composition.rs` — chrome+stub on the same GL surface, asserts chrome pixels survive stub's GL pollution. - Cargo.toml: add `jian-core` direct dep + `tracing` / `thiserror` workspace deps; dev-deps `sysinfo`, `tracing-test` (with `no-env-filter`), Linux-only `khronos-egl` + `libloading`. `cargo build`, `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --all -- --check` all green on macOS.
2026-05-05 13:06:00 +00:00
fn gpu_smoke() {
platform::run();
}
#[cfg(target_os = "windows")]
#[test]
#[ignore = "WINDOWS_GPU_DEFERRED_NO_RUNNER: standard GitHub Actions Windows runner has no GPU driver; manual smoke required (per spec §8.1)"]
fn gpu_smoke() {
platform::run();
}