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 acceptance #2 (chrome + canvas-stub on the same GPU surface)
|
|
|
|
|
//! / plan v7 Task 2 Step 16d / round 6 BLOCK-R6-2.
|
|
|
|
|
//!
|
|
|
|
|
//! Companion to `gpu_smoke.rs`: this test specifically asserts that
|
|
|
|
|
//! `CanvasViewportStub::render_into` and `NativeBackend::fill_rect` can
|
|
|
|
|
//! coexist inside a single `SharedSkiaContext::with_frame` closure, and
|
|
|
|
|
//! that the chrome pixels survive the stub's deliberate GL-state
|
|
|
|
|
//! pollution (acceptance #4 — chrome readback unaffected).
|
|
|
|
|
//!
|
|
|
|
|
//! Per-OS dispatch matches `gpu_smoke.rs` (Linux EGL pbuffer + macOS
|
|
|
|
|
//! invisible window + Windows ignored).
|
|
|
|
|
|
|
|
|
|
#![cfg_attr(target_os = "windows", allow(dead_code))]
|
|
|
|
|
|
|
|
|
|
mod common;
|
|
|
|
|
|
2026-05-16 15:49:58 +00:00
|
|
|
use op_editor_ui::{Color, Point2D, Rect};
|
|
|
|
|
use op_host_native::{CanvasViewportStub, 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
|
|
|
|
|
|
|
|
/// Inside the `with_frame` closure: stub paints first (and pollutes
|
|
|
|
|
/// stencil + blend), chrome paints on top. If `SharedSkiaContext`
|
|
|
|
|
/// correctly mediates the GL state, the chrome pixel must read back
|
|
|
|
|
/// red regardless of what the stub left behind.
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
fn paint_chrome_and_stub(ctx: &mut SharedSkiaContext, backend: &mut NativeBackend) {
|
|
|
|
|
let chrome_rect = Rect {
|
|
|
|
|
origin: Point2D::new(50.0, 50.0),
|
|
|
|
|
size: Point2D::new(100.0, 100.0),
|
|
|
|
|
};
|
|
|
|
|
let stub_rect = skia_safe::Rect::from_xywh(50.0, 50.0, 400.0, 300.0);
|
|
|
|
|
ctx.begin_frame();
|
|
|
|
|
ctx.with_frame(|canvas, glow| {
|
|
|
|
|
// Stub first — paints a blue rect inside the stub region and
|
|
|
|
|
// pollutes GL state without restoring.
|
|
|
|
|
CanvasViewportStub::render_into(glow, canvas, stub_rect);
|
|
|
|
|
// Chrome second — must override the stub at (50,50)-(150,150).
|
|
|
|
|
backend.fill_rect(canvas, chrome_rect, Color::RED);
|
|
|
|
|
});
|
|
|
|
|
ctx.present();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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): see `gpu_smoke.rs` for
|
|
|
|
|
// the full rationale. STEP1A_REQUIRE_GPU=1 ⇒ setup-failure is
|
|
|
|
|
// a hard panic; otherwise we surface an INCONCLUSIVE marker
|
|
|
|
|
// so acceptance #2 / #4 don't pass silently on hostless CI.
|
|
|
|
|
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
|
|
|
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_chrome_stub_composition: STEP1A_REQUIRE_GPU=1 but {msg}");
|
|
|
|
|
}
|
|
|
|
|
eprintln!(
|
|
|
|
|
"⚠ INCONCLUSIVE gpu_chrome_stub_composition: {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)`;
|
|
|
|
|
// size queried from EGL pbuffer GL viewport.
|
|
|
|
|
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_chrome_stub_composition: STEP1A_REQUIRE_GPU=1 but {msg}");
|
|
|
|
|
}
|
|
|
|
|
eprintln!(
|
|
|
|
|
"⚠ INCONCLUSIVE gpu_chrome_stub_composition: {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_and_stub(&mut ctx, &mut backend);
|
|
|
|
|
|
|
|
|
|
// Chrome pixel must be red even though the stub poked
|
|
|
|
|
// STENCIL_TEST + BlendFunc(ONE, ZERO).
|
2026-05-05 13:15:00 +00:00
|
|
|
// Phase A Gate round 2 BLOCK 2(a) fix — `glow()` borrows;
|
|
|
|
|
// clone the `Arc` so the handle outlives the later `teardown`.
|
|
|
|
|
let glow = ctx.glow().expect("glow handle").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 chrome_px = [0u8; 4];
|
|
|
|
|
let mut stub_px = [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 chrome_px)),
|
|
|
|
|
);
|
|
|
|
|
// Outside the chrome rect but inside the stub rect — should
|
|
|
|
|
// be the stub's blue paint.
|
|
|
|
|
glow.read_pixels(
|
|
|
|
|
300,
|
|
|
|
|
200,
|
|
|
|
|
1,
|
|
|
|
|
1,
|
|
|
|
|
glow::RGBA,
|
|
|
|
|
glow::UNSIGNED_BYTE,
|
|
|
|
|
glow::PixelPackData::Slice(Some(&mut stub_px)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let _ = ctx.teardown();
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
chrome_px,
|
|
|
|
|
[255, 0, 0, 255],
|
|
|
|
|
"chrome pixel polluted by stub's GL state — got {:?}",
|
|
|
|
|
chrome_px
|
|
|
|
|
);
|
|
|
|
|
// Stub painted blue; check the blue channel dominates.
|
|
|
|
|
assert!(
|
|
|
|
|
stub_px[2] > 200 && stub_px[0] < 60,
|
|
|
|
|
"stub region missing blue pixels — got {:?}",
|
|
|
|
|
stub_px
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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,
|
|
|
|
|
chrome_red: bool,
|
|
|
|
|
stub_blue: bool,
|
|
|
|
|
inconclusive: Option<String>,
|
|
|
|
|
window: Option<Window>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct Composition {
|
|
|
|
|
state: Rc<RefCell<State>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ApplicationHandler for Composition {
|
|
|
|
|
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(800u32, 600u32));
|
|
|
|
|
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_and_stub(&mut ctx, &mut backend);
|
2026-05-05 13:15:00 +00:00
|
|
|
// Phase A Gate round 2 BLOCK 2(a) fix — `glow()` borrows;
|
|
|
|
|
// clone the `Arc` so the handle outlives `teardown`.
|
|
|
|
|
let glow = ctx.glow().expect("glow handle").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 chrome_px = [0u8; 4];
|
|
|
|
|
let mut stub_px = [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 chrome_px)),
|
|
|
|
|
);
|
|
|
|
|
glow.read_pixels(
|
|
|
|
|
300,
|
|
|
|
|
200,
|
|
|
|
|
1,
|
|
|
|
|
1,
|
|
|
|
|
glow::RGBA,
|
|
|
|
|
glow::UNSIGNED_BYTE,
|
|
|
|
|
glow::PixelPackData::Slice(Some(&mut stub_px)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
state.chrome_red = chrome_px[0] > 200 && chrome_px[1] < 60 && chrome_px[2] < 60;
|
|
|
|
|
state.stub_blue = stub_px[2] > 200 && stub_px[0] < 60;
|
|
|
|
|
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) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn run() {
|
|
|
|
|
// Same `catch_unwind` pattern as `gpu_smoke.rs::macos`. On
|
|
|
|
|
// worker-thread test runs winit panics with "EventLoop must
|
|
|
|
|
// be created on the main thread" — degrade to inconclusive.
|
|
|
|
|
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 comp = Composition {
|
|
|
|
|
state: state.clone(),
|
|
|
|
|
};
|
|
|
|
|
let _ = event_loop.run_app(&mut comp);
|
|
|
|
|
let s = state.borrow();
|
|
|
|
|
if let Some(reason) = &s.inconclusive {
|
|
|
|
|
return Err(reason.clone());
|
|
|
|
|
}
|
|
|
|
|
Ok((s.chrome_red, s.stub_blue))
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
match result {
|
|
|
|
|
Ok(Ok((true, true))) => {}
|
|
|
|
|
Ok(Ok((chrome_red, stub_blue))) => {
|
|
|
|
|
panic!("gpu_chrome_stub_composition: chrome_red={chrome_red} stub_blue={stub_blue}")
|
|
|
|
|
}
|
|
|
|
|
Ok(Err(reason)) => {
|
|
|
|
|
eprintln!("gpu_chrome_stub_composition: 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_chrome_stub_composition: inconclusive — winit panicked (likely \
|
|
|
|
|
`EventLoop` not on the main thread): {msg}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
mod platform {
|
|
|
|
|
pub fn run() {}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 14:33:00 +00:00
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
|
#[test]
|
|
|
|
|
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.
|
|
|
|
|
#[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]
|
2026-05-05 14:33:00 +00:00
|
|
|
#[ignore = "LINUX_GPU_SKIA_LOADER_TBD: skia-safe Interface::new_native cannot resolve GL syms from EGL pbuffer + llvmpipe (see gpu_smoke.rs)"]
|
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_chrome_stub_composition() {
|
|
|
|
|
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_chrome_stub_composition() {
|
|
|
|
|
platform::run();
|
|
|
|
|
}
|