feat(html5): HTML5Runtime -- native WebView runtime for Chromium-based games

Draft
JE
36m ago
+46133-107

Description

Adds HTML5Runtime -- a second, native game runtime alongside the existing Wine/Proton container. Chromium-based desktop games (NW.js, Electron, RPG Maker MV/MZ, Construct 2/3, GameMaker HTML5, Godot Web, Unity WebGL, Tyrano) run directly in Android WebView instead of stacking Chromium-in-NW.js-in-Wine-in-Box64. Same library, same download pipeline, same controller/achievement/cloud-save UX -- dramatically better perf, thermals, and battery.

The Wine path is untouched and remains the default. Everything here is additive and opt-in: a runtime column defaults to wine, the feature sits behind a FeatureGate flag, and titles are routed to WebView only when an engine is positively fingerprinted.

Core value: Steam-owned HTML5 games launch from the library and just run -- faster and cooler than under Proton -- with saves that round-trip through the existing Wine prefix so Steam Cloud / GOG Galaxy sync keep working with no per-title remap.

What's included

  • HTML5Runtime + WebView host -- per-engine pack/shim system, request interceptor (source game folder stays read-only; all patches applied in-flight).
  • Engine fingerprinting + install watcher -- detects the engine at install time and tags the container; bare-WebView fallback for self-contained wasm titles (Godot).
  • Runtime-aware launch path -- GameRuntime split; WebView suspends on activity background (Web Audio paused at both lifecycle boundaries).
  • Cloud save sync (Steam + GOG) -- Chromium LevelDB <-> Steam UFS, and a JS fs bridge <-> GOG Galaxy cloud. Both stores are supported and validated end-to-end on device (round-trip in both directions). Saves land where Steam UFS / GOG Galaxy expect because the runtime loads the Windows NW.js distribution (process.platform = win32, Windows-styled env vars, C:/... paths translated into the Wine prefix) -- no per-title remap. Epic save-sync is wired through the same path but is unconfirmed (no verified round-trip yet).
  • Input -- gamepad, touch, and physical mouse/keyboard inside the WebView; native-pad masking so engine input stacks (Unity InputSystem, Impact) don't double-read.
  • Achievements (Steam) -- Steam-aware close-time sync that emits into the same Goldberg event bus the Wine path already uses (no parallel achievement plumbing).
  • UI -- library/dialog/settings integration, opt-in service, "Reclaim Wine Storage" action (surgical wipe of drive_c/{windows,Program Files*}, preserves saves under drive_c/users/).
  • i18n -- 79 new runtime strings translated across 14 locales.
  • ANGLE-override advisory snackbar for the handful of titles that need a system WebView/driver swap (see Limitations).
  • Vendored :iq80-leveldb module (pure-Java LevelDB fork, per-thread snappy scratch) -- needed because Chromium's IDB/LevelDB format has edges the stock library mishandles on older Android.

Engine pack support

PackCoversStatus
pack:c3Construct 2 + Construct 3 (incl. NW.js-wrapped)Validated (Hypnospace, SolCesto, Universe For Sale)
pack:rmmvRPG Maker MV + MZValidated (OMORI, Look Outside, ISAT)
pack:nwjsImpact-engine titles (CrossCode-class)Validated (CrossCode, Alabaster Dawn)
pack:electronElectron-wrapped titlesValidated (Wayward, Cookie Clicker, Tyrano-on-Electron)
pack:gmsGameMaker Studio HTML5 exportValidated (RouletteKnight, AutoNecrochess)
pack:godotGodot Web (self-contained wasm, bare-WebView)Validated (DnDG demo)
pack:unityUnity WebGL (brotli-over-loopback serving + canvas-fit)Validated (Perihelion)
pack:tyranoTyrano builder titlesValidated

Pack identifier is the runtime, not the packaging format (pack:c3 covers C2/C3 even when NW.js-wrapped; pack:nwjs is reserved for Impact engines specifically).

