Commit graph

7 commits

Author SHA1 Message Date
Kayshen-X b066a1c057 feat(shell-native): Phase A Gate round 3 fixes
Apply 5 patches from Codex Phase A Gate round 2 review against spec
v19.1 (FROZEN at openpencil-docs commit 526791f):

- BLOCK 1: `SharedSkiaContext::new(provider) -> Result<Self>` single-arg
  per spec §3.3. Provider owns surface configuration; constructor queries
  GL viewport / sample count / stencil bits via glow after make_current
  returns (option C — no trait change, no caller-side `SurfaceConfig`).
  `dpi` field on `SurfaceConfig` was dead and is dropped.
- BLOCK 2(a): `glow()` returns `Option<&Arc<glow::Context>>` (borrow,
  not clone) per spec §3.3. Hot-path callers clone explicitly.
- BLOCK 2(b): mobile `on_pause` drops `glow_handle` alongside surface
  per spec §3.4 — backing GL context is invalid once activity backgrounds.
- CONCERN 1: `default_framebuffer_id` is now a required trait method
  (no default body); explicit overrides on `GlutinProvider` (0),
  `EglPbufferProvider` (0), `EaglProvider` (unimplemented! Step 1f),
  `AndroidEglProvider` (0). Forces Step 1f mobile impls to specify the
  non-zero CAEAGLLayer-backed FBO rather than silently inheriting 0.
- CONCERN 2: new `tests/resize_smoke.rs` with two raster-backed tests —
  grow 400×300→800×600→400×300 paints through `NativeBackend` without
  panic; resize span emits on grow / shrink / 0×0 clamp paths.
- NIT: stale "Spec mini-patch pending" comments rewritten to reflect
  v19.1 frozen state.

cargo build / test / clippy / fmt all green on macOS local.
2026-05-05 21:15:00 +08:00
Kayshen-X d26b53c06d 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 21:09:00 +08:00
Kayshen-X c1ce879582 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 21:06:00 +08:00
Kayshen-X 22003f9b0c chore(shell-native): add transient P0 probe gate (Step 1a)
Drives the three-OS CI matrix verification of the skia-safe + glutin +
glow + winit dep stack per Step 1a spec §7.

- examples/p0_probe.rs: stencil_visibility + readback chain runner (must
  own a real OS main thread because winit on macOS rejects
  EventLoop::new() from cargo test worker threads).
- tests/p0_probe.rs: subprocess-invoke wrapper, gated
  #[ignore = "P0_PROBE_GATE"] so default cargo test stays untouched.
- Cargo.toml: add transient [target.'cfg(not(target_arch = "wasm32"))'.
  dev-dependencies] block (skia-safe 0.97 + glutin 0.32.3 + glutin-winit
  0.5.0 + glow 0.17.0 + raw-window-handle 0.6.2 + scopeguard 1.2.0 +
  winit defaults). Pinned to versions resolved in /tmp/skia-glow-probe.
- .github/workflows/rust-check.yml: install Linux GL prereqs (xvfb,
  mesa, libxkbcommon, libwayland) and add a P0-probe-gate step running
  cargo test --ignored on each OS (Linux through xvfb-run; Windows
  early-returns per spec §8.2 WINDOWS_GPU_DEFERRED_NO_RUNNER).

All three artefacts are TRANSIENT — reverted in a follow-up cleanup
commit after CI is green and the loader-compat notes commit lands.
Task 1 owns the permanent integration.
2026-05-05 12:23:49 +08:00
Kayshen-X 006646057d feat(shell-native): Phase A Gate round 3 fixes
Apply 5 patches from Codex Phase A Gate round 2 review against spec
v19.1 (FROZEN at openpencil-docs commit 526791f):

- BLOCK 1: `SharedSkiaContext::new(provider) -> Result<Self>` single-arg
  per spec §3.3. Provider owns surface configuration; constructor queries
  GL viewport / sample count / stencil bits via glow after make_current
  returns (option C — no trait change, no caller-side `SurfaceConfig`).
  `dpi` field on `SurfaceConfig` was dead and is dropped.
- BLOCK 2(a): `glow()` returns `Option<&Arc<glow::Context>>` (borrow,
  not clone) per spec §3.3. Hot-path callers clone explicitly.
- BLOCK 2(b): mobile `on_pause` drops `glow_handle` alongside surface
  per spec §3.4 — backing GL context is invalid once activity backgrounds.
- CONCERN 1: `default_framebuffer_id` is now a required trait method
  (no default body); explicit overrides on `GlutinProvider` (0),
  `EglPbufferProvider` (0), `EaglProvider` (unimplemented! Step 1f),
  `AndroidEglProvider` (0). Forces Step 1f mobile impls to specify the
  non-zero CAEAGLLayer-backed FBO rather than silently inheriting 0.
- CONCERN 2: new `tests/resize_smoke.rs` with two raster-backed tests —
  grow 400×300→800×600→400×300 paints through `NativeBackend` without
  panic; resize span emits on grow / shrink / 0×0 clamp paths.
- NIT: stale "Spec mini-patch pending" comments rewritten to reflect
  v19.1 frozen state.

cargo build / test / clippy / fmt all green on macOS local.
2026-05-05 12:23:15 +08:00
Kayshen-X 3b4b7a6f36 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 12:23:13 +08:00
Kayshen-X ad079e7662 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 12:23:12 +08:00