Native release polish: a usable routing client on Android, a served APK, and 1.0.0 #45
No reviewers
Labels
No labels
agent
agent:ci
agent:done
agent:failed
agent:needs-input
agent:refined
agent:refining
agent:running
agent:skip
autonomous
driven
local
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jeroen/cartopolis!45
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/44-native-release-polish"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #44. Supersedes the narrower #34.
The headline
The APK has never once published, and the reason was hidden by a formatting error. CI ran
cargo fmt --checkas the first step of thecheckjob, so an unformatted merge ended the job in 30 seconds and the Android lane below it never ran. Behind it sat a real compile break: the APK is built--no-default-features, which dropssolo, which makesSessionMode::SoloandSessionStatus::Startingunconstructed — andunused = "deny"makes that an error.deploy-android.ymlfailed on every push andviberfox.garage44.eu/apk/has been a 404 throughout.Both fixed.
cargo check -p viberfox_android --target aarch64-linux-androidnow passes, verified against a real NDK — the first time that has ever been true in this repo.cargo fmt --checkmoved to the last step, on the rule that whatever gates last cannot hide anything.What else is in here
Android is a platform now. A JNI location provider behind the existing
LocationWatchseam (no Java, no dex —hasCode="false"is what lets this app ship without Gradle), keep-screen-on,ACTION_VIEWintents, a flush onWillSuspend(Android never sendsAppExit), worker quiesce while backgrounded, and a Back gesture that unwinds keyboard → driver mode → panel → exit.Text entry exists. NativeActivity has no IME, so every field could be focused and not typed into — which for a routing client is the address bar. The client draws its own keyboard.
The phone can render.
Msaa::Sample2was applied on Android where Vulkan's guaranteed minimum is1|4; the clipmap rasterised 224 tiles at 512² (~310 MB) where the globe had already concluded 256² (~78 MB); the worker cap was silently doing nothing on native.Bugs that made it feel unreliable. Collision was entirely off whenever the Height layer was on (the airborne test read a position with terrain lift folded in, and Dutch relief is ~2 m against a 0.2 m threshold — the avatar walked through every wall). One ☆ press permanently removed the HUD and compass.
while true do endat Luau top level hung the simulator forever and survived restart. Crafted tile bytes could request ~24 TB and abort the app.Almost no emoji render at all. A new test found that the bundled font stack cannot draw
▶ ◀ ✔ 🏠 📷 📋 📍 🚩 📝 ✏ 🔄 ☀ 🌙 ↗, so Back, Forward, Home, the camera picker and every bookmark row action have been painting empty boxes on every platform since they were written. The inventory comment asserted the opposite, twice.Navigation. A failed reroute used to end navigation silently; the route now survives, the line stays drawn, the marker snaps to the road, GPS quality is visible and gates rerouting, and there are turn chimes — the only part usable without looking at the screen. You can finally route to a search result, a bookmark or a POI.
Release. MIT OR Apache-2.0 across all six crates, 1.0.0, a monotonic
versionCode, a window background so cold start is not a black screen, an adaptive launcher icon, Play Protect instructions, andrelease.ymlgated on tests.Verification
Green: workspace
--all-targets;viberfox371,viberfox_geo109,viberfox_core26,viberfox_simulator48+,big_space14;--no-default-features;aarch64-linux-android; wasm on nightly;cargo fmt --check.Two things could not be verified anywhere in this container and need real hardware:
adb installwithadb logcat -s viberfox:V bevy_render:V wgpu:V— launch, map streams, rotate, home, resume, back. Nothing between "it compiles for aarch64" and "a person uses it" has ever been observed.PendingIntentregistration land in the cache we poll. That is how AOSP'sLocationProviderManagerbehaves, but if it is wrong the symptom is an arrow that never moves — and there is no third dex-free option, only GameActivity and Gradle. The four device-dependent claims are written intorun_location's doc comment.Anything touching camera, shaders, egui layout or lighting is visually unverified; this container has no GPU.
The Linux build defaults to the `solo` feature, so this simulator runs in-process in the shipped app — and `panic = "abort"` means anything here kills the user's client outright. The worst of it: `Slot::compile` ran the author's top-level chunk with a plain `pcall`, not a coroutine. Luau's `lua_isyieldable` is `nCcalls <= baseCcalls` and only `lua_resume` raises the base, so a `pcall` body is non-yieldable — and mlua *silently discards* a `VmState::Yield` it cannot honour. `LOAD_FUEL` therefore never fired, and the comment promising that `while true do end` at the top level must not hang the host was simply false. The existing test only covered a loop inside `on_start`, which does run in a coroutine and was correctly metered; that is why it survived. The consequences compounded: the host thread never returned to `rx.recv()`, `HostMsg` is an unbounded channel fed every scripted prim 20x/s, so the process grew until OOM — and because the source is persisted before it runs, a restart replayed it and hung again before any other prim compiled, while still logging "scripts loaded" with the full count. The chunk now runs as a fuel-bounded thread like every handler, and `Slot::handler` uses `raw_get`, closing the second route in (a top-level `setmetatable(getfenv(), ...)`, which `luaL_sandbox` does not prevent). The rest, in the same spirit — things one client could do to everyone else: - Per-connection inboxes were unbounded, and `send` on an unbounded channel never fails. A client whose TCP window closed blocked the drain while nearby speakers pushed ~100 KB/s of voice into it forever. Now two bounded lanes, text (256) and voice (64), with different overflow policies. - Voice could be delivered into any room without joining it, and the room name was bounded only by `MAX_FRAME` = 32 MiB. Membership is checked and `MAX_ROOM_LEN` applies to voice, text and subscribe alike. - `ClientIntent { move_x: INFINITY }` divided to NaN and wrote NaN into the authoritative position, out to every client. NaN alone was already rejected (`NaN > 1e-4` is false); infinity was the hole. - argon2 ran while holding the SQLite mutex, which the tick loop also takes while holding the world write lock — fifty unauthenticated sockets froze every client. The row read now drops the guard before hashing, and the verify is `spawn_blocking` behind a semaphore. - Session tokens never expired and the map only grew; `SetPrimScript` had no rate limit and broadcast the source to everyone; `accept_loop` hot-spun on EMFILE; the timer queue could silently evict a script's `Start` event. - The Luau sandbox opened the `debug` library. Not exploitable today, but the stated model was wrong and no test would have caught it growing. No protocol change: `MAX_ROOM_LEN` is a constant beside `ChatScope`, so `PROTOCOL_HISTORY` gains no row.`quality::Budget` already dialled the phone down — shadows off, bloom zero, sky-IBL off — but the levers that mattered most had no field, and every env-var escape hatch (`VIBERFOX_FPS_CAP`, `VIBERFOX_NO_SPACE_RAYMARCH`, `VIBERFOX_QUALITY`) is unreachable on Android, which has neither a process environment nor a query string. So each of these is a `Budget` field. **MSAA was the dangerous one.** `Msaa::Sample2` applied on Android, and Vulkan's guaranteed minimum for `framebufferColorSampleCounts` is `1 | 4` — 2x is optional. That is byte-for-byte the failure the surrounding comment already documents twice for WebGPU and lavapipe: `main_texture_sampled` fails to create and nothing renders at all. Cost, too: `AtmosphereSettings` requires `Hdr`, so at 1080x2340 the attachments are ~100 MB with 2x, and 16 B/px x 2 samples + depth overruns a tile buffer and forces extra binning per draw. **The clipmap rasterised every tile at 512² on every tier.** The globe fixed this long ago (256² plus `render_mvt_sized`, measured as 2-3x oversampled per axis); the flat path never got it. 224 tiles at 512² RGBA with mips is ~310 MB against ~78 MB, and since scan conversion dominates parsing by ~20x and scales with area, it is also most of the time-to-first-map. Note the caveat recorded at the constant: the finest ring drops to ~1.4 texel/m, which is tolerable only because at ground level the ground is vector geometry over the imagery rather than the imagery. The rest, briefly: - Six of the largest mesh and texture assets were stored in both worlds. Nothing reads them back. `tile_loader` already documented this exact mistake for tile textures ("paid for twice… hundreds of megabytes") and fixed it there; the meshes never got the same treatment. - Atmosphere LUTs recomputed ~4 M march steps per frame at Bevy's desktop defaults; halved on the phone. Space-tier raymarching is off there. - `AsyncComputeTaskPool` now honours `Budget::worker_cap`. It could not before: `platform::workers::init` is a no-op off the web, so the phone's cap of 3 constrained nothing and the real width was `TaskPoolOptions`. - Android gets FIFO present mode. `AutoNoVsync` is a Wayland/i915 occlusion workaround; on a handset it means rendering frames SurfaceFlinger discards, paid for in heat on a device that throttles. - `apply_feature_toggles` swept every building, window, 3DBAG mesh, tile and region quad every frame. It is now gated on a *value* comparison, not `resource_changed::<DataLayers>()` — `update_data_layers` mutably derefs the resource every frame and Bevy marks changed on `DerefMut`, so that gate would have been true forever. The gate also covers the space-tier boolean and the anchor tile, or a descent from orbit leaves the buildings hidden. Newly streamed entities get their own `Added<>` pass. - `animate_stop_rings` wrote every ring's `Transform` every frame; the phase is quantised to 20 Hz, above the threshold where a large soft-edged shape stops reading as continuous and below every frame rate we ship. - Four leaks of the shape this repo has fixed three times already — a key removed from one collection and left in a parallel one: `HeightLayer:: patches` had no removal path at all, `TransitVehicles::label_mats` was skipped by its own teardown, `detail_pois` was kept while its three sibling maps were pruned (bounded rather than pruned — it is also the dedup key for the detail stream), and the routing `GraphCache` pinned up to three graphs for the session after one route. - `bookmarks` flattened the whole tree every frame with the panel closed; the HUD built its strings above its own visibility gate. Fixes along the way that are not performance: **Collision was off whenever the Height layer was on.** The airborne test read `display_position`, which folds in `terrain_lift` — and CLAUDE.md measures Dutch-city relief at ~2 m against an `AIRBORNE_EPS` of 0.2. So the client believed the avatar was airborne on every frame and skipped steering entirely: the avatar walked through every wall online, and offline the snap-out backstop never ran, stranding anyone who teleported inside a building. The offline movement path always subtracted the terrain, which is what makes the other two sites bugs rather than a design choice. **One bookmark permanently killed the HUD and the compass.** `render_hud` set `bookmarks.panel_open`, a legacy operand nothing ever cleared, and the compass, locate button, clock, coordinates and altitude all read it. Press the star once and the status overlay was gone for the session. `RightPanel` is the real state now and is the only thing consulted. **Nothing persisted on Android.** `user.json` was cwd-relative and a NativeActivity process starts at `/`, so every save failed and only warned — bookmarks, Home, layer prefs and the remembered session were lost on every app switch, which also meant the one destination path that needs no typing did not survive. The store now resolves an app-data root the way the tile cache already did, writes through a temp file and a rename, quarantines a corrupt file instead of overwriting it within one frame, and carries `#[serde(default)]` on every leaf so one added field cannot wipe an install. The disk cache gained a startup size sweep; it had no bound and nothing ever reclaimed it. Street labels now sample the terrain field and rebuild on its revision — the road surfaces under them were displaced and the names were not, so with the Height layer on they sank into the road.