switch-webapp-player is an nx.js homebrew app for running one fullscreen static Canvas/WebGL/WASM-style web app from the Switch SD card. It is based on the nx-canvas template and uses nx.js runtime APIs directly. It does not use the Switch WebApplet/browser.
The MVP hardcodes this app root:
sdmc:/switch/webapps/
default/
app.json
bundle.js
asset.json
app.wasm
assets/
shaders/
wasm-test/
app.json
bundle.js
app.wasm
Example app.json:
{
"title": "Default Web App",
"entry": "bundle.js",
"width": 1280,
"height": 720,
"fullscreen": true
}The runner scans sdmc:/switch/webapps/ for child folders containing app.json. If the only app is default/, it launches that app immediately and skips the launcher screen. If multiple apps are present, it shows a simple Canvas launcher.
Launcher controls:
- Up/Down or ArrowUp/ArrowDown: select app
- A or Enter/Space: launch selected app
- B or Escape: refresh/rescan
Runtime combo:
- Hold L + R + Minus for about one second to stop the current app.
- If multiple apps are available, this returns to the launcher.
- If only
default/exists, this exits the app.
The selected entry file is loaded from manifest.entry. Repeatable sample apps are included in examples/; copy one example's files to sdmc:/switch/webapps/default/ for direct-launch testing, or copy several examples into separate subfolders to use the launcher.
For Citron on Windows, the matching folder is usually:
C:\Users\<you>\AppData\Roaming\citron\sdmc\switch\webapps\default\
Available examples:
examples/default-webapp/- immediate Canvas 2D draw,requestAnimationFrame()animation, andfetch("./asset.json").examples/fetch-test-webapp/- checksfetch("asset.json"),fetch("./asset.json"),fetch("/asset.json"), nested asset fetches,arrayBuffer(), and 404 handling.examples/wasm-webapp/- loads./app.wasmwithfetch().arrayBuffer(), callsWebAssembly.instantiate(), and draws the result.examples/gamepad-webapp/- checksnavigator.getGamepads(), gamepad connection events, axes, buttons, and D-pad movement.examples/pointer-webapp/- checks touchscreen-to-pointer mapping, mouse fallback events, mirrored window touch events, coordinates, and drag trails.examples/webgl-probe-webapp/- checkscanvas.getContext("webgl")/experimental-webgland shows native WebGL diagnostics.examples/webgl-egl-probe-webapp/- checks the nonstandard EGL/OpenGL ES backend prototype diagnostics and optional GPU clear probe.examples/webgl-clear-webapp/- first native WebGL milestone target; animatesclearColor()once the patched nx.js runtime exposes WebGL.examples/webgl-state-webapp/- checks basic WebGL state calls andgetParameter().examples/webgl-shader-state-webapp/- checks shader/program lifecycle state.examples/webgl-buffer-state-webapp/- checks buffer upload and vertex attribute setup.examples/webgl-triangle-webapp/- draws a software-rasterized triangle throughdrawArrays().examples/webgl-transform-triangle-webapp/- animates a triangle with transform/color uniforms.examples/webgl-quad-webapp/- draws an indexed quad withdrawElements().examples/webgl-textured-quad-webapp/- uploads generated RGBA pixels and draws a textured quad.examples/webgl-external-texture-quad-webapp/- loads./assets/checker.rgbawithfetch().arrayBuffer(), uploads it withtexImage2D(), and draws a textured quad.examples/webgl-sprite-mesh-benchmark-webapp/- cycles 18/36/72 rotating alpha sprites in one dynamic indexed mesh.examples/webgl-axis-aligned-sprite-benchmark-webapp/- cycles 18/36/72 non-rotated alpha sprites to exercise the native indexed-quad fast path.examples/webgl-low-trig-axis-sprite-benchmark-webapp/- cycles 18/36/72 moving non-rotated alpha sprites with per-frame mesh upload but without per-sprite trig.examples/webgl-uniform-axis-sprite-benchmark-webapp/- cycles 18/36/72 moving non-rotated alpha sprites using a static quad,uniform2f()offsets, and repeateddrawElements().examples/webgl-static-axis-sprite-benchmark-webapp/- cycles 18/36/72 static non-rotated alpha sprites with no per-frame mesh upload, isolating native draw cost.
Raw RGBA texture assets are currently width * height * 4 bytes in RGBA8 order. The external texture example uses:
const bytes = new Uint8Array(await fetch("./assets/checker.rgba").then((r) => r.arrayBuffer()));
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 64, 64, 0, gl.RGBA, gl.UNSIGNED_BYTE, bytes);Example PowerShell copy command for Citron:
Copy-Item -Recurse -Force .\examples\wasm-webapp\* "$env:APPDATA\citron\sdmc\switch\webapps\default\"Example multi-app launcher layout for Citron:
New-Item -ItemType Directory -Force "$env:APPDATA\citron\sdmc\switch\webapps\wasm-test"
Copy-Item -Recurse -Force .\examples\wasm-webapp\* "$env:APPDATA\citron\sdmc\switch\webapps\wasm-test\"
New-Item -ItemType Directory -Force "$env:APPDATA\citron\sdmc\switch\webapps\gamepad-test"
Copy-Item -Recurse -Force .\examples\gamepad-webapp\* "$env:APPDATA\citron\sdmc\switch\webapps\gamepad-test\"The app installs browser-like globals before loading the bundle:
window,self, andglobalpoint atglobalThiswindow.innerWidth,window.innerHeight, andwindow.devicePixelRatio- existing nx.js
performance.now()and event APIs - timer-backed
requestAnimationFrame()/cancelAnimationFrame()shims for the current MVP - browser-style
navigator.getGamepads()wrapper with standard-ish A/B/X/Y ordering andgamepadconnected/gamepaddisconnectedevents - touchscreen forwarding from native
screentouch events topointerdown/pointermove/pointerup, mouse fallback events, and mirroredwindow/documenttouch events - WebGL context probing for
webglandexperimental-webgl; with the patched local nx.js runtime this exposes an experimental framebuffer-backed WebGL subset - a minimal
documentshim - one fullscreen canvas returned by
document.createElement("canvas")anddocument.getElementById("canvas") - app-root-relative
fetch()for local SD-card files
Local fetches such as fetch("./asset.json"), fetch("asset.json"), fetch("/asset.json"), fetch("./app.wasm"), fetch("./assets/texture.png"), and fetch("./assets/checker.rgba") resolve against the selected app root.
WASM bytes can be loaded with:
const bytes = await fetch("./app.wasm").then((r) => r.arrayBuffer());
const result = await WebAssembly.instantiate(bytes, imports);- No real DOM.
- No HTML parser.
- No CSS/layout.
- WebGL is an experimental framebuffer-backed subset, not a full browser/GPU WebGL implementation.
- WebGL currently supports a narrow 2D-oriented path: clear/state calls, shaders/program handles, buffers, simple uniforms,
drawArrays(),drawElements(), RGBATEXTURE_2Duploads, and nearest/linear/clamp/repeat texture parameters. - WebGL does not yet support real GLSL execution, varying evaluation beyond the built-in UV path, depth testing, blending, framebuffer objects, mipmaps, compressed textures, cube maps, or GPU acceleration.
- No Web Workers.
- No Service Workers.
- No IndexedDB.
localStorageis only available if provided by nx.js.- Only one fullscreen canvas is supported.
- JS should be bundled into a single script-style
bundle.jsfor now. - Static ESM
import/exportsyntax inbundle.jsis not supported yet. Use a bundler output format such as IIFE/plain script. - Gamepad mapping is currently minimal. Buttons are exposed as A, B, X, Y, L, R, ZL, ZR, Minus, Plus, StickL, StickR, Up, Down, Left, Right.
- Pointer mapping is currently touch-only. It emits one pointer stream per changed touch and does not yet synthesize hover, wheel, pointer capture, or CSS cursor behavior.
See docs/webgl-plan.md for the native-backed WebGL milestone plan.
- Record Citron/device FPS for rotating and axis-aligned WebGL sprite benchmarks at 18, 36, and 72 alpha sprites.
- Add optional audio support.
- Add an optional lightweight
index.htmlloader later.