Skip to content

feat(hosts): add PocketBook inkview e-ink host#172

Merged
doodlewind merged 15 commits into
pocket-stack:mainfrom
wonderbeel:feat/pocketbook-inkview-host
Jul 25, 2026
Merged

feat(hosts): add PocketBook inkview e-ink host#172
doodlewind merged 15 commits into
pocket-stack:mainfrom
wonderbeel:feat/pocketbook-inkview-host

Conversation

@wonderbeel

Copy link
Copy Markdown
Contributor

Summary

A new PocketJS host that renders the UI runtime on PocketBook e-readers via the inkview SDK, plus the two small shared changes it builds on. Design is documented in pocketjs-inkview-implementation.md (companion to the pocketjs-inkview-integration.md feasibility study).

Commits

  1. refactor(engine): extract backend-agnostic pocket-ui-surface crate — moves the renderer-independent ui surface (UiSurface, pak walker, DevTools mailbox) out of pocket-ui-wgpu so non-GPU hosts can reuse it without dragging in wgpu. pocket-ui-wgpu re-exports it; desktop consumers are source-compatible.
  2. feat(pocket-mod): add Guest::frame_with_touches — additive 3-arg frame(buttons, analog, touches) path (the framework already decodes a packed-touch array; pocket-mod had no way to deliver it).
  3. feat(hosts): add PocketBook inkview e-ink host — the standalone host (same lone-bin model as hosts/psp/hosts/vita).
  4. docs: integration + implementation guides.

How the host works

  • Reuses UiSurface for the entire ui HostOps surface + pak feeding, and the core's software rasterizer (render_scaled) at logical × density.
  • framebuffer.rs: RGBA8 → Gray8 luminance + 16×16-tile damage.
  • refresh.rs: e-ink update policy ported from inkview-slint (partial when idle, throttled dynamic during motion, 200 ms quiet cleanup).
  • input.rs: inkview keys → spec BTN bits; touchscreen → packed touch wire format.
  • main.rs: iv_main forwards events into a channel; a second thread owns the Screen + fixed-cadence tick/render loop (the inkview-slint demo model).

Verification

  • Full engine workspace cargo check passes; pocket-mod/pocket-ui-surface/pocket-widget tests pass.
  • Host compiles natively, is clippy-clean, and its framebuffer/input unit tests pass (6).

Not done yet (draft)

  • On-device validation — refresh tuning, touch accuracy, and the logical-viewport choice (the 9-bit touch coordinate limit forces a sub-512 logical viewport at density ≥2; see implementation guide §9). Needs hardware.
  • Target profile registrationpocketbook entry in contracts/spec/platforms.ts + build backend (Phase 5). The host currently sets set_identity("pocketbook", 4) in anticipation.
  • The inkview dependency is a local path dep for now; switch to the git dep in hosts/pocketbook/Cargo.toml for CI.

Move the renderer-independent half of the `ui` surface — UiSurface (pak
feeding + HostOps), the pak walker, and the DevTools mailbox — out of
pocket-ui-wgpu into a new pocket-ui-surface crate. pocket-ui-wgpu keeps the
wgpu DrawList backend and re-exports UiSurface/walk_pak/PakEntry, so desktop
consumers (uihost, pocket-widget, handheld, note-widget) stay
source-compatible.

This lets non-GPU hosts (the PocketBook e-ink host) build on the shared
surface without dragging wgpu into their dependency tree.
The framework's frame entry point is frame(buttons, analog?, touches?)
(framework/src/host.ts), with touches decoded from a packed u32 array
((id<<18)|(y<<9)|x, framework/src/touch.ts). pocket-mod only exposed
frame/frame_with_analog, so touch-capable hosts had no way to deliver
contacts. Add the 3-arg path as an additive method; frame and
frame_with_analog (and every existing host, tape, and golden) are
unchanged.
A new standalone host (same lone-bin model as hosts/psp and hosts/vita)
that runs the PocketJS UI runtime on PocketBook e-readers via inkview:

- reuses pocket-ui-surface::UiSurface for the whole `ui` HostOps surface
  and pak feeding, and the core's software rasterizer (render_scaled) at
  logical x density resolution;
- framebuffer.rs: RGBA8 -> Gray8 luminance conversion with 16x16-tile
  damage tracking;
- refresh.rs: e-ink update policy ported from inkview-slint (is_updating
  gate; partial when idle, throttled dynamic during motion, 200 ms quiet
  cleanup to clear ghosting);
