Skip to content

Add js/wasm (WebGL2) backend for the opengl package#128

Draft
fisherevans wants to merge 11 commits into
gopxl:mainfrom
fisherevans:fisher/wasm-support
Draft

Add js/wasm (WebGL2) backend for the opengl package#128
fisherevans wants to merge 11 commits into
gopxl:mainfrom
fisherevans:fisher/wasm-support

Conversation

@fisherevans
Copy link
Copy Markdown

@fisherevans fisherevans commented Apr 21, 2026

Summary

Adds a parallel WebGL2 backend to the opengl package under the js && wasm build tag so pixel can target the browser. The existing GLFW + go-gl files gain //go:build !js; new *_wasm.go siblings implement:

  • Window — creates/attaches to an HTML canvas, owns the WebGL2 context, drives the render loop via requestAnimationFrame, handles WebGL context loss by reloading the page, auto-syncs the canvas backing store to CSS size × devicePixelRatio each frame so resize/fullscreen transitions propagate into window.Bounds().
  • Input — keyboard and mouse via DOM events (keydown, keyup, mousedown, mouseup, mousemove, wheel, mouseenter, mouseleave, blur, contextmenu) mapped onto pixel's Button constants, fed through the same internal.InputHandler the desktop backend uses, and surfaced through the existing Pressed / JustPressed / MouseScroll / callback APIs.
  • Gamepads — polled each frame via the HTML5 Gamepad API (navigator.getGamepads()). Standard-layout pads are remapped so button and axis order match the desktop GLFW backend (including promoting LT/RT from analog buttons to the trigger axes); non-standard pads pass through as raw indices.
  • Cursor / Monitor — browser-appropriate shims (cursor via CSS, monitor stubbed).
  • Run — replaces GLFW's mainthread.Run with a direct call under WASM.

The only desktop-visible change is explicit float literals (0.0, 2.0) added to two of pixel's internal shaders — GLSL ES 300 is stricter about int→float conversion than 330 core, so the change lets the same source compile for both targets.

Dependencies

Depends on companion WASM branches of glhf and mainthread:

Test plan

  • Desktop builds unchanged (go build ./..., go test ./... — two tests/ floating-point failures are pre-existing on upstream main).
  • GOOS=js GOARCH=wasm go build ./... succeeds against the companion glhf + mainthread WASM branches.
  • GOOS=js GOARCH=wasm go test -c compiles all packages cleanly.
  • Exercised end-to-end: game boots in a browser with keyboard, mouse, and gamepad input working.

Fisher Evans and others added 3 commits April 20, 2026 22:16
Adds a parallel WebGL2 backend under the `js && wasm` build tag so pixel
can target the browser without touching desktop code paths. The existing
GLFW + go-gl files are tagged `!js`; new `*_wasm.go` siblings implement
Window, Canvas, input (keyboard/mouse via DOM events), and stubs for
cursor/joystick/monitor.

Also refactors a few desktop call sites off the raw `go-gl/gl` package
and onto glhf wrappers (`BlendFuncSeparate`, `BlendEquation`,
`ActiveTexture`) so the same source compiles for both targets. Two of
pixel's internal shaders gained explicit float literals (`0.0`, `2.0`)
so GLSL ES 300 — which is stricter about int->float conversion than
330 core — accepts them unchanged.

Depends on companion WASM branches of glhf and mainthread. Desktop
behavior is unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The PR description claimed mouse input was wired through DOM events,
but input_dom_wasm.go only installed keyboard listeners. Add mousedown /
mouseup / mousemove / mouseenter / mouseleave / wheel / contextmenu
handlers, map MouseEvent.button to pixel.MouseButtonN, and fire all
user-registered callbacks (button, char, mouse moved/entered, scroll)
so game code that sets them sees the same events as on desktop.

Also drop the unused internal.InputHandler{} shim and its stale "not
wired up yet" comment, and add a WebAssembly section to the top-level
README describing how to load the canvas, which features are stubbed,
and the required build command. Adjust the "Missing features" list so
the HTML5 backend isn't still called out as missing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Polls navigator.getGamepads() once per UpdateInput and feeds the results
through the same internal.JoystickState machinery the desktop backend uses.
Standard-layout pads are remapped so button / axis order matches the GLFW
backend (including promoting LT/RT from analog buttons to the trigger
axes); non-standard pads pass through as raw indices so applications can
still address them.
Fisher Evans added 7 commits April 21, 2026 10:59
On WASM, mainthread.CallNonBlock spawns a goroutine. Every WebGL call via
syscall/js is a goroutine scheduling point, so the deferred clear can preempt
between draw calls and wipe an in-flight render. Using the blocking Call
ensures the clear runs synchronously in order with other GL operations.
On WASM, mainthread.CallNonBlock spawns a goroutine for each GL call.
Because every syscall/js invocation is a goroutine scheduling point, a
non-blocking draw or vertex upload can be preempted by window.Update
(SwapBuffers) before it runs, producing black frames. Changed both
canvasTriangles.draw and CopyVertices to use the blocking Call.

CopyVertices previously used CallNonBlock only for small batches (<256
floats) as a perf optimization. The optimization is dropped in favor of
correctness across all platforms.
Maps touchstart/touchmove/touchend/touchcancel to MouseButton1 + mouse
move events so single-finger touch works identically to a left-click on
all platforms. preventDefault on all touch events (with passive:false)
stops browser pan, zoom, and long-press callout behaviours.
…loading

Auto-reload was silent and jarring on mobile (iOS drops WebGL contexts
when switching tabs). Now the page handles recovery UX via the callback;
falls back to reload if the callback is not defined.
MaxDevicePixelRatio (default 2) prevents DPR=3 devices (iPhone Pro)
from allocating a ~12 MB framebuffer that exhausts iOS GPU memory
limits. DPR=2 is indistinguishable for pixel-art content.

pixelOnContextLost now receives a diagnostics object: renderer, dpr,
backing store dimensions, estimated framebuffer size, elapsed time,
and loss count so the page can surface actionable info to the user.
…unting

Track all active touches by Touch.identifier in Window.activeTouches.
ActiveTouches() returns a snapshot of all simultaneous touch positions
so callers (e.g. virtual gamepads) can hit-test each finger independently.

Fix MouseButton1 edge detection under multi-touch: previously any finger
lifting fired Release even if other fingers were still down. Now Press
fires only when the count goes 0→1, Release only when it goes 1→0.

Desktop Window gets a nil-returning stub so the method is available
on both platforms without conditional compilation at call sites.
@dusk125
Copy link
Copy Markdown
Contributor

dusk125 commented Apr 21, 2026

Hey, since it looks like you're still working on this, do you mind changing this to draft until you're ready please?

@fisherevans fisherevans marked this pull request as draft April 21, 2026 17:20
@fisherevans
Copy link
Copy Markdown
Author

Yeah, of course - my apologies

@dusk125
Copy link
Copy Markdown
Contributor

dusk125 commented Apr 21, 2026

No worries! Thanks for that!

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.

2 participants