Scope notes

  • Steam-first. Steam UFS is the primary cloud target; GOG Galaxy save-sync also round-trips (both validated on device). Epic save-sync is wired in but unconfirmed -- no successful round-trip verified yet. Amazon HTML5 is a later milestone.
  • Diff size is inflated by the vendored :iq80-leveldb module, 14-locale string files, and JS shim assets; the net app-logic surface is app/gamenative/html5/* (~99 files) plus the runtime split.
  • The top commit (on-screen joystick ...) is an on-screen-control fix kept in this PR because some packs' touch input relies on it. Its [NON-SCOPE] commit label predates that decision.

Why the save-sync and achievement changes (and why they touch the Wine path)

A few changes in this PR sit in shared Steam/GOG-core code rather than the HTML5 package. They were required to make HTML5 saves and achievements round-trip, but they also correct behavior on the existing Wine path -- so they're called out here explicitly, and several could land independently of the HTML5 milestone.

Save sync. The cloud stores save files opaquely -- it has never cared that they're LevelDB. Under Wine the desktop Chromium / NW.js writes its IndexedDB / LocalStorage into the prefix and those files already round-trip like any other save. The HTML5 path is harder for three reasons, none of which the Wine path has to solve:

  • Two profiles, not one. The WebView writes its IndexedDB / LocalStorage (on-disk LevelDB) into the Android WebView profile dir, not the Wine-prefix paths Steam UFS / GOG Galaxy sync. So saves have to be bridged between the two profiles -- at runtime boundaries only (launch / exit), never live, so neither DB is mutated mid-game. The runtime loads the Windows NW.js distribution (process.platform = win32, Windows-styled env vars, C:/... translated into the prefix) precisely so the bridged files land at the AppData / install paths the cloud already expects -- no per-title remapping.
  • Chromium version skew. The Android WebView's Chromium and the desktop Chromium that wrote the cloud copy are different versions, so the bridge has to read/write the LevelDB itself rather than trust the engine -- hence the vendored :iq80-leveldb reader/writer (Chromium IndexedDB uses a custom idb_cmp1 comparator and format edges the stock library mishandles).
  • LevelDB multi-generation accretion. LevelDB is an LSM tree; cross-device restore plus accretive cloud sync can leave several DB generations (old + new MANIFEST / .log / .ldb) colliding in one dir. The Wine path leans on the desktop Chromium's own obsolete-file cleanup to tolerate that; the transcode path can't, so it wipe-and-rebuilds the game-facing DB at each boundary, prunes stale generations before read, and (HTML5-only) mirror-deletes cloud-only files. Without it, the rebuilt DB references stale tables (a "Frankenstein DB") and the game opens but can't load its saves.

Two of those needs reach into shared store code:

  • GOG cloud protocol correctness (gzip uploads with deterministic md5/ETag, +00:00 timestamps, User-Agent, GalaxyConfig.json client_id fallback) to match Galaxy / Heroic. Without these, GOG cloud round-trips diverge or fail. These protocol fixes apply to all GOG titles, Wine included -- a general GOG-cloud fix, not HTML5-specific.
  • two Steam UFS correctness fixes the round-trip work exposed: addpath subfolder handling, and a reconcile when completeAppUploadBatch returns EResult.Fail.

One deliberate asymmetry: destructive cloud deletion (mirror-deleting cloud-only files) is opt-in and HTML5-only on both stores -- gated by propagateDeletions (GOG) / the equivalent Steam gate, both keyed on "is this an HTML5 container." The Wine path on Steam and GOG stays accretive and never deletes cloud files. HTML5 needs mirror-delete because it rewrites the LevelDB wholesale at each boundary, so a stale cloud file would otherwise resurrect.

Achievements. Both achievement fixes stem from one Steam protocol detail: userStats.achievementBlocks[].unlockTime[] is sticky -- Steam keeps the timestamps populated even after an achievement is reset (Support, ClearAchievement, SAM). Only userStats.stats[].statValue holds the live earned bitmask.

  • Close-time sync previously rebuilt the base bitmask from the sticky unlockTime[], so every sync re-uploaded the prior earned bits and silently undid any user-initiated reset. It now seeds from userStats.stats.
  • Close-time sync now fetches user stats, subtracts the already-earned achievements, and pushes only what Steam doesn't already know -- falling back to the legacy additive push when the fetch fails (offline at close), so offline-earned unlocks aren't lost.

Both achievement paths are shared: Wine and HTML5 both call storeAchievementUnlocks and closeApp's syncAchievementsFromGoldberg. So these are Steam-core correctness fixes that benefit the existing Wine runtime too, and could merge independently of HTML5.

Recording

TODO: attach short clips -- one RMMV title (OMORI), one Construct (Hypnospace), one Impact (CrossCode) launching + a save round-trip through Steam Cloud.

Test plan

  • Wine launch path unchanged -- full legacy unit suite 1568/0 (14 skipped) green post-rebase.
  • Per-engine device launches: pack:c3 / rmmv / nwjs / electron / gms / godot / unity / tyrano (titles listed above).
  • Save round-trip both directions through Steam UFS (Look Outside Return Home) and GOG Galaxy (In Stars and Time).
  • Epic save-sync round-trip -- wired but not yet confirmed on device.
  • Achievements: reset on Steam -> in-game unlock -> live upload -> reflected on Steam web (Look Outside).
  • Gamepad d-pad + sticks + face buttons across all packs; physical mouse/keyboard.
  • Suspend/resume audio at QuickMenu and activity-background boundaries.
  • Reviewer: verify on a device with a stock (un-swapped) system WebView.

Limitations & Risks (with mitigations)

  1. WebView version variance. Older system Chromium lacks GPU caps some engines need. Mitigation: feature-detect on boot; minimum-version gate; engine fingerprint gates routing so unsupported titles stay on Wine.

  2. ANGLE-Vulkan vs GL conflict (per-title). A few titles (terra-engine, Alabaster Dawn) need ANGLE-Vulkan on newer WebView; others (RMMZ filter-RT titles like Look Outside) break with it. Mitigation: ANGLE-off is the global default (correct for the majority); an advisory snackbar flags the exception titles and points at the WebView-impl/Shizuku swap. Documented, not silent.

  3. Shader precision on Adreno. mediump fp16 math produces banding/stripes on Adreno that desktop fp32 hides. Mitigation: shaderSource hook promotes mediump -> highp (shipped; fixed Alabaster Dawn stripes + fuzzy shadows).

  4. Chromium LevelDB format edges. Save corruption risk where Chromium's IDB/LevelDB format meets a Java reader (idb_cmp1 comparator, .ldb/.sst, mmap covariant nio, CRC32C on API<34). Mitigation: vendored iq80 fork (per-thread snappy scratch, PureJavaCrc32C, release-8 for API<34). Two edges hit and mitigated; flagged to watch for a third.

  5. Save sync is asymmetric by design. WebView Chromium is the live save store during gameplay; the LevelDB <-> UFS rewrite happens only at runtime boundaries (launch / exit), not live two-way. Mitigation: intentional -- source folder stays read-only, sync at boundaries avoids mid-game corruption. Documented invariant.

  6. VP8 webm cutscenes render as broken-image placeholders on devices without hardware VP8 (some Tyrano titles). Mitigation: documented limitation; transcode-to-VP9 workaround.

  7. Engine native-vs-HTML5 export ambiguity. Several engines ship native + web exports sharing file extensions. Mitigation: fingerprint discriminates by engine-runtime presence (.wasm, html5game.js, Build/*.framework.js), not .exe absence (corrected over 3 iterations with native/web fixtures).

  8. Render Scale < 1 looks rough on some RMMV/nwjs titles. Mitigation: CSS pixelated injection ships; deeper JS-shim fix (texParameteri / imageSmoothingEnabled) is follow-up.

  9. Godot in-game quit freezes briefly then recovers via onRenderProcessGone (Godot's own synchronous cleanup, not our code). Mitigation: QuickMenu is the clean exit path; documented.

  10. Back-compat. Mitigation: runtime defaults to wine, all guards additive, feature behind FeatureGate; legacy suite green; Wine path is the no-regression bar.

Type of Change

  • Compatibility improvements
  • Performance / stability improvement
  • Other (new runtime -- requires prior approval / discussion)

Checklist

  • If I have access to #code-changes, I have discussed this change there and it has been green-lighted...
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

Summary by cubic

Adds HTML5Runtime, a native Android WebView runtime that runs Chromium-based desktop games directly instead of stacking Chromium-in-NW.js-in-Wine. Wine stays the default; WebView routing is opt-in and only engages when an engine is positively fingerprinted. The runtime splits into Wine and WebView paths, and a running WebView suspends on activity background to match Wine pause semantics. The shared cloud-save and achievement fixes below also correct behavior on the existing Wine path. All 79 new runtime strings are translated into 15 locales.

Bug Fixes

  • Fixes CrashHandler wrapping itself on every app boot and double-dispatching crashes; it now installs once and dispatches once.
  • Fixes Steam Cloud sync: reconciles the cache after failed upload batches, skips commitFileUpload for SHA-deduped blobs, and silent-rehydrates when cached content matches the cloud; decodes embedded and bare %Root% tokens in sync paths.
  • Fixes Steam achievements: seeds unlocks from live userStats.stats so user resets aren't re-uploaded; only pushes newly earned ones.
  • Fixes GOG Galaxy cloud sync with a Heroic-compatible protocol and streamed gzip snapshots; gates uninstall on in-flight close-time uploads.
  • Self-classifies RMMV/MZ save substrate at runtime and scrubs LevelDB scratch; stubs Effekseer WASM on affected WebView builds; scopes CrossCode profile sync per store.
  • Caches OMORI's AES decrypt key for cold-boot/offline launches; reports container language on navigator.language and Steam bridges; skips depot re-downloads on language changes for WebView.
  • Exits to library on Godot engine-quit wedges; advises terra-engine titles to enable ANGLE-on-Vulkan.
  • Adds on-screen joystick deadzone and zero-axis stick release; stabilizes CI by marking leveldb fixtures binary and checking out submodules.
  • Keeps runtime resolution on the IO thread; config import and Reset-to-defaults no longer flip an html5 game back to Wine, and reclaimWineStorage refuses to wipe a Wine container itself.
  • Serves every html document with a CSP confining frames and objects to the game origin, so remote documents can't load in the game WebView; http(s) links open in the browser.
  • WebView sessions emit game_exited and game_runtime_switched telemetry, mirroring the Wine event; new html5 controls profiles seed their on-screen layout from the default Wine profile.

Refactors

  • Vendors :iq80-leveldb (pure-Java) with a per-thread snappy scratch fix and builds snappy-java from source for 16 KB-aligned native libs; both are pinned as submodules under the GameNative org and checked out by CI.
  • Adds declarative pack registration and typed engine/source ids.