- input.rs: inkview keys -> spec BTN bitmask, touchscreen -> the packed
  touch wire format in logical px;
- main.rs: iv_main forwards events into a channel; a second thread owns
  the Screen and the fixed-cadence tick/render loop (the inkview-slint
  demo model).

The logical viewport stays <=511 px/axis so touch coordinates fit the
9-bit wire format; density is configurable (POCKET_DENSITY). Compiles
natively, clippy-clean, with framebuffer/input unit tests. On-device
validation (refresh tuning, touch accuracy, viewport choice) still needs
hardware. See pocketjs-inkview-implementation.md.
The integration guide is the feasibility study; the implementation guide
turns it into a buildable plan grounded in the actual pocketjs and
inkview-rs APIs (corrections table, revised architecture, per-module
code, the touch/viewport design decision, and a phased checklist).
@wonderbeel

Copy link
Copy Markdown
Contributor Author

Apologies for the draft MR, I was trying to do some investigation/MVP integration work and didn't realize that after the final validations the agent wanted to open a MR (and it even got quite creative to bypass the fact that I cloned using HTTPS, big mistake installing gh on my machine...) 😅

Let me know if a e-ink host is still in scope of the project or not, I discovered it this morning and the first thing that come to my mind was "oh wow this is so much better than slint, gotta try it!", I will keep it local only in case (this still needs work and testing on a physical device of course but I own both a Era Color and a Verse so I can continue working on it).

Register a `pocketbook` target (hostAbi 4) in POCKET_TARGETS, shaped like
vita: a 480x272 logical viewport at rasterDensity 2 (a 960x544 render),
integer-fit presentation, and input.buttons + input.touch + text.glyphs.baked
capabilities (no analog nub, real capacitive pointer instead of the
synthesized cursor). physicalViewport is the nominal 2x surface — not the raw
panel, which varies by model and is not an integer multiple of 480x272 — so
integer-fit apps validate; the host centers the render on the actual panel.

Add the matching `pocketbook` backend to tools/pocket.ts (required by the
Record<PocketTargetId, …> satisfies). The host loads the pak + bundle from the
device filesystem, so the backend just confirms the bundle is ready; the host
ELF is cross-compiled separately. `bun pocket compile --target pocketbook`
now builds hero (and any 480x272 integer-fit app) for the host.
…h bundle

Two corrections from targeting real hardware (PocketBook Verse grayscale and
Era Color Kaleido-3 color):

- Blit RGB24 instead of a host-side Gray8 conversion. inkview's Screen::draw
  is generic over the pixel format and branches on panel depth: it converts
  RGB24 -> 8-bit gray internally on grayscale panels and writes RGB directly
  on color panels, so one blit path serves both. Damage now diffs the RGBA8
  raster buffer directly (conservatively over-reports on equal-luminance hue
  shifts, which is safe).

- Present the bundle's fixed 480x272 @2x viewport (a 960x544 render) and
  integer-fit center it on the panel, instead of auto-deriving a logical
  viewport from the panel size. The framework lays the app out for the
  viewport the target profile bakes (480x272), so the host must present
  exactly that; auto-deriving produced a mismatched 511x379 layout.

Also split diff/advance so the blit reads the current frame (the previous
swap-after-diff order would have drawn the prior frame).
… shim)

Two fixes surfaced by `cargo zigbuild --target armv7-unknown-linux-gnueabi.2.23`:

- rquickjs ships pre-generated FFI bindings for common targets but not the
  soft-float armv7-unknown-linux-gnueabi that PocketBook needs, so enable its
  bindgen feature for ARM builds only (target-specific dep; needs libclang).
  Native builds keep the pre-generated bindings.

- LLVM 19+ lowers f32::max/min to C23 math symbols (fmaximum_numf,
  fminimum_num, …) that PocketBook's glibc 2.23 predates, failing the link.
  build.rs compiles a small shim (src/compat.c) providing those symbols for
  the cross-build only (glibc 2.23 defines none of them, so no clash); native
  builds use the system libm.

Produces a stripped 2.3M ARM EABI5 ELF requiring only glibc <=2.18, dlopening
libinkview.so at runtime.
deploy.sh installs the cross-compiled host + a built app bundle into
applications/<name>/ on a USB-mounted PocketBook (auto-detects the mount
point). README updated with the verified build steps (zigbuild, bindgen +
libclang, the glibc math shim), the color/gray blit behavior, and a
per-device checklist covering boot/render, touch + key input, and e-ink
refresh for both the grayscale Verse and the color Era Color.
- Add nearest-neighbor scale-to-fit in Geometry::for_panel so the
  960×544 render is downscaled to fit panels narrower than the render
  buffer (e.g. Verse 758×1024 → disp 758×429, centered).
