Brewser is a homebrew web runtime and app platform for the Nintendo Switch, a real V8 + Skia + WebGL2 engine running natively on the Tegra X1, built for the kind of web apps Chrome can't reach. Catalogue and submissions at brewser.io
✨ Highlights
- Hardware-accelerated video streaming - async decode, in-shader YUV→RGB, zero-copy delivery, A/V decouple, and a content-fps-aware present gate. 720p60 now holds a steady 60 fps on device.
- In-app video player - app-drawn control bar with loading placeholder, play/pause glyph, tap-to-toggle, and a fullscreen path.
- Live-DOM engine leap - a ~5,300-line rendering push plus complete CSS
@keyframes, vertical margin collapsing, andborder-collapsesupport. - Update & compatibility UX - new-version boot toast, in-app version notification,
minBrewserVersioncompatibility notice, and an Offline Mode toggle. - Emoji atlas refresh - the full bundled emoji set re-rendered at higher resolution.
📺 Video Engine (native / nx.js core)
The <video> path was rebuilt from decode to present. All items below are HW-validated (2026-09-06).
- Async decoder open -
VideoDecodernow opens off the V8 main thread, so starting a stream no longer freezes the shell. - In-shader YUV upload - the decoder emits planar I420 (1.5 B/px) and Skia does YUV→RGB on the GPU, cutting per-frame texture upload ~2.6× while keeping full 720p.
- BGRA-direct path - drops the per-frame double swizzle for BGRA sources.
- Zero-copy frame delivery - frames wrap the decoder's buffer instead of a per-frame ~1.4 MB
malloc. - A/V decode decouple - single-thread demux-ahead with per-stream packet deques and non-blocking video enqueue; audio is drained to a 0.25 s target so a full video ring can never starve audio (
aur_underrunsheld at 0). - GC-hitch fix - the YUV delivery path now reuses one persistent
ArrayBuffer(deliver_ab) instead of handing V8 a fresh one each frame. This eliminated the 720p periodic hitch: major GC was firing 2–3×/s (~7.5 ms each) against the 33 ms budget; it now drops to majGC = 0. - Content-fps present gate -
VideoDecoder.fpsis now exposed. The 30 Hz present lock (added to smooth 30 fps content) was halving 60 fps content to 30 shown fps; the shell now keeps 60 Hz for >32 fps content.- HW results: 720p60 → steady 60 fps; 480p60 → ~53–60; 4K60 → ~51 (decode ceiling).
gfxSetSwapInterval()binding - EGL swap-interval pacing lever (the 30 Hz lock is left in place but flag-gated after HW proved a slow present starves the single decode thread's audio).
🎬 Video Player (page runtime + shell)
- YUV-mode
<video>consumer viaSwitch.createBitmapFromYUV, with aVIDEO_USE_YUVkill-switch and frame-bitmap reuse. - App-drawn control overlay: fixed-video blit, "Loading stream" placeholder, paused play-glyph, above-composite repaint.
- Tap-to-play/pause on the video frame in non-fullscreen mode.
- Shell publishes active-decoder content fps (
getActiveVideoContentFps()) to drive the 60 Hz gate; new player UI assets bundled (play/pause/volume_on/volume_off/fullscreen/gear).
🧬 Live HTML / CSS / DOM Rendering
- Major live-DOM update - ~5,300-line expansion across the HTML parser,
live-css,live-dom,live-layout,live-overlay,inline-css,live-form, and the SVG painter (+1,457 lines). - Complete CSS
@keyframes(HW/Citron-confirmed) - addstranslate(px + %),background-color/colorinterpolation,animation-delay, andanimation-direction(normal/reverse/alternate/alternate-reverse) on top of the existing rotate/scale/opacity + easing. - Vertical margin collapsing (CSS 2.1 §8.3.1) (HW/Citron-confirmed) - adjacent-sibling, parent↔first/last-child collapse-through, and empty-block self-collapse, correctly gated by BFC/border/padding edges. Also fixes a pre-existing bug where padded/bordered containers dropped their last child's margin.
border-collapse: collapse(HW/Citron-confirmed) - shared table borders drawn centred on cell edges so adjacent borders coincide instead of doubling.- Live CSS + emoji atlas - path-utils, canvas-runner, emoji-atlas, and permission-policy refinements.
🔔 Updates, Compatibility & Offline
- New-version boot toast - boot update check surfaces available updates at startup.
- In-app version notification and refreshed Check for Updates / missing-app modal flows.
minBrewserVersioncompatibility notice - the app modal shows a non-blocking amber notice when an installed app expects a newer Brewser, sourced from the installed manifest first (falling back to the catalogue).- Offline Mode toggle - one
__brewserOfflineModeglobal gates the boot reachability probe, version poll, and telemetry; catalogue reads from cache, the connectivity indicator shows orange, and network-bound controls (Check for Updates / Download) are disabled. Applies live without a reboot.
🔐 Permissions & Compatibility
- Permission warnings catalogue (
warnings.json) - per-permission risk level + user-facing warning copy for network, local_network, storage, system, filesystem read/write, device_info, account, external_links, USB, Bluetooth, and NFC. - Surface compatibility push - shell + navigation fixes broadening general web-page compatibility.
- Added a local-network classifier test suite (91 assertions).
😀 Assets
- Full bundled emoji atlas re-rendered at higher resolution (~360 glyphs, including flags and ZWJ sequences).
🔐 Checksums
brewser.nro: 26ea567f10188f6938625dbedd0cebd29363afae25f78b7b4211f43738c3f629
brewser-forwarder.nsp: 1e3268fe83d45a811c38366645ed59854d6a0a4243de9c97ab156ffdc1a33af8
🙏 Credits
Brewser stands on the shoulders of nx.js by TooTallNate, Atmosphère, V8, Skia, Mesa/Nouveau, and libuv - and on the wider Switch homebrew community.
⚖️ Disclaimer
Brewser is a homebrew application for consoles running custom firmware. It is not affiliated with, endorsed by, or associated with Nintendo. Use CFW at your own risk.
Found a bug? Open an issue · Built something cool? Submit it at brewser.io/submit · Say hi on the GBAtemp thread
