Skip to content

Split Battlestation runtime for GPUWindow#167

Merged
devallibus merged 2 commits intomasterfrom
feat/battlestation-gpuwindow-runtime-split
Mar 6, 2026
Merged

Split Battlestation runtime for GPUWindow#167
devallibus merged 2 commits intomasterfrom
feat/battlestation-gpuwindow-runtime-split

Conversation

@devallibus
Copy link
Owner

Summary

  • extract Battlestation mission orchestration out of the DOM entrypoint
  • preserve the existing web / Tauri / BrowserWindow path on the shared runtime loop
  • add a native Electrobun GpuWindow path with shared mission logic and native Three WebGPU rendering
  • document the current GPUWindow limitations explicitly

What changed

  • added shared mission/theme config and a shared startBattlestationRuntime() controller
  • browser entrypoint now delegates to the shared runtime controller instead of owning all simulation/input/audio logic inline
  • scene rendering now has a renderer-agnostic core plus an Electrobun GPU wrapper using WebGPURenderer
  • added Electrobun GPUWindow entrypoint and updated Battlestation Electrobun scripts/config to current shell conventions
  • added local Electrobun type shims for the Battlestation example
  • documented native-mode limitations in examples/battlestation/README.md

GPUWindow limitations

  • no HTML HUD overlay in native mode
  • no Web Audio tone playback in native mode yet
  • mission status is surfaced through the window title and console alerts
  • keyboard + mouse controls are implemented directly through Electrobun window/screen APIs

Validation

  • bunx tsc --noEmit -p examples/battlestation/tsconfig.json
  • bun run build:web in examples/battlestation
  • bun run build:electrobun:browser in examples/battlestation
  • bun run build:electrobun:gpu in examples/battlestation

Tracking

@devallibus
Copy link
Owner Author

PR Review — claude-opus-4-6 (Claude Opus 4.6, 1M context)

Overall

This is a textbook runtime split. The 264-line index.ts monolith is decomposed into adapter interfaces + a shared controller, and a GPUWindow entrypoint drops in cleanly against those seams. The refactoring is disciplined — no feature creep, no over-abstraction.

Verdict: Approve with two observations worth checking.


Architecture

The adapter pattern here is exactly right:

startBattlestationRuntime()
├── DefenseSceneAdapter    — render, projectiles, explosions
├── BattlestationAudioAdapter — volume, mute, tones
├── BattlestationControlSource — axis, fire, mute, pointer targets
└── BattlestationHudAdapter    — mission, profile, alerts, status

Both entrypoints (index.ts for browser/Tauri, gpu.ts for native) provide their own implementations of these 4 adapters against the same shared controller. Mission orchestration, alert management, input latching, and tick loop are in one place.

Strengths

  • Scene generalization (scene.ts): createDefenseScene(canvas)createDefenseSceneWithRenderer(viewport, renderer, theme, getDevicePixelRatio). The DefenseViewport and DefenseRenderer interfaces are minimal — exactly the methods used. The original convenience wrapper is preserved at the bottom of the file for the browser path.

  • ResizeObserver correctly removed: Replaced by syncRendererSize() called on every render() frame. This is required for the GPU path (no ResizeObserver in Bun runtime) and harmless for the browser path — comparing dimensions is cheap.

  • Config extraction (config.ts): Types + fallback constants shared between both entrypoints. Clean single-source.

  • Cleanup pattern: startBattlestationRuntime() returns () => void that clears the interval, disposes controls and scene. Both entrypoints wire this to their cleanup hooks (window.on("close") for GPU, beforeunload for browser).

  • satisfies ElectrobunConfig in electrobun.config.ts: Type-checks the config without widening the inferred type. Nice.

  • WASM module declaration (wasm.d.ts): "./wasm/battlestation_wasm""*battlestation_wasm" — correctly handles the different relative import paths from index.ts vs gpu.ts.

  • subscribeAlerts removed from browser path: The old event-subscription-based alert flow is replaced by direct hud.setAlert() calls from the runtime. Simpler, and publishAlert still fires events for any external listeners.

  • Backwards compat: bun/index.js re-exports ./browser, so the existing electrobun config's default entrypoint still works.


Observations

  1. GPUWindow mouse coordinate mapping may be off by title bar height (gpu.ts:83-97): Screen.getCursorScreenPoint() returns global screen coordinates. These are mapped to the 640×640 game world using window.getFrame(), but getFrame() likely returns the outer frame including the title bar (titleBarStyle: "default" is set). If so, cursor.y - frame.y would include the title bar offset, causing the y-coordinate to be shifted upward in the game world. The inside check would also admit clicks on the title bar as valid game targets.

    This may need frame.y + titleBarHeight as the effective top edge, or switching to a content-area frame API if Electrobun provides one. Worth a quick manual test with a click at the very top of the renderable area.

  2. Magic key codes in gpu.ts:7-10: LEFT_KEYS = new Set([37, 65, 123]) — 37 is ArrowLeft, 65 is 'A', but 123 is platform-specific (F12 on Windows, or a macOS virtual key code for arrow keys in native context). Similarly 124 in RIGHT_KEYS. A brief comment mapping these to their meanings would help future readers. Same for 13/32 (Enter/Space) and 77 (M) — less ambiguous but still worth documenting since these are raw key codes, not key names.


Minor notes (non-blocking)

  • Inline type imports (index.ts:1,19): import("./services/backend").MissionView and import("./services/profile").OperatorProfile — these work but are unusual. They avoid top-level imports of modules no longer needed at runtime in the browser entrypoint. Acceptable but worth noting for future readers who may find the pattern unfamiliar.

  • scene-gpu.ts double as unknown as casts (lines 17-33): Necessary because the Electrobun canvas shim and WebGPURenderer don't conform to the DOM types. The cast targets match DefenseViewport and DefenseRenderer exactly. This is the correct cross-runtime bridging pattern.

  • No tests: Acceptable for an example app refactor. The adapter interfaces are implicitly exercised by both entrypoints, and the core webtau framework tests remain untouched.


No Issues Found

  • No security concerns.
  • No behavioral regressions in the browser/Tauri path — the runtime controller preserves the exact same tick/alert/fire/mute logic.
  • The inFlight guard prevents concurrent step execution in both paths.
  • electrobun bumped to ^1.15.1 with correct lock file entries.

LGTM.


Reviewed by claude-opus-4-6 (Claude Opus 4.6, 1M context)

@devallibus devallibus merged commit a35dcd7 into master Mar 6, 2026
9 checks passed
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.

[P2] Battlestation GPUWindow runtime split and playable path

1 participant