Making of King Tide

Chapter 06 — Porting to Steam

"It's just a web game" meets a console.

King Tide runs in a browser tab. To sell it on Steam — and run it on the Steam Deck — it needs to look like a native app: a window, an executable, a Steam depot. The web part was done. The shell around it nearly sank the whole Steam port — and the hardest part, by a wide margin, wasn't the game at all. It was getting the thing to launch through Steam in the first place.

Why Tauri had to go

The first wrapper was Tauri: tiny binaries (~10 MB) and a Rust core that could one day link Steam's SDK directly. On paper, perfect. In practice it nearly sank the port — and not for any one dramatic reason. It was death by a thousand hoops.

The single biggest problem, by far, was just getting it to launch through Steam at all. Tauri renders with the operating system's own web view — WebKitGTK on SteamOS — which dynamically links a sprawl of system libraries. But Steam launches games inside the Steam Linux Runtime, a locked-down container that doesn't ship that library set. Double-click it from the desktop and it'd come up; launch it the way players actually do — through Steam — and it died before a window ever appeared. Every workaround we tried just surfaced the next missing piece. We sank far more time into "why won't it start?" than into anything in the game itself.

And the reward for finally coaxing it open was a gut-punch: it wasn't performant. WebKitGTK gave us no usable WebGPU, so the renderer silently fell back to the older WebGL2, and the modern water pipeline (the node-material shaders from Chapter 01) crawled. So the best case after all that effort was a build that launched unreliably and, when it did, ran badly. That's not a thing you can ship.

The hypothesis: Electron bundles its own Chromium. So it doesn't depend on the container's libraries, and it ships a modern browser engine with WebGPU. The price is size — about 210 MB versus 10 — which is fine if it actually runs. So before committing, we built a throwaway spike and tested it on a real Deck.

The spike, bug by bug

Four problems surfaced, in order. Tellingly, the first three were the exact same fight that doomed Tauri — just getting the thing to start through Steam — except this time each had a clean, one-time fix instead of a fundamental dead end. Every lesson is now baked into the build.

1. Every asset 404s under file://

Loading the build straight off disk, nothing loaded. Vite builds with absolute asset paths (/assets/…), and under file:// those resolve against the filesystem root. The fix was to serve the bundle over a custom app://bundle/ scheme instead — which, conveniently, also gives the page a secure context, the thing WebGPU refuses to run without.

2. The launcher did nothing — and logged nothing

We zipped the build onto the Deck and the launcher silently did nothing — not even an error log, which was the clue: the process never ran. A plain .zip doesn't preserve Unix permissions, so the executable landed without its +x bit and Steam couldn't exec it. Lesson, now permanent in CI: ship Linux trees as a tar, never a zip.

3. "Steam says running" — but no window

Launched through Steam, the process showed as running with nothing on screen; from the file manager it worked fine. The culprit: Electron's chrome-sandbox helper must be setuid-root, copied depot files never are, and the runtime container blocks the fallback the bare desktop allowed — so sandbox init failed and Electron exited silently. Because we only ever load local, trusted content, the fix is the standard one for Steam: --no-sandbox.

4. The real one: 200 lines of shader errors (and the best news of the spike)

Once it launched, the log filled with ~200 repeats of unresolved interpolation sampling 'either'. But buried in the same log was a line that only ever prints when WebGPU is genuinely active — so the core bet had paid off: real WebGPU on the Deck. 🎉 The errors turned out to be Three.js's internal mipmap shaders using a newer WGSL syntax (@interpolate(flat, either)) that the older Dawn engine inside Electron 31 couldn't parse — a version mismatch, not a wrapper bug. The fix was a one-liner: bump to Electron 42, whose Dawn understands it.

Live demo — this panel runs the same checks the port hinges on, against your browser right now. The water renders on WebGL2 either way; toggle the fallback look to see what losing the modern path costs.

If your browser shows WebGPU: available and Secure context: yes, you're passing exactly the checks WebKitGTK-in-the-container failed. That gap — modern Chromium versus the system web view — is the entire reason for the migration.

Building out the real pipeline

The spike was green, so the throwaway became the shell. A few decisions that aren't obvious until a container bites you:

What this cost, and what's still open

The honest ledger: binaries went from ~10 MB to ~210 MB. Two things are deliberately not done yet — the Steamworks SDK (achievements, cloud saves, rich presence) isn't wired in (the old Rust commands were never-called stubs, so nothing functional was lost), and the online leaderboard server still rejects the app://bundle origin, so leaderboards are offline in the desktop build until that's allowed. Both are tracked.

The quiet lesson

"Web-first" doesn't mean "ships anywhere for free" — the native shell is a real platform with sandboxes, secure-context rules, container libraries, and file-permission gotchas of its own. But notice what didn't change: not a single line of game code. The same boundary that lets this very site embed the real engine (Chapter 05) is what made ripping out one native shell and bolting on another a contained, one-pull-request job. The game is just a dist/ folder; everything else is packaging.

Read the code

The whole wrapper is electron/main.cjs; packaging in electron-builder.yml; the Steam depot upload in tools/steam-upload.mjs. The full chronological account is PR #204, and the operator docs in docs/steam-deck.md + docs/desktop-builds.md.