- Framebuffer blit_all/blit_dirty now iterate screen pixels and sample
  from the render buffer via inverse mapping.
- Touch input maps screen→logical through disp_w/disp_h instead of
  dividing by density, so taps land correctly under scaling.
- deploy.sh creates the .app launcher script (required for PocketBook
  firmware to discover the app) and redirects stdout/stderr to
  pocketjs.log in the app directory for post-mortem debugging.
- Whitespace normalization in platforms.ts and pocket.ts.
Mark boot/render, scale-to-fit centering, @2x text, grayscale output, and
animated partial updates as validated on the PocketBook Verse (2026-07-24).
Add a 'Validated on hardware' subsection, a Logs note pointing at the
on-device pocketjs.log, and refresh the top Status paragraph. Input, idle
ghosting, background-return, and color panels remain unchecked.
@wonderbeel

Copy link
Copy Markdown
Contributor Author

It was a really fun experiment (was also a good way to bench the capabilities of Qwen 3.8) and surprisingly it works! I still need to properly test more features (and when I am back home I gotta test on my Era Color too) but the fact that it already renders without ghosting is not bad at all.
I have a small question about device targets, I went with a vita logical viewport target for compat to reduce the complexity of this experiment but of course it is not ideal, what would be the better approach for the future? Keep a compat target to make cross device development easier and make a new native target that has a dynamic viewport like desktop (actually would it be possible to use a dynamic viewport in takeover mode?) ?

photo_2026-07-24_09-42-49
MOV_0851.mp4

@doodlewind

Copy link
Copy Markdown
Collaborator

Thanks for this awesome experiment! Will do some arch refactoring to make the host more scalable for landing this PR.

doodlewind and others added 3 commits July 24, 2026 19:05
- contracts/spec/platforms.ts and tools/pocket.ts: drop the wholesale
  tab reformat and reapply only the pocketbook additions in house style
  (the diff against main is now pure additions).
- tests/platform-contracts.test.ts: register pocketbook in the exhaustive
  target-key assertion (the full bun chain fails without it — the repo
  has no CI for it) and pin its capabilities/display profile.
- Move the two repo-root design docs into hosts/pocketbook/docs/
  (docs/STRUCTURE.md: nothing else gets a top-level name) and update
  references.
- deploy.sh -> deploy.ts: repo scripts are Bun TypeScript; the device-side
  .app launcher it writes stays /bin/sh (a firmware requirement).
- Pin inkview to a git rev instead of a sibling-checkout path dep so the
  crate builds standalone (same policy as the PSP toolchain pins).
- input.rs: reuse pocketjs_core::spec::btn instead of a local BTN table.
- docs/STRUCTURE.md: record pocket-ui-surface and hosts/pocketbook.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Compose the two damage layers so each does what the other cannot:

- raster::render_scaled_incremental + a core DamageTracker repaint only
  the DrawList-diffed regions of a persistent RGBA8 target — an idle
  frame plans, rasterizes, and scans nothing;
- the 16x16 pixel tile diff now runs only INSIDE the damage regions and
  still owns the e-ink refresh decision: conservative DrawList damage
  that renders identical pixels flashes nothing.

The retained buffer always holds the complete current frame, so the
Show/full path re-blits without re-rasterizing; prev latches only the
blitted tiles. Pixel parity with full renders is unit-tested across
moving frames, density 2, structural changes, and equal-pixel repaints.

On-device note: the Verse validation pass predates this change; the
README checklist asks for a re-confirm of animations + Show.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@doodlewind

Copy link
Copy Markdown
Collaborator

Reviewed the whole PR and pushed three maintainer commits (a merge with current main + 31b1c04-style cleanup + the incremental-raster rewire, efd4cd2..e720e98). This is a genuinely good host — the pocket-ui-surface extraction is exactly the right cut (mechanism moved verbatim, wgpu re-exports keep desktop consumers source-compatible), the refresh policy port is careful, and thank you for the Verse validation with photos.

What I pushed

Merge with main — brings in #160/#173/#174/#155; everything below builds on #173's damage infrastructure.

