Web support for N64 static recompilations built on N64Recomp, N64ModernRuntime, RecompFrontend and RT64: the game, its menus and RT64 built with Emscripten into one page that draws through WebGPU. It is game-agnostic. A recomp keeps its own submodules at the commits it pins and adds this repo beside them, and its web build patches their checkouts (Using it in a recomp). It was written for San Francisco Rush 2049: Recompiled, this project's own recomp as Perfect Dark's, Super Mario 64's, F-Zero X's, Mario Party 1's, 2's and 3's, Super Smash Bros.', Paper Mario's, Rayman 2's and Donald Duck: Goin' Quackers' are, and sixteen more recomps build a page with it (Games).
Written almost entirely by AI (Claude), like the recomp it came from.
Each game's repo builds its page with this repo; a page holds no ROM, the player loads their own. This project's own recomps aside, the games are here to build out and test the web support on more than one game: what one of them or one browser turns up is fixed in this repo or the forks unless it really is that game's own, so every game and both browsers get it. Rush 2049, Perfect Dark, Super Mario 64, F-Zero X, Mario Party 1, 2 and 3, Super Smash Bros., Paper Mario, Rayman 2 and Donald Duck: Goin' Quackers are this project's own recomps, each with releases of its desktop build for Windows and Linux on its repo's Releases page.
| Game | Recomp | Repo | Page |
|---|---|---|---|
| San Francisco Rush 2049 | this project's own recomp | rush-2049-recomp | play |
| GoldenEye 007 | GoldenEye64Recomp by podulator, from cblock85's | GoldenEye64Recomp-web | play |
| The Legend of Zelda: Majora's Mask | Zelda 64: Recompiled by Zelda64Recomp | Zelda64Recomp-web | play |
| Banjo-Kazooie | Banjo: Recompiled by BanjoRecomp | BanjoRecomp-web | play |
| Conker's Bad Fur Day | CBFD-Recompiled by sciaschi | CBFD-Recompiled-web | play |
| Donkey Kong 64 | Donkey Kong 64: Recompiled by Rainchus | Donkey-Kong-64-Recompiled-web | play |
| Wave Race 64 | Wave Race 64: Recompiled by danielgomesvieira2000 | wave-race-64-recomp-web | play |
| Pilotwings 64 | Pilotwings 64: Recompiled by danielgomesvieira2000 | pilotwings-64-recomp-web | play |
| Banjo-Tooie | Banjo-Tooie: Recompiled by Vidanox | BanjoTooieRecompiled-web | play |
| Mario Kart 64 | MarioKart64Recomp by sonicdcer | MarioKart64Recomp-web | play |
| Star Fox 64 | Starfox64Recomp by sonicdcer | Starfox64Recomp-web | play |
| The Legend of Zelda: Ocarina of Time | OoT: Recompiled by JonB-Dev | oot-recompiled-web | play |
| Diddy Kong Racing | DKR-R by ThatGuyMcd | DKR-R-web | play |
| Jet Force Gemini | Jet Force Gemini: Recompiled by VibinToast | JetForceGemini-Recomp-web | play |
| Star Wars Episode I: Racer | racer by sp00nznet | racer-web | play |
| Pokémon Snap | Snap64Recomp by JackandBeans | Snap64Recomp-web | play |
| Rush 2: Extreme Racing USA | Rush 2 Recompiled by bryantmh | rush2recompiled-web | play |
| 1080° Snowboarding | 1080° Snowboarding: Recompiled by thebradshaw | 1080-Snowboarding-recomp-web | play |
| Perfect Dark | this project's own recomp | perfect-dark-recomp | play |
| Super Mario 64 | this project's own recomp | super-mario-64-recomp | play |
| F-Zero X | this project's own recomp | f-zero-x-recomp | play |
| Mario Party | this project's own recomp | mario-party-recomp | play |
| Mario Party 2 | this project's own recomp | mario-party-2-recomp | play |
| Mario Party 3 | this project's own recomp | mario-party-3-recomp | play |
| Super Smash Bros. | this project's own recomp | super-smash-bros-recomp | play |
| Paper Mario | this project's own recomp | paper-mario-recomp | play |
| Rayman 2 | this project's own recomp | rayman-2-recomp | play |
| Donald Duck: Goin' Quackers | this project's own recomp | donald-duck-goin-quackers-recomp | play |
This is for a recomp built on RecompFrontend that already builds for the desktop. Its dependencies stay as they are:
rt64, N64ModernRuntime and RecompFrontend remain its own submodules at the commits it pins, this repo is added
beside them, and the web build patches their checkouts with patches/. The patches change nothing in a desktop build
but fixes to bugs it has too (The forks), and the changes stay in the checkouts, uncommitted.
git submodule add https://github.com/n64hub/N64Recomp-browser lib/N64Recomp-browser
git -C lib/N64Recomp-browser checkout <the newest v0.x.0 tag> # forks.txt and the patches/ made from itFrom the recomp's folder, before building N64Recomp from its N64ModernRuntime/N64Recomp (the page needs the code
the patched one writes, step 5):
cmake -P lib/N64Recomp-browser/patches/apply.cmake lib # the folder holding rt64, N64ModernRuntime, RecompFrontendThat patches rt64 with the plume and hlslpp inside it, N64ModernRuntime with its N64Recomp, and RecompFrontend, on
top of any changes the recomp makes to them itself, and leaves one that is patched already as it is. A project kept
somewhere else is patched by name: cmake -P lib/N64Recomp-browser/patches/apply.cmake N64Recomp tools/N64Recomp.
apply.cmake's header says how to take a patch out again.
In the recomp's CMakeLists.txt, in an if (EMSCRIPTEN) block (the desktop build doesn't change):
include(lib/N64Recomp-browser/patches/apply.cmake) # patches the checkouts, if they aren't already
n64recomp_browser_apply_patches(${CMAKE_SOURCE_DIR}/lib)
add_compile_options(-pthread -fwasm-exceptions -msimd128 -mtail-call "-DXXH_HAS_INCLUDE(x)=0")
add_link_options(-pthread -fwasm-exceptions -mtail-call)
include(lib/N64Recomp-browser/frontend/build_speed.cmake) # ccache, if it is on PATH
set(RT64_PREBUILT_SHADER_LIST ${CMAKE_SOURCE_DIR}/<the game's shader list>) # required: step 4
# ... add rt64 (RT64_STATIC), N64ModernRuntime and RecompFrontend as for the desktop ...
add_subdirectory(lib/N64Recomp-browser/frontend ${CMAKE_BINARY_DIR}/web_frontend)
n64recomp_browser_frontend_page(<the recomp's executable>)
target_link_options(<the recomp's executable> PRIVATE -sINITIAL_MEMORY=2GB) # if 1536MB, frontend/'s, isn't enoughBuild with emcmake cmake ... -DRT64_NAGA_EXECUTABLE=<naga> (cargo install naga-cli), with spirv-opt on
PATH (SPIRV-Tools, apt install spirv-tools, which specializes the prebuilt shaders below), after embuilder build sdl2-mt freetype-legacysjlj once. With ccache on PATH, a new build dir, or another
checkout with its build dir at the same place in it, compiles nothing that was compiled before (frontend/build_speed.cmake
says how), and -DN64RECOMP_BROWSER_FAST_LINK=ON links a page to try something in seconds instead of minutes,
without Binaryen's whole-program optimizations (not for a page people play). The page needs a browser with WebGPU and
must be cross-origin isolated: served with Cross-Origin-Opener-Policy: same-origin and
Cross-Origin-Embedder-Policy: require-corp, or with coi_serviceworker.js beside it on a host that can't set
headers (GitHub Pages). Mods a player adds are off: the live recompiler emits machine code, which a page can't load.
A recomp's built-in mods work (step 5). BanjoRecomp-web's CMakeLists.txt and BUILDING.md are a whole port done this
way.
What changes in the game's own code (behind __EMSCRIPTEN__) and files:
- RT64's Framerate at Display and its resolution at Auto by default, so that every page presents at the display's rate
(60 a second on a 60 Hz display, 144 on a 144 Hz one), RT64 interpolating between the game's frames, and draws at
the canvas's size, whatever the recomp's desktop build defaults to. RecompFrontend's defaults are those. A recomp
with settings of its own sets them with Emscripten (Ocarina of Time's
machine_defaults, Jet Force Gemini's Frame Interpolation, with a page's saved settings moved to it once), and one that interpolates only in a profile of its own plays that profile on a page (DKR-R's Modern, in its default widescreen). The game's own frame rate setting stays as the game has it, but where a faster one plays the page better (Jet Force Gemini's Game Frame Rate 60: docs/game-forks.md, The games' own frame rates). - A window made without
SDL_WINDOW_ALLOW_HIGHDPI, so the canvas stays in CSS pixels. With it, a recomp that makes its own window gets a canvas at the device's pixels, which on a laptop at 200% draws four times the pixels and takes every click at the wrong spot: Wave Race 64's Load ROM opened nothing there (wave-race-64-recomp-web 9d3f621). - Sound through
n64recomp_browser::audio(frontend/audio_output.h:open,queue,queued_frames) in place ofSDL_QueueAudioandSDL_GetQueuedAudioSize. SDL2's port plays from the page's thread, which WebGPU's work keeps busy, and its sound breaks up there (Rush 2049's music was choppy on a laptop in Chrome). - No native file dialog (nfd: RecompFrontend asks the page; a recomp with menus of its own calls
n64recomp_browser::pick_files,frontend/file_picker.h, and gets the picked files' paths) and noSDL_INIT_HAPTIC(Emscripten's SDL2 has no haptics, andSDL_Initfails as a whole when asked for them, gamepads with it). - Nothing for a menu drawn with ImGui through RT64's inspector (DKR-R's overlay): on a page the inspector draws through plume (rt64-web 8b9dce2), with ImGui's font atlas as its only texture, so a menu that shows images of its own leaves them out there.
- Input read whatever SDL's window focus says. On a page SDL's keyboard focus follows the canvas's window focus events,
which a page opened behind another window, or driven by a script, never gets, though its tab gets the keys: DKR-R
read the controller only while its window had focus, and its page took no input (DKR-R-web's
runtime_platform.cpp). - RT64's render to RAM costs a 5-6 ms wait per display list in a browser: turn it off with
recompui::renderer::set_render_to_ram(false)if the game never reads its frames on the CPU (Banjo's frame reads are patched to sample RT64's framebuffer on the GPU, so it is off there too). Majora's Mask reads them: the Pictograph Box compresses the photo it took on the CPU, so it stays on there. Donkey Kong 64's code reads its framebuffers too (a Banana Fairy photo), and it is left on there (not tried off). The wait is per list, so it costs most where a frame is drawn in many: Mario Party 2's 4-player minigames draw one in 15-17 RSP tasks, and in Chrome (uncapped, a desktop GPU) Tile Driver made 8-9 game frames a second with the copy, 12 with it a list late (below) and the game's own 30 without it, so it is off there. - A loop that waits by polling the count (
osGetCount) or memory, with no OS call inside, holds every other game thread for as long as it spins: ultramodern runs one game thread at a time and switches only in an OS call. GoldenEye's main loop waited for its next frame that way, and its audio thread waited behind it: ~0.4 s of sound short at a time in Firefox, ~20% for ~40 s while Chrome built shaders. GoldenEye64Recomp-web'spatches/fps.cyields in each poll (recomp_yield_self_1ms). A trace of count calls a second finds such a loop. - A frame can take seconds on a page: the game waits while the browser builds the shaders it needs (Firefox's boot
waits are 5-30 s). A scheduler that takes a frame that slow for a hung RCP and goes on without it, by sending
itself the done message or resetting the RSP, then builds the next frames into display lists RT64 hasn't read yet,
and RT64 reads a mix of two frames' commands. Pilotwings 64's scheduler did that after 50 retraces, and its page
stopped soon after Firefox's boot wait (a palette load past the end of memory, a texture 8404 texels wide):
pilotwings-64-recomp-web's
patches/sched.ctakes those timeouts out. Look for a retrace handler that counts retraces while the RSP or RDP is busy. - A recomp's patches that move the display lists into a larger buffer of their own have to move every one, the boot's
first included. Mario Kart 64's pointed every task at its larger pool but left the boot's first list in the game's
pool, so RT64 read zeros on into the pool where the game was already writing the next frame, and about one boot in
twenty on the page hung or crashed. MarioKart64Recomp-web's
patches/gfx_config.cpatchesrendering_inittoo. - A runtime that holds the game thread submitting a display list until the list is drawn holds the whole game through render to RAM's readback (ultramodern runs one game thread at a time). Jet Force Gemini's (its base's, not N64ModernRuntime's) did, and with the exact copy its page lost 250,000-300,000 sound frames in 160 s. JetForceGemini-Recomp-web copies RDRAM as the game submits a list and has RT64 read the list from the copy, so the game goes on and has the RSP's completion at once, and the RDP's once the frame is drawn; render to RAM's copies land in the game's RDRAM and in the copy both (DKR-R-web's runtime reads its lists from a copy too). Running on, a scheduler that counts retraces while a task is current can then take a frame waiting for shaders for a hung RCP (above).
- Printing from the game's threads is cheap:
frontend/thread_stdio.jshands each line a thread writes to stdout or stderr to the page's console from that thread. Emscripten runs every write on the page's thread and waits for it there (its file system lives there), and that thread is WebGPU's too, so each line held the thread that printed it for as long as a frame's work. Banjo-Tooie's upstream prints ~100 lines a second: on its title screen in headless Chrome its sound was 12,900-13,400 frames short of ~2.1 million without it, 235-1,325 with it. A write to a file still goes to the page's thread. - If the game needs render to RAM but can take a frame one display list old, call
recompui::renderer::set_render_to_ram_late(true)too (rt64-web'srenderToRAMLate): a list's copy into RDRAM then lands at the first VI or list start after the GPU has it, or where the next list's draws are synced if that comes first, and no list waits for it. Only the last copy of a list is late: one that a later part of the same list reads is still made in place. In headless Chrome, render to RAM on: Conker's Bad Fur Day in Windy 10.7-11.8 ms a list against 14.8-16.4 ms with the exact copy, GoldenEye in Dam 3.8-5.2 ms against 8.0-9.7. On a phone, whose GPU takes longer over a frame than the game takes between lists, Conker's page draws 15-21% more game frames than when each list's start waited for the copy (docs/phones.md). It is wrong for a game whose CPU reads a frame it has only just drawn: Majora's Mask flushes its task queue and then reads its pictograph and its pause menu's background at once, and would get the frame before, so it keeps the exact copy. The game runs while a late copy is pending, so the copy keeps every word the CPU wrote to the framebuffer before or while it lands (rt64-web cb75c5c) and the console reports each such write: run the page through the game's levels with it and read those lines. Conker's CPU writes to framebuffers with a pending copy in every chapter, 89 times over its shader plan, and the page plays the same; GoldenEye's never does. A word the CPU writes with the value it already held looks untouched, though, and gets the frame, so it is also wrong for a game that puts code or data in a framebuffer's memory once the RDP is done: Jet Force Gemini's CPU reworks its shadows' render targets once the RDP has drawn them, and keeps the exact copy too. Conker's, GoldenEye's, Donkey Kong 64's, Perfect Dark's and F-Zero X's pages take the copy late (docs/game-forks.md). - Give RT64 a list of the game's shaders, so the page builds them before the game draws with them.
This is part of every port, not an optional extra: without it the page is unplayable on most machines. Until a
shader is built RT64 draws with its ubershader, which costs several times as much a pixel: on a laptop's Intel
graphics in Chrome that is a frame a second for minutes, and Firefox, which builds a pipeline in line with
everything else, builds none during play, so it would draw everything with it. The list is a file of shader descriptions, five hex words a line (a line
starting with
#is a comment), in sections: those above the first line readingsection <name>(one word) are the boot, and each section after it a level or a part of the game. In Chrome, which builds pipelines on threads of its own, the page builds the whole list behind the launcher and the game from when it starts, in the list's order, the boot and the section after it on half the cores and the rest one at a time, and the game waits only when a frame asks for at least 4 shaders of one section that aren't built, for those, holding its frame under a small "Preparing graphics" indicator in a corner; the rest of that section is built next. Rush 2049's page, cold, has its 75 pipelines built ~5 s after it opens, behind the launcher. Firefox waits instead, since anything built behind the game stalls its frames: for the boot before the game's first frame, and for a whole section when a frame first asks for 4 of its shaders. So a list's boot is what the game shows before its first level, and the rest goes in sections: Rush 2049's boot (logos, title, menus) is 21 pipelines and 2 s in Firefox, against 75 and 9.8 s as one list, and its race section's 41 take 7 s when the first race starts. A boot that would still be long can end at the title, with the menus the next section: Pilotwings 64's title and menus were 80 pipelines and 24-37 s in Firefox before its first frame, and its title alone is 33 and 7-9 s, the menus' 47 built after Start; Chrome builds that second section on the boot's threads too, so it doesn't wait for it. A description can be in several sections. One the list missed, or a few at a time, is built behind the game, which draws with the ubershader meanwhile; Firefox builds it before the next frame instead, the game waiting, a fraction of a second a shader. The page remembers each one the list missed, with the section the game was in (the frontend's shell keeps them inlocalStorage), and on later visits builds it with that section as if the list had it, so what a recording didn't reach costs a player one visit, not every one. SetRT64_PREBUILT_SHADER_LISTto the file before adding rt64. Each one is specialized at build time byspirv-opt(rt64-web's CMakeLists.txt says how), its pixel shader both with dual-source blending and without, and they are built largest first, as many at once as the computer has cores, each pipeline once: descriptions that come out as the same shaders and the same pipeline state share one. A line readingbackgroundalso starts a section of that name. - Record that list by code, not by playing:
tools/record_shaders.mjs(its header says how) serves a build, opens it in headless Chrome with?record-shaders, under which the page prebuilds nothing and prints anrt64 shader:line for each description the game asks for, and follows a plan of the game's own, a JSON file of steps that reach each level: keys, clicks, waits, and JavaScript that writes the game's memory. Each section is recorded from a fresh load of the page, so it has everything its level draws, and what the boot draws goes above the first section. A plan usually needs a small patch in the game that starts a level or shows a menu when the script asks, called once a frame: GoldenEye's ispatches/shader_survey.c,tools/shader_plan.jsonandtools/record_shaders.shin GoldenEye64Recomp-web. Why run the game at all: a shader is the RDP's state at a draw, the combiner and render mode, and the game's code sets much of it as it draws, so no table in the ROM lists them. Record each level's start, not the whole game: what a level draws later is mostly what its start drew, and the rest is built as it comes. Chrome keeps built pipelines for later visits in a cache of 6 MB per profile, for every site together: Rush 2049's 105, about 4 MB, are then all built in 0.4-0.7 s and the indicator never shows; a game whose list doesn't fit waits on every visit, and sections keep each wait to what one level needs. Measured in Chrome 154 on an RTX 2070 (headless): Rush 2049's list, then without sections, took 6.3-7.2 s cold on 12 cores; 6 at a time it took 10.0-10.8 s optimized byspirv-optagainst 16.1-17.7 s only specialized, and its WGSL is 5.9 MB against 17.5 MB, since many descriptions that differ only in what the shaders ignore come out the same. Rush 2049's list issrc/rt64_shaders.txtin its repo.
The rest is what a link or a run of the page shows when it is missing:
- Generate the recompiled code with the N64Recomp built from the patched
N64ModernRuntime/N64Recomp(or N64Recomp-web), and build it with-Werror=implicit-function-declaration. wasm-ld turns a call whose signature doesn't match the callee into a trap, and upstream'sfuncs.hleaves out some of what the C calls (the runtime's libultra replacements, ignored functions,recomp_trigger_event); N64Recomp-web's declares all of it, so only a hook's own calls are left to the recomp: a hook whose text calls the recomp's C declares it there (Donkey Kong 64'sload_dk64_overlay, inus.toml). A patch's call to a libultra function goes to the runtime's<name>_recomp, as the game's own calls do (N64Recomp-web 9eb59c4; upstream callsosInitialize(rdram, ctx), which reaches ultramodern'sosInitialize(void)). - A game function with a C library name clashes with the library's declaration once
funcs.hdeclares it: GoldenEye has its ownstrtol,strncmp,isalphaand six more. List each in[patches] renamedin the game's config and the patches' config, and it becomes<name>_recompin both (N64Recomp-web ec0748e renames the patches' calls to it too). N64Recomp-web also reads a jump table from the section that holds it, where upstream reads it at the function's own ROM offset: GoldenEye keeps them all in a.rodatasection of their own, so upstream's read lands past the ROM's end. - Build the recompiled code with
-mtail-call(theadd_compile_optionsabove). The MIPS code leaves a function by jumping to another, a tail call, which the C writes as a call and a return. Natively clang makes that a jump; in wasm it stays a call, so code that chains them without returning grows the stack until the browser stops the page ("Maximum call stack size exceeded"): Conker's Bad Fur Day does, minutes in. With the tail call feature, N64Recomp-web's C forces each one to areturn_call(RECOMP_TAIL_CALLinrecomp.h,musttail); clang won't make one by itself, past the early returnRECOMP_FUNC_ENTRYputs in every function. Without the flag the calls stay plain ones. - A patch calls a native (a symbol at an address in the patches'
syms.ld) as it calls a recompiled function, with(uint8_t* rdram, recomp_context* ctx), so the native has to take that. One with its own arguments, or a runtime function the patches call by its name, happens to work on x86 and traps in wasm, and only the link says so: wasm-ld'sfunction signature mismatch, whichfrontend/makes an error (--fatal-warnings). Donkey-Kong-64-Recompiled-web wrapsload_dk64_overlayand ultramodern'syield_selfindk64_load_overlayanddk64_yield_self, and GoldenEye64Recomp-webyield_self_1msinrecomp_yield_self_1ms. - The patches library's generated C is built like the recompiled code: with
-Werror=implicit-function-declaration, andfuncs.hincluded if it doesn't include it itself (Zelda64Recomp-web'sPatchesLib). - A built-in mod (
recomp::mods::register_embedded_mod) is recompiled to C at build time and linked in:n64recomp_browser_precompiled_mod(<target> <mod.nrm> <the recomp's syms.toml> <name>), withN64RECOMP_BROWSER_OFFLINE_MOD_RECOMPset to an OfflineModRecomp built from N64Recomp-web, adds the C (OfflineModRecomp--static <name>) to the target, andrecomp::mods::register_precompiled_mod(<name>)besideregister_embedded_modmakes the runtime use it for that mod. The runtime matches it by a hash of the mod's code, so another version of the mod the player installs isn't given the wrong code (it fails to load like any mod, with no live recompiler). wasm code can't be rewritten, so the functions a mod replaces or hooks are redirected at their entry instead: N64Recomp-web's C asksRECOMP_FUNC_ENTRYthere, which is one byte read for a function nothing redirects. Hooks on a function in the patches library work the same way; hooks on a built-in mod's own functions don't load (OfflineModHooked). - A mod that fails to load ends in an SDL message box, which calls
alertfrom a worker. Workers have none, so the frontend'sworker_alert.jssends it to the console as an error rather than letting the exception stop the page. - RecompFrontend includes
patches/ui_funcs.hfrom five directories aboverecompui/src/api/, so from<recomp>/patches/when it sits in<recomp>/lib/: the declaration ofrecomp_run_ui_callbacks, the patch function that runs UI callbacks (tests/build/patches/ui_funcs.his a stand-in). - RecompFrontend compiles recompui's shaders with the
DXCandDXC_*_OPTSits parent sets, as on the desktop (tests/build/CMakeLists.txtsets the ones it needs). - DXC for recompui's shaders is the build machine's, not one built for the target.
- fmt 11.1 (N64Recomp's) needs
target_compile_options(fmt PRIVATE -include cstdlib)with Emscripten's libc++. - Emscripten's
<limits.h>definesPAGESIZE, so a source that includes RecompFrontend'sslot_map.hafter it needs#undef PAGESIZE. - The page is 32-bit. Code that gets away with a length that wraps around in a 64-bit build doesn't here: Banjo's
overlay decompression gave inflate
end - startwithendbeforestart, andnext_in + avail_inwrapped, so it never finished and the page hung at boot.
The patches are made on the upstream commits in patches/patches.txt, the ones the forks are built on. On every push
CI applies them to the commits the recomps here pin (patches/pins.txt: Banjo-Kazooie, Majora's Mask, Donkey Kong 64
with Killklli's runtime and RecompFrontend, Conker's Bad Fur Day, and GoldenEye 007, whose upstream predates
RecompFrontend, so its web fork pins the commits the forks are built on), and over Diddy Kong Racing's web fork's own
patches to its runtime and rt64 (DKR-R-web's scripts/build_web.sh --patches), and builds every library of a page
from Banjo-Kazooie's. What to expect from a recomp's own commits:
- Near those: they apply. Where the code around a change differs too much for
git apply,apply.cmakemerges three ways against the commit the patch was made on, fetched from the fork, and says so (Donkey Kong 64's RecompFrontend needs that). On those very commits (Majora's Mask'srecomp-frontendbranch) every patch applies as it is, and the recomp's own code builds unchanged. - Far from them, older or newer: a patch may not apply at all.
apply.cmakethen stops and names the files that conflict. rt64's, plume's and hlslpp's newest upstream commits don't take theirs today. Moving that one submodule to a commit nearerpatches.txt's (that commit itself always works), or pinning the forks (below), gets past it. The forks merge newer upstream commits in over time (Changing it), and each tag's patches move with them. - On top of the recomp's own patches to the same lines: they conflict. The forks keep their changes off the lines a
recomp's own patch is known to touch (Conker's Bad Fur Day adds includes to rt64 beside theirs). Where both make the
same change their own way, the recomp leaves that file out, as a path in the project after its name:
cmake -P .../apply.cmake N64Recomp tools/N64Recomp src/analysis.cpp(Conker's own patch already finds a jump table in another section, which N64Recomp'ssrc/analysis.cppchange does for GoldenEye). A recomp that makes the web's change to a file left out its own way, merged by hand with its own, takes the forks' later changes to it withcmake -P .../apply.cmake rt64 lib/rt64 --since <commit> <file>..., run once its own change is in: what the web has changed in those files since this repo's<commit>, the one its merge was made from, merged three ways. Only a change that meets its own lines stops the build, for another merge by hand, which is then from the new commit. - Applying isn't building. A patch that applies can meet an API that has moved, and then the recomp's own code
changes: Banjo-Kazooie's runtime is older than the one the forks are on, and its
recomp::starttakes arguments, not arecomp::Configuration, so itsmain.cppcalls it its own way, as on the desktop. A recomp's own fork of a submodule can also want a library a page hasn't got: Donkey Kong 64's RecompFrontend (Killklli's) has a mod store that fetches with curl, found withfind_package(CURL REQUIRED). The store stays off on the page, so Donkey-Kong-64-Recompiled-web puts aFindCURL.cmakeof its own first onCMAKE_MODULE_PATHwith Emscripten, with acurl/curl.hwhose every call fails (web/curl), rather than patching the store.
A recomp can instead point its rt64, N64ModernRuntime and RecompFrontend submodules at the forks (The forks), at
the commits forks.txt names at a tag, and leave out the apply.cmake lines: the web support is then in the
submodules' history, not in their working trees. None of the games here does: it moves the recomp off upstream's
commits, and every new tag moves its submodules again.
The changes to the upstream projects are commits on the main branch of a fork of each, on top of a commit upstream
(newer upstream commits are merged in, never rebased onto). They stay forks: RT64, plume, N64Recomp, N64ModernRuntime
and RecompFrontend don't take code written with an LLM (their CONTRIBUTING.md), so none of this is offered to them.
fork upstream what the fork adds
n64hub/plume-web renderbag/plume a WebGPU backend, the display's rate timed from
animation frames, 2 frames in flight (up to 4
where the GPU reports them done late;
?presents-in-flight=<n>; ?present-stats prints
what holds presents back every 5 s);
any backend run on one thread; waits that
don't end on Windows' 15.6 ms timer tick,
as Firefox's timed waits do there
n64hub/rt64-web rt64/rt64 RT64 on WebGPU: WGSL shaders, no descriptor
indexing, background pipeline builds, frames
interpolated to the display's rate (a frame
a game draws in several display lists and
frames of uneven VIs too) and paced
evenly when the GPU falls behind, a game's
shaders specialized at build time, their
decals drawn without ending the render pass
(a depth test and the stencil); tile copies
reused from list to list (with render to RAM
off, each one was a new texture); what the
CPU writes stretched across a wide picture,
with RT64_STRETCH_CPU_CHANGES, an option a
game turns on
n64hub/N64Recomp-web N64Recomp/N64Recomp the FPU rounding mode on wasm; funcs.h
declares every function the C calls; a mod
recompiled to C ahead of time; functions a mod
replaces or hooks, redirected at their entry;
tail calls as wasm tail calls; a division by
zero gives the VR4300's results
n64hub/N64ModernRuntime-web N64Recomp/N64ModernRuntime Emscripten: no live recompiler, RDRAM
without a guard region; built-in mods linked
in, their replacements and hooks as redirects;
OSThread's context past libultra's thprof on
a 32-bit host; game timers on time where a
page's timed waits end late (Firefox on
Windows); a ROM read in 4 MB pieces; no exit
on a Status write, no thread put in a queue
twice, a sound buffer's KSEG1 address read
through KSEG0's
n64hub/RecompFrontend-web N64Recomp/RecompFrontend recompui on WebGPU, the canvas as the window,
a file picker on a thread of its own, render
to RAM as an option and its copy a display
list late as another; a game of several
players plays as one until they are
assigned, or with RECOMPINPUT_PADS_IN_ORDER,
an option it turns on, each pad its own port
n64hub/hlslpp-web redorav/hlslpp fixes to the WebAssembly SIMD backend RT64's
CPU-side matrices use with -msimd128
rt64-web's src/contrib/plume points at plume-web and its src/contrib/hlslpp at hlslpp-web, and
N64ModernRuntime-web's N64Recomp at N64Recomp-web. Every web
change is behind __EMSCRIPTEN__ or __wasm__ (or a CMake option on by default with Emscripten), so a desktop build
of the forks is the upstream one, but for the declarations N64Recomp-web adds to funcs.h and plume-web's
RenderSwapChain::isWaitReady() (always true but on WebGPU) and waitOnCaller() (always false but on WebGPU), which
change nothing there, fixes to bugs a desktop build has too (a crash, a hang, a wrong frame), which docs/forks.md
says are "on every build", and RT64_STRETCH_CPU_CHANGES, off on every build until a game turns it on. Each commit's
message says what it does and why.
patches/ holds each fork as one patch, everything its main adds to the upstream commit it is on, so a recomp can
take the web support without moving its submodules to the forks (Using it in a recomp).
forks.txt: the fork commits that work together. A tag here is a release: a recomp pins its submodules to the commitsforks.txtnames at that tag, or applies the patches at that tag.patches/: each fork atforks.txt's commit as a patch on the upstream commit it is on (patches.txt), made bytools/export_patches.sh;apply.cmake, which patches a recomp's own checkouts with them (its header says how); andpins.txt, the upstream commits recomps pin, which CI patches.frontend/: what makes a recomp built on RecompFrontend a page, as link options andn64recomp_browser_frontend_page()(recomp_web_frontend_page()works too) (its header says what each part does): threads, the page, HOME kept in IndexedDB (a database a page, a ROM stored from a worker, the browser asked to keep it), the saves synced with the player's own Google Drive once they sign in on the hub (save_sync.js), a thread's prints without waiting for the page's thread, SDL2 audio from a thread, sound played on the browser's audio thread (audio_output.h), the browser's file picker for a recomp with menus of its own (file_picker.h), the WebGPU device, a notice in Firefox on Windows' Intel graphics naming its fixes, fullscreen in landscape at a phone's first tap, an on-screen controller on a phone once the game starts (site/touch_pad.js, served from the hub's top and put into a game's frame by the hub, so it changes without a page build; it steps aside for a game that draws touch controls of its own), andcoi_serviceworker.js.renderer/: RT64 as the runtime's renderer without RecompFrontend, for a test page with no menus.tools/:fetch_forks.shchecks out the forks atforks.txt's commits (CI uses it),fetch_pins.sha recomp's own commits inpatches/pins.txt, patched, andpublish_page.shputs a recomp's built page on GitHub Pages, in its folder of the hub (n64hub), as the only commit on an orphangh-pagesbranch (its header says how);publish_site.shpublishes the games' page (site/) at the hub's top. A page holds the recompiled code, so it is built where the ROM is, never in CI.publish_pages.shis the deploy after a change: it rebuilds every game's page in the Games table from its main on a build host, publishes only those whose files differ from what Pages serves (a commit builds the same bytes in any checkout), and boots each one it published from Pages in headless Chrome;play_pages.shthen plays the pages Pages serves by their regular plans, most often in Firefox, which needs no build, so another machine can do it during a retest.timer_tick.c, linked into a test build, makes a desktop's timed waits end on Windows' default 15.6 ms tick, as Firefox's do on a laptop.record_shaders.mjsrecords a prebuilt shader list by a plan, andplay_page.mjsplays a page, a build or one on Pages, by the same kind of plan in Chrome or Firefox and measures it: presents a second, sound frames short, WebGPU errors, the shader waits, the GPU's load and the browser's cores, a line every few seconds and a summary, for a game's notes andplays.jsonl;--recordappends the run toplays.jsonl, every page play kept, a JSON line each, so how each game's page plays can be followed day by day;ab_pages.mjsplays a new build and the page Pages serves interleaved in Chrome, says whether the build plays worse, and with--smokeplays the build once in Firefox for crashes and errors (the retest's checks);retest.shruns a whole retest with them, a game at a time from build to publish.pass_probe.js, a preload for it, says what a page's presents spend the GPU on: its passes, their size, their draws by shader and the GPU time of each;webgpu_census.js, another, counts its WebGPU calls and bytes a second by kind, andgpu_growth.mjsplays those kinds one at a time in a test page,gpu_growth.html, reading Firefox's GPU process memory around each;memory_scan.ps1says whose code made what that process grows.capture_window.ps1plays a desktop build by a plan and captures its own window, andframe_log.mjsreads RT64's frame log beside those captures (docs/playing.md).boot_release.shboots a recomp's desktop release archive from a new folder, as a player first runs it, before itstools/release.shpublishes it: on Windows throughcapture_window.ps1, on Linux in a container ofboot_release.Dockerfileon software Vulkan.frame_lengths.js, a preload too, logs how long the game's frames are against the page's presents, andframe_lengths.mjscounts from that what each guess at a frame's length would hold and cut.wait_probe.jslogs whether a timed wait off the page's thread ends on time, each second of a play, andheap_probe.jshow much of its heap the page has taken, and the request that ran it out.page_driver.mjsdrives both browsers for them (each header says how), and in Chrome reports an exception on any of the page's threads with its stack, which the page itself gets only as an ErrorEvent without one.wasm_offset.mjsnames the function of a page's wasm at the offset a trap gives.tests/: the plume device thread test, hlslpp's wasm SIMD backend against plain C++, the WGSL checker, andbuild/, every library of a page built with Emscripten from the forks. CI (.github/workflows/ci.yml) runs all four.docs/: notes for whoever changes this repo, besideCLAUDE.md: what the web needs from a recomp and RT64 (porting.md), how a page is played and measured (playing.md), the retest and publishing (publishing.md), every fork commit (forks.md), what porting each game fork found (game-forks.md), and what has been measured about the pages in Chrome and Firefox (findings.md).
A change to a fork is a commit on its main branch, made on top of what is there (never a force-push: recomps and
tags pin those commits). Then bump forks.txt here to the new commit, and to the new rt64-web or
N64ModernRuntime-web commit too when the change was in plume-web, hlslpp-web or N64Recomp-web (their submodule pointer
moves with it). To move a fork to a newer upstream commit, merge that commit into main and say in the merge's message
what upstream brought, and move its upstream commit in patches/patches.txt. With every change to either, run
tools/export_patches.sh and commit the patches with it: CI checks that they match. A patch that stops applying to a
recomp's commits in patches/pins.txt means the fork has to change. Once a change to what a page is built from merges and passes CI, CI tags main with the
next release.
Licensed GPLv3 (COPYING), as the recomp it came from; the forks keep their upstream licenses.