Implements punktfunk-planning/design/android-skia-console-port.md (WP1–WP7 + hosting), reversing the 2026-08-10 "port rejected" verdict — its key premise (no Android Skia prebuilts) was stale at skia-safe 0.99, and the refactor constraint was lifted. Five commits, net −152 LOC while adding the entire native host: the third gamepad-UI implementation is gone.
What this is
The pf-console-ui shell the Vulkan session binary shows on Windows/Linux now fronts Android's gamepad/console mode too, drawn by the native crate through Skia's GL backend on an EGL surface over the SurfaceView. Kotlin keeps every service (trust store, discovery, mTLS library fetch + cache, pairing, WoL, settings) and feeds the console's existing model seam (ConsoleShared / LibraryShared / ConsoleBus) over JNI — the desktop's 1,100-line service was bridged, not ported.
By commit
84db02ef — the shell becomes portable (pf-client-core, pf-console-ui, native host). Portable modules split out (menu_nav — with the stick hysteresis Apple and Android each had to add on glass, now one implementation with tests; audio_format, decoder_pref, console), all re-exported under their old paths. pf-console-ui gains the vulkan-overlay default feature (the session binary is unchanged), a Key enum for SDL-free input, a SettingsStore seam, Viewport{insets,scale}, Platform-filtered settings rows, and the portable Console driver both hosts hold. skia-safe features are target-specific, so the desktop's prebuilt key — and the flatpak's pinned archive — cannot drift.
6d1c7187 — Android hosts it. The Kotlin bridge (SkiaConsole / SkiaConsoleShell / ConsoleJson), raw pad samples through the shared MenuNav, touch/keyboard/remote input, deep links, session edges; the Android-only settings rows ride Settings::extra under android.* keys so the desktop struct grows no one-platform fields; Controllers/Licences stay Compose, opened via the new ConsoleCmd::OpenPlatformScreen. Also a test-only fixed-step clock for the screenshot dump — the aurora's phase was wall-clock time, so the dump could never be a pixel gate under load; with it, this branch renders byte-identical PNGs to main on all 37 desktop scenes.
f3a69d5b — armeabi-v7a joins. rust-skia publishes no armv7 archive; building it through skia-bindings itself (FORCE_SKIA_BUILD=1) takes ~80 s on an M-series Mac. The console host compiles on all three shipping ABIs.
428fef2b — the Compose console is deleted (~6.5 kLOC + its tests + its Roborazzi store scenes). Kept: the Gamepad chrome files (the D7 platform screens and the touch UI's pad nav draw with them), the touch library, GamepadUi.kt. debug.punktfunk.console_backend=none forces the touch UI for on-glass triage.
ba227057 — the archives are hosted where git assets live: public release unom/skia-binaries @ 0.99.0 (R2-backed), mirroring rust-skia's release layout so the {tag}/{key} template fits verbatim. kit/build.gradle.kts bakes the URL in as the default — zero config for dev and CI, GitHub out of the Android build path; property/env/CI-var remain as staging overrides. Per skia-safe bump: rebuild the armv7 archive (recipe beside the sha256 table), cut a new release, mirror the 64-bit assets.
Desktop non-regression (the plan's §5 contract)
clients/session keeps default features; the Vulkan overlay, SDL path and in-stream chrome are the same code reached through the Console façade.
The resolved desktop skia key stays jpegd-jpege-pdf-textlayout-vulkan (flatpak pin untouched).
Tests only grew: pf-console-ui 180 → 181, pf-client-core 221.
Pixel-identical: with the fixed-clock dump, main vs this branch = AE 0 / identical md5 on all 37 scenes.
Additive desktop changes only: stick hysteresis in MenuNav (pinned by test), the settings rows' platform filter (desktop shows exactly the set it did — pinned by test), serde derives on the model types.
Verified
Container gate (fmt, clippy -D warnings --all-targets, plain build, tests) green; cargo ndk clippy -D warnings green on arm64-v8a / armeabi-v7a / x86_64; every ABI's skia-bindings log shows DOWNLOAD AND INSTALL SUCCEEDED (the silent-source-build trap).
:app:testDebugUnitTest + :kit:testDebugUnitTest green; assembleDebug with wiped skia-bindings dirs re-fetches all three ABIs from the baked default.
On glass (NP3, user + post-deletion re-check): the console comes up on EGL 1.5 / GLES 3, renders the desktop-identical home (carousel, OS marks, presence pips, aurora, legend), input and navigation work as expected.
Known costs / follow-ups (named in the plan)
TalkBack does not reach the console screens (SurfaceView is opaque to a11y); the touch UI remains fully accessible. Mitigation sketched in the plan (an AccessibilityNodeProvider over the shell's published hit boxes).
The Compose console's marketing shot scenes are gone — console store shots come from the desktop screenshot dump (same pixels by construction) or device capture.
Console TV/32-bit box pass and a pad-heavy session are still worth an on-glass round; a Deck --browse + one Windows session before merge is the desktop's final spot check.
The phone's design-unit scale floor (density × 0.6 under the couch formula) is a tuning knob for glass.
Implements `punktfunk-planning/design/android-skia-console-port.md` (WP1–WP7 + hosting), reversing the 2026-08-10 "port rejected" verdict — its key premise (no Android Skia prebuilts) was stale at skia-safe 0.99, and the refactor constraint was lifted. Five commits, **net −152 LOC** while adding the entire native host: the third gamepad-UI implementation is gone.
## What this is
The `pf-console-ui` shell the Vulkan session binary shows on Windows/Linux now fronts Android's gamepad/console mode too, drawn by the native crate through **Skia's GL backend on an EGL surface** over the `SurfaceView`. Kotlin keeps every service (trust store, discovery, mTLS library fetch + cache, pairing, WoL, settings) and feeds the console's existing model seam (`ConsoleShared` / `LibraryShared` / `ConsoleBus`) over JNI — the desktop's 1,100-line service was bridged, not ported.
## By commit
- **`84db02ef` — the shell becomes portable (pf-client-core, pf-console-ui, native host).** Portable modules split out (`menu_nav` — with the stick hysteresis Apple and Android each had to add on glass, now one implementation with tests; `audio_format`, `decoder_pref`, `console`), all re-exported under their old paths. `pf-console-ui` gains the `vulkan-overlay` default feature (the session binary is unchanged), a `Key` enum for SDL-free input, a `SettingsStore` seam, `Viewport{insets,scale}`, `Platform`-filtered settings rows, and the portable `Console` driver both hosts hold. skia-safe features are target-specific, so the desktop's prebuilt key — and the flatpak's pinned archive — cannot drift.
- **`6d1c7187` — Android hosts it.** The Kotlin bridge (`SkiaConsole` / `SkiaConsoleShell` / `ConsoleJson`), raw pad samples through the shared `MenuNav`, touch/keyboard/remote input, deep links, session edges; the Android-only settings rows ride `Settings::extra` under `android.*` keys so the desktop struct grows no one-platform fields; Controllers/Licences stay Compose, opened via the new `ConsoleCmd::OpenPlatformScreen`. Also a **test-only fixed-step clock** for the screenshot dump — the aurora's phase was wall-clock time, so the dump could never be a pixel gate under load; with it, this branch renders **byte-identical PNGs to `main` on all 37 desktop scenes**.
- **`f3a69d5b` — armeabi-v7a joins.** rust-skia publishes no armv7 archive; building it through skia-bindings itself (`FORCE_SKIA_BUILD=1`) takes ~80 s on an M-series Mac. The console host compiles on all three shipping ABIs.
- **`428fef2b` — the Compose console is deleted** (~6.5 kLOC + its tests + its Roborazzi store scenes). Kept: the Gamepad chrome files (the D7 platform screens and the touch UI's pad nav draw with them), the touch library, `GamepadUi.kt`. `debug.punktfunk.console_backend=none` forces the touch UI for on-glass triage.
- **`ba227057` — the archives are hosted where git assets live**: public release **[`unom/skia-binaries` @ 0.99.0](https://git.unom.io/unom/skia-binaries/releases/tag/0.99.0)** (R2-backed), mirroring rust-skia's release layout so the `{tag}/{key}` template fits verbatim. `kit/build.gradle.kts` bakes the URL in as the default — zero config for dev and CI, GitHub out of the Android build path; property/env/CI-var remain as staging overrides. Per skia-safe bump: rebuild the armv7 archive (recipe beside the sha256 table), cut a new release, mirror the 64-bit assets.
## Desktop non-regression (the plan's §5 contract)
- `clients/session` keeps default features; the Vulkan overlay, SDL path and in-stream chrome are the same code reached through the `Console` façade.
- The resolved desktop skia key stays `jpegd-jpege-pdf-textlayout-vulkan` (flatpak pin untouched).
- Tests only grew: pf-console-ui 180 → **181**, pf-client-core **221**.
- **Pixel-identical**: with the fixed-clock dump, `main` vs this branch = AE 0 / identical md5 on all 37 scenes.
- Additive desktop changes only: stick hysteresis in `MenuNav` (pinned by test), the settings rows' platform filter (desktop shows exactly the set it did — pinned by test), serde derives on the model types.
## Verified
- Container gate (fmt, clippy `-D warnings --all-targets`, plain build, tests) green; `cargo ndk clippy -D warnings` green on arm64-v8a / armeabi-v7a / x86_64; every ABI's skia-bindings log shows `DOWNLOAD AND INSTALL SUCCEEDED` (the silent-source-build trap).
- `:app:testDebugUnitTest` + `:kit:testDebugUnitTest` green; `assembleDebug` with wiped skia-bindings dirs re-fetches all three ABIs from the baked default.
- **On glass (NP3, user + post-deletion re-check):** the console comes up on EGL 1.5 / GLES 3, renders the desktop-identical home (carousel, OS marks, presence pips, aurora, legend), input and navigation work as expected.
## Known costs / follow-ups (named in the plan)
- TalkBack does not reach the console screens (`SurfaceView` is opaque to a11y); the touch UI remains fully accessible. Mitigation sketched in the plan (an `AccessibilityNodeProvider` over the shell's published hit boxes).
- The Compose console's marketing shot scenes are gone — console store shots come from the desktop screenshot dump (same pixels by construction) or device capture.
- Console TV/32-bit box pass and a pad-heavy session are still worth an on-glass round; a Deck `--browse` + one Windows session before merge is the desktop's final spot check.
- The phone's design-unit scale floor (`density × 0.6` under the couch formula) is a tuning knob for glass.
The shell that pf-console-ui draws for the Vulkan session binary is the console UI
the Android client should have been showing all along (design/android-skia-console-port.md).
This is WP1–WP3 of that plan: nothing the desktop does changes, and the same crate now
compiles for aarch64/x86_64-linux-android against rust-skia's prebuilt GL archives.
pf-client-core: the data the shell speaks is un-gated for android — trust::Settings and
the known-hosts store, the profiles model, deep links, the library MODEL (the ureq
fetches stay desktop), and three modules split out so the platform-bound ones can stay
platform-bound: audio_format (the AUDIO_FORMAT_* table, re-exported by session),
decoder_pref (migrate_decoder_pref, re-exported by video), menu_nav (MenuEvent /
MenuNav / PadInfo, re-exported by gamepad) and console (OverlayAction, PointerInput,
SessionPhase, re-exported by pf-presenter — pf-console-ui sits above pf-presenter, so the
types could not live there). MenuNav gains the stick hysteresis both the Apple and the
Android console had to grow on glass: an engaged direction holds until its own axis
falls below MENU_RELEASE (0.3), so a diagonal flick is one move, not two.
pf-console-ui: the Vulkan overlay + SDL event path move behind the default
`vulkan-overlay` feature (clients/session is unchanged); a `Key` enum replaces SDL
scancodes; a `SettingsStore` seam replaces the six direct settings-file sites (desktop =
the file, exactly as before; SnapshotStore for hosts across a language boundary);
`Viewport{width,height,insets,scale}` replaces bare w×h (zero insets + the couch formula
on the desktop = byte-identical layout); `Platform` filters the settings rows (the
desktop shows every row it did); ConsoleOptions carries the store, the platform and the
GPU cache budget; the portable `Console` driver is what both hosts hold. skia-safe's
features are target-specific so the desktop key stays jpegd-jpege-pdf-textlayout-vulkan
(the flatpak pin) and Android resolves gl-jpegd-jpege-pdf-textlayout. The model types
derive serde: the wire IS the model.
clients/android/native: src/console/ — a hand-declared EGL binding, Skia's GL
DirectContext over FBO 0, one render thread paced by eglSwapBuffers that owns the
console and applies Kotlin's input from a command queue, and ~28 nativeConsole* JNI
seams (surface lifecycle, pad samples through the shared MenuNav, pointer, keys, text,
session phases, model pushers, a blocking event poll, the command-bus drain). Gated to
the 64-bit ABIs: rust-skia publishes no armv7 archive and skia-bindings would silently
build Skia from source; nativeConsoleAvailable() tells Kotlin which.
Gates: container fmt + clippy -D warnings (all targets) + build + tests green
(pf-console-ui 180, pf-client-core 221); cargo ndk clippy -D warnings for
pf-client-core / pf-console-ui / punktfunk-client-android on arm64-v8a, x86_64 and
armeabi-v7a; skia-bindings logged DOWNLOAD AND INSTALL SUCCEEDED for both 64-bit keys.
WP4 + WP5 of design/android-skia-console-port.md. The Android client now hosts the same
console shell the Linux/Windows session binary shows — drawn by native over EGL/GLES on a
SurfaceView (the WP3 host) — and Kotlin keeps every service behind it.
Kotlin (clients/android/app/.../console/):
- SkiaConsole — the process-lifetime holder (the console's stack survives a stream, exactly
as the desktop's does): host rows from KnownHostStore + discovery + the reachability sweep,
the library pipeline (cache first, wake + retry across the boot window, catalog, running
set, posters over mTLS as bytes), pairing via nativePair, the wake-and-wait loop, the
settings round-trip (Settings ↔ trust::Settings JSON; console-owned keys persisted whole),
OverlayAction::Launch → connectToHost with the host's profile / a pinned card's one-off /
the request-access budget, CancelConnect, Quit → task to back, CopyText → clipboard, and
punktfunk:// links (known-and-pinned dials; anything needing trust is a notice — a link
never establishes trust).
- SkiaConsoleShell — the composable: the SurfaceView + its lifecycle, safe-area insets
(systemBars ∪ displayCutout, in surface px), a density floor under the couch scale for
phones, MainActivity's pad probes feeding raw MenuSamples (buttons, stick, HAT) into the
shared MenuNav, remote D-pad keys as discrete events, hardware keys as Keys + typed text,
the system Back as B, touch as pointer input, haptic pulses, and the two Compose overlays
the console can open (Controllers, Licences).
- ConsoleJson — the wire, in the model types' own serde shapes.
- App.kt picks SkiaConsoleShell over GamepadShell when SkiaConsole.wanted(): the native host
exists (64-bit ABIs) and sysprop debug.punktfunk.console_backend != compose. The Compose
console stays until the on-glass matrix and the armv7 archive land (WP7).
- NativeBridge: the nativeConsole* externs. GameEntry.platform parsed + cached (Collections).
pf-console-ui (WP5): the Android-only settings rows over Settings::extra `android.*` keys
(low-latency decode, phone rumble/gyro, SC2 passthrough, DualSense capture, controller-UI
mode) and two platform-screen action rows (Controllers, Licences) that raise the new
ConsoleCmd::OpenPlatformScreen; row_on() keeps them off the desktop's list, which shows
exactly the rows it did (pinned by test). "Send logs" hides where nothing serves it.
pf-console-ui (test harness): a test-only fixed-step clock for the screenshot dump. The
dump's aurora phase was wall-clock time, so two runs agreed only at equal machine load and
speed — a pixel comparison of main against a candidate read as a regression that was pure
drift. With the fixed step the same tree renders the same pixels anywhere.
Gates: container fmt + clippy -D warnings + build + tests; cargo ndk clippy -D warnings on
arm64-v8a / x86_64 / armeabi-v7a; :app:compileDebugKotlin; the fixed-clock dump of this
tree vs main.
WP6 of design/android-skia-console-port.md. rust-skia publishes prebuilt Skia archives for
aarch64/x86_64/i686-linux-android but not armv7-linux-androideabi, and the 32-bit TV boxes
(Chromecast-with-Google-TV class) are console-UI-always devices — so the archive is built once
with skia-bindings itself (FORCE_SKIA_BUILD=1 for pf-console-ui's gl+textlayout feature set:
1666 ninja steps, ~80 s on an M-series Mac) and packed in the exact skia-binaries/ layout the
downloader unpacks. Key a25a0fdb7d90429aa2d1-armv7-linux-androideabi-gl-jpegd-jpege-pdf-textlayout,
sha256 4867856b… (table in kit/build.gradle.kts beside the two GitHub keys).
- clients/android/native: the console host and its deps are on every Android ABI now
(the arch gate is gone; nativeConsoleAvailable() answers true on all three).
- kit/build.gradle.kts: forwards SKIA_BINARIES_URL (-PskiaBinariesUrl or the env) to every
cargo-ndk run — the {tag}/{key} template that serves all three keys from one mirror.
- .gitea/workflows/android.yml: the repo variable SKIA_BINARIES_URL feeds it.
Proven: on a clean target dir with the url template pointing at a local mirror, every ABI's
skia-bindings log says DOWNLOAD AND INSTALL SUCCEEDED (armv7 included), the full debug APK
builds and installs, and cargo ndk clippy -D warnings is green on all three ABIs.
⚠ Until the three archives are hosted (storage.unom, then set the CI variable), an armv7 build
without the url falls back to a Skia source build — minutes with ninja present, a red leg
without it. Hosting is the one step left in this WP.
WP7 of design/android-skia-console-port.md, unlocked by WP6 (every ABI carries the native
host) and the on-glass pass. The third gamepad-UI implementation is gone: Android's console
is now the same pf-console-ui shell Windows and Linux show, and the Compose mirror that
re-implemented it screen by screen is deleted rather than parked.
Deleted (~6.5 kLOC): GamepadHome, GamepadSettingsScreen, GamepadAddHostScreen,
GamepadDialogs, HomeTiles, the console halves of LibraryScreen (coverflow, ConsoleLibrary),
ConnectOverlay (ConnectTakeover), ConnectScreen / ConnectPrompts / AdaptiveDialogs
(gamepadUi branches, console options dialog), App.kt's GamepadShell + GamepadScreen — and
their tests (HomeTilesTest, GamepadSettingsLayout/RowsTest, ConsoleSubScreenRoutes/RowsTest,
the buildSettingsRows pins in GamepadPaletteTest, the Compose-console shot scenes; the tab
vocabulary is pinned by the Rust twin against the same shared vectors file).
Kept, deliberately: GamepadChrome/Nav/Aurora/Ink/Palette — the Compose platform screens the
console still opens (Controllers, Licences per D7), MainActivity's touch-UI pad navigation,
and the settings palette preview all draw with them; LibraryPosition + the TOUCH library
(the touch shell's own pushed screen); GamepadUi.kt (the mode switch).
App.kt folds the native host's presence into `gamepadUi` itself: no host, no console —
a controller then drives the touch UI through Compose focus (nothing left to fall back to).
The sysprop becomes a triage switch (`debug.punktfunk.console_backend=none` forces the
touch UI on glass; the `compose` value is meaningless now).
Marked breaking for the store-screenshot surface: the Compose console's marketing scenes
(console home, console settings, coverflow, connect takeover) are gone — the Skia shell
renders over native GL and cannot compose under Roborazzi. Its shots come from the desktop
screenshot dump (the same pixels by construction) or a device capture.
Gates: :app:testDebugUnitTest + :kit:testDebugUnitTest green, installDebug on the NP3 and
the console renders on glass (carousel, OS marks, legend, aurora — the desktop look).
The WP6 hosting step lands where the project's git assets live: a public release on
git.unom.io (R2-backed) rather than the sccache bucket — unom/skia-binaries, tag 0.99.0,
mirroring rust-skia's own release layout so skia-bindings' {tag}/{key} url template fits
it verbatim:
https://git.unom.io/unom/skia-binaries/releases/download/{tag}/skia-binaries-{key}.tar.gz
The release carries the armv7-linux-androideabi archive we build (rust-skia publishes
none) and byte-for-byte mirrors of the aarch64/x86_64 assets, each with its sha256 and
provenance in the release notes.
kit/build.gradle.kts now bakes that template in as the DEFAULT for every cargo-ndk run —
dev boxes and CI need no configuration, GitHub is out of the Android build path entirely,
and `-PskiaBinariesUrl` / the SKIA_BINARIES_URL env (and CI's repo variable) remain as
overrides for staging the next skia-safe bump's archives before they are released.
Proven: unauthenticated downloads of all three archives byte-match the local sha256s;
a clean-target cargo-ndk armv7 check installs from the release (`DOWNLOAD AND INSTALL
SUCCEEDED`); assembleDebug with the skia-bindings build dirs wiped re-fetched all three
ABIs from the default and built green.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements
punktfunk-planning/design/android-skia-console-port.md(WP1–WP7 + hosting), reversing the 2026-08-10 "port rejected" verdict — its key premise (no Android Skia prebuilts) was stale at skia-safe 0.99, and the refactor constraint was lifted. Five commits, net −152 LOC while adding the entire native host: the third gamepad-UI implementation is gone.What this is
The
pf-console-uishell the Vulkan session binary shows on Windows/Linux now fronts Android's gamepad/console mode too, drawn by the native crate through Skia's GL backend on an EGL surface over theSurfaceView. Kotlin keeps every service (trust store, discovery, mTLS library fetch + cache, pairing, WoL, settings) and feeds the console's existing model seam (ConsoleShared/LibraryShared/ConsoleBus) over JNI — the desktop's 1,100-line service was bridged, not ported.By commit
84db02ef— the shell becomes portable (pf-client-core, pf-console-ui, native host). Portable modules split out (menu_nav— with the stick hysteresis Apple and Android each had to add on glass, now one implementation with tests;audio_format,decoder_pref,console), all re-exported under their old paths.pf-console-uigains thevulkan-overlaydefault feature (the session binary is unchanged), aKeyenum for SDL-free input, aSettingsStoreseam,Viewport{insets,scale},Platform-filtered settings rows, and the portableConsoledriver both hosts hold. skia-safe features are target-specific, so the desktop's prebuilt key — and the flatpak's pinned archive — cannot drift.6d1c7187— Android hosts it. The Kotlin bridge (SkiaConsole/SkiaConsoleShell/ConsoleJson), raw pad samples through the sharedMenuNav, touch/keyboard/remote input, deep links, session edges; the Android-only settings rows rideSettings::extraunderandroid.*keys so the desktop struct grows no one-platform fields; Controllers/Licences stay Compose, opened via the newConsoleCmd::OpenPlatformScreen. Also a test-only fixed-step clock for the screenshot dump — the aurora's phase was wall-clock time, so the dump could never be a pixel gate under load; with it, this branch renders byte-identical PNGs tomainon all 37 desktop scenes.f3a69d5b— armeabi-v7a joins. rust-skia publishes no armv7 archive; building it through skia-bindings itself (FORCE_SKIA_BUILD=1) takes ~80 s on an M-series Mac. The console host compiles on all three shipping ABIs.428fef2b— the Compose console is deleted (~6.5 kLOC + its tests + its Roborazzi store scenes). Kept: the Gamepad chrome files (the D7 platform screens and the touch UI's pad nav draw with them), the touch library,GamepadUi.kt.debug.punktfunk.console_backend=noneforces the touch UI for on-glass triage.ba227057— the archives are hosted where git assets live: public releaseunom/skia-binaries@ 0.99.0 (R2-backed), mirroring rust-skia's release layout so the{tag}/{key}template fits verbatim.kit/build.gradle.ktsbakes the URL in as the default — zero config for dev and CI, GitHub out of the Android build path; property/env/CI-var remain as staging overrides. Per skia-safe bump: rebuild the armv7 archive (recipe beside the sha256 table), cut a new release, mirror the 64-bit assets.Desktop non-regression (the plan's §5 contract)
clients/sessionkeeps default features; the Vulkan overlay, SDL path and in-stream chrome are the same code reached through theConsolefaçade.jpegd-jpege-pdf-textlayout-vulkan(flatpak pin untouched).mainvs this branch = AE 0 / identical md5 on all 37 scenes.MenuNav(pinned by test), the settings rows' platform filter (desktop shows exactly the set it did — pinned by test), serde derives on the model types.Verified
-D warnings --all-targets, plain build, tests) green;cargo ndk clippy -D warningsgreen on arm64-v8a / armeabi-v7a / x86_64; every ABI's skia-bindings log showsDOWNLOAD AND INSTALL SUCCEEDED(the silent-source-build trap).:app:testDebugUnitTest+:kit:testDebugUnitTestgreen;assembleDebugwith wiped skia-bindings dirs re-fetches all three ABIs from the baked default.Known costs / follow-ups (named in the plan)
SurfaceViewis opaque to a11y); the touch UI remains fully accessible. Mitigation sketched in the plan (anAccessibilityNodeProviderover the shell's published hit boxes).--browse+ one Windows session before merge is the desktop's final spot check.density × 0.6under the couch formula) is a tuning knob for glass.The shell that pf-console-ui draws for the Vulkan session binary is the console UI the Android client should have been showing all along (design/android-skia-console-port.md). This is WP1–WP3 of that plan: nothing the desktop does changes, and the same crate now compiles for aarch64/x86_64-linux-android against rust-skia's prebuilt GL archives. pf-client-core: the data the shell speaks is un-gated for android — trust::Settings and the known-hosts store, the profiles model, deep links, the library MODEL (the ureq fetches stay desktop), and three modules split out so the platform-bound ones can stay platform-bound: audio_format (the AUDIO_FORMAT_* table, re-exported by session), decoder_pref (migrate_decoder_pref, re-exported by video), menu_nav (MenuEvent / MenuNav / PadInfo, re-exported by gamepad) and console (OverlayAction, PointerInput, SessionPhase, re-exported by pf-presenter — pf-console-ui sits above pf-presenter, so the types could not live there). MenuNav gains the stick hysteresis both the Apple and the Android console had to grow on glass: an engaged direction holds until its own axis falls below MENU_RELEASE (0.3), so a diagonal flick is one move, not two. pf-console-ui: the Vulkan overlay + SDL event path move behind the default `vulkan-overlay` feature (clients/session is unchanged); a `Key` enum replaces SDL scancodes; a `SettingsStore` seam replaces the six direct settings-file sites (desktop = the file, exactly as before; SnapshotStore for hosts across a language boundary); `Viewport{width,height,insets,scale}` replaces bare w×h (zero insets + the couch formula on the desktop = byte-identical layout); `Platform` filters the settings rows (the desktop shows every row it did); ConsoleOptions carries the store, the platform and the GPU cache budget; the portable `Console` driver is what both hosts hold. skia-safe's features are target-specific so the desktop key stays jpegd-jpege-pdf-textlayout-vulkan (the flatpak pin) and Android resolves gl-jpegd-jpege-pdf-textlayout. The model types derive serde: the wire IS the model. clients/android/native: src/console/ — a hand-declared EGL binding, Skia's GL DirectContext over FBO 0, one render thread paced by eglSwapBuffers that owns the console and applies Kotlin's input from a command queue, and ~28 nativeConsole* JNI seams (surface lifecycle, pad samples through the shared MenuNav, pointer, keys, text, session phases, model pushers, a blocking event poll, the command-bus drain). Gated to the 64-bit ABIs: rust-skia publishes no armv7 archive and skia-bindings would silently build Skia from source; nativeConsoleAvailable() tells Kotlin which. Gates: container fmt + clippy -D warnings (all targets) + build + tests green (pf-console-ui 180, pf-client-core 221); cargo ndk clippy -D warnings for pf-client-core / pf-console-ui / punktfunk-client-android on arm64-v8a, x86_64 and armeabi-v7a; skia-bindings logged DOWNLOAD AND INSTALL SUCCEEDED for both 64-bit keys.WP6 of design/android-skia-console-port.md. rust-skia publishes prebuilt Skia archives for aarch64/x86_64/i686-linux-android but not armv7-linux-androideabi, and the 32-bit TV boxes (Chromecast-with-Google-TV class) are console-UI-always devices — so the archive is built once with skia-bindings itself (FORCE_SKIA_BUILD=1 for pf-console-ui's gl+textlayout feature set: 1666 ninja steps, ~80 s on an M-series Mac) and packed in the exact skia-binaries/ layout the downloader unpacks. Key a25a0fdb7d90429aa2d1-armv7-linux-androideabi-gl-jpegd-jpege-pdf-textlayout, sha256 4867856b… (table in kit/build.gradle.kts beside the two GitHub keys). - clients/android/native: the console host and its deps are on every Android ABI now (the arch gate is gone; nativeConsoleAvailable() answers true on all three). - kit/build.gradle.kts: forwards SKIA_BINARIES_URL (-PskiaBinariesUrl or the env) to every cargo-ndk run — the {tag}/{key} template that serves all three keys from one mirror. - .gitea/workflows/android.yml: the repo variable SKIA_BINARIES_URL feeds it. Proven: on a clean target dir with the url template pointing at a local mirror, every ABI's skia-bindings log says DOWNLOAD AND INSTALL SUCCEEDED (armv7 included), the full debug APK builds and installs, and cargo ndk clippy -D warnings is green on all three ABIs. ⚠ Until the three archives are hosted (storage.unom, then set the CI variable), an armv7 build without the url falls back to a Skia source build — minutes with ninja present, a red leg without it. Hosting is the one step left in this WP.The WP6 hosting step lands where the project's git assets live: a public release on git.unom.io (R2-backed) rather than the sccache bucket — unom/skia-binaries, tag 0.99.0, mirroring rust-skia's own release layout so skia-bindings' {tag}/{key} url template fits it verbatim: https://git.unom.io/unom/skia-binaries/releases/download/{tag}/skia-binaries-{key}.tar.gz The release carries the armv7-linux-androideabi archive we build (rust-skia publishes none) and byte-for-byte mirrors of the aarch64/x86_64 assets, each with its sha256 and provenance in the release notes. kit/build.gradle.kts now bakes that template in as the DEFAULT for every cargo-ndk run — dev boxes and CI need no configuration, GitHub is out of the Android build path entirely, and `-PskiaBinariesUrl` / the SKIA_BINARIES_URL env (and CI's repo variable) remain as overrides for staging the next skia-safe bump's archives before they are released. Proven: unauthenticated downloads of all three archives byte-match the local sha256s; a clean-target cargo-ndk armv7 check installs from the release (`DOWNLOAD AND INSTALL SUCCEEDED`); assembleDebug with the skia-bindings build dirs wiped re-fetched all three ABIs from the default and built green.