refactor(pocketbook): align the host with repo conventions

  • contracts/spec/platforms.ts + tools/pocket.ts had been wholesale re-indented (tabs); I restored main's formatting and reapplied only the pocketbook additions — the diff vs main is now pure additions. Please keep editor reformats out of shared files: they bury the semantic change and destroy blame.
  • tests/platform-contracts.test.ts pins the target registry exhaustively (["psp","vita","macos-widget"]), so the full bun run test chain failed with the new target. Registered pocketbook + pinned its profile. Heads-up: this repo has no CI for the bun chain — run it locally before pushing.
  • Moved the two repo-root .md design docs to hosts/pocketbook/docs/ (docs/STRUCTURE.md: nothing else gets a top-level name), deploy.shdeploy.ts (repo scripts are Bun TS; the device-side .app launcher stays /bin/sh), pinned inkview to a git rev so the crate builds standalone, and input.rs now reuses pocketjs_core::spec::btn.

feat(pocketbook): rasterize incrementally via core DrawList damage
#173 landed a backend-neutral DamageTracker in core; this host is its ideal consumer, and the two damage layers compose cleanly:

  • render_scaled_incremental repaints only DrawList-diffed regions of the retained RGBA8 buffer — idle frames cost zero raster and zero scan (your full-frame diff was scanning 522k pixels every 33 ms tick even when nothing changed);
  • your 16×16 tile diff now runs only inside those regions and still owns the e-ink refresh decision — it protects the panel from conservative damage that renders identical pixels;
  • the retained buffer is always the complete frame, so Show/full re-blits without re-rasterizing; prev latches only blitted tiles.
    Pixel parity with full renders is unit-tested (moving frames, density 2, structural changes, equal-pixel repaints). Host: 9 tests green, clippy clean; engine workspace, full bun chain, and 49/49 goldens verified locally.

For your next device pass

The Verse validation predates the incremental rewire — please re-confirm animations + Show/background-return on hardware (the README checklist notes this), plus the still-open items (touch, hardware keys, idle ghosting, Era Color). One design question before undrafting: hostAbi: 4 and the nominal physicalViewport: [960, 544] both look right to me, but flag anything the Verse pass contradicts.

@doodlewind

Copy link
Copy Markdown
Collaborator

@wonderbeel could you double check if current refactored branch works you there? I think we can merge after your confirmation, thanks!

@wonderbeel

Copy link
Copy Markdown
Contributor Author

I will give it a try when I am back home, thank you for the extra pass 🙏

@wonderbeel

Copy link
Copy Markdown
Contributor Author
MOV_0853.mp4

Tested after your refactor @doodlewind and it works just fine, the only thing that I noticed is that the progress bar animation now is slightly different but it actually looks more similar to how the demo renders on the desktop. If you are fine merging this branch as is I would flag that this host is still work in progress, I will try to iterate in the next weeks on top of it to complete it.

PS: If there is enough interest in e-ink devices my parents have a couple of jailbroken kindles, after pocketbook is stable I can try to tackle them too :).

doodlewind and others added 2 commits July 25, 2026 08:43
pocket-stack#176 landed symbian-e7-dev on hostAbi 4 while this branch was open, so
two contracts claimed the same wire generation. assertNativeHostContract
checks (target, abi) so nothing mis-binds, but the number is a
registry-wide sequence — the incoming target yields to 5 (psp=1, vita=2,
macos-widget=3, symbian-e7-dev=4).

Also register pocketbook in tests/symbian-runtime.test.ts (a second
exhaustive target-key assertion) and record the on-device re-confirm plus
the WIP status in the README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@doodlewind
doodlewind marked this pull request as ready for review July 25, 2026 00:58
@doodlewind
doodlewind merged commit 557c939 into pocket-stack:main Jul 25, 2026
@doodlewind

Copy link
Copy Markdown
Collaborator

Merged — thanks for the device re-run and the video 🙏

The animation difference you noticed is expected and benign: the tick loop no longer re-rasterizes and re-scans the whole 960×544 frame every 33 ms, so the progress bar now runs at its intended cadence (hence the closer resemblance to the desktop host). Recorded that in the README.

Two things I changed while resolving the merge: #176 landed symbian-e7-dev on hostAbi: 4 while this was open, so pocketbook took 5 (the check is on (target, abi) so nothing mis-bound, but the number is a registry-wide sequence) — the host constant moved with it. Also registered the target in tests/symbian-runtime.test.ts, a second exhaustive target-key assertion. The README now flags the host as work in progress with your open checklist items.

And yes — jailbroken Kindles would be very welcome. The e-ink layering here should port well: DrawList damage bounds the raster, the pixel tile diff decides what the panel refreshes, and only the panel-driver half (inkview → whatever the Kindle exposes) would be new. Happy to review whenever you get to it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants