Netcode - Recomps vs Decomps

I started building rollback netcode, and it wasn't my first time building netcode in general, and I had researched rollback mechanics and techniques before but started building it for real in the decompilation PC port of Smash Bros 64, Battleship.

I learned a lot about how native games handle netcode, and how to control the sim cadence between peers with lockstep techniques, and sync verification by hashing objects in the game.

However I also learned there are a lot of deep complications to establishing this kind of netcode in a full decompilation port which do not apply to static recompilations, and in fact, I was able to advance my work on a modular and scalable rollback netcode library for decomps/recomps by porting my work out of Battleship and into PSXrecomp.

Full Decompilations

Full decompilations have translated and named every function in C source code to recreate the exact behaviours of the source material, then to build a usable PC port we fork that decompilation and leverage it with other pieces of software to process that code, render it, manage inputs, and so on. The appeal of this is the ability to freely modify that code and modify, or create entirely new functions and behaviours, inject assets, etc, and extend the original software.

This has a deep appeal for creating unique netplay functionalities, we can very easily inject new ingame menus, and tie in connectivity behaviours to initialize native netplay experiences into the game, by connecting players to a server or mesh of server nodes for peer discovery.

In a full decompilation port, we own the entire game loop very freely, and we can build specific netplay experiences into the software directly, and easily, but most of all we are able to run the software fully outside the bounds of a virtual environment (like an emulator does) and since we are not simulating a virtual environment, and we own the entire game loop, we can create powerful and extendable netplay experiences, like Super Mario 64 Coop with 30 player multiplayer, because they can create unique architectures and mechanics for validating and maintaining game synchronization.

This comes at a steep cost in development time however, and the actual work of validating synchronization across an entire game, especially one that was never designed from the ground up for netplay of any sort, can involve a significant amount of work, testing, and debugging sync behaviour with different objects, animations, object states, physics, etc.

Static Recompilations

Static recompilations typically use Ghidra and other tools with scripting to generate C source code from a given piece of software, and PSXrecomp provides tools for generating C code based on disc images of games for the PSX. The result is not human readable, and is basically a large collection of magic numbers that just happens to work as a source code that reproduces the original software. The issue with that is that you can't own the game loop, you can't reliably hook up native and modern software and libraries directly to the source code to run it freely, or own the game loop, the way you can in a full decomp.

Out of necessity, you can only run this software via a runtime environment, a virtual environment that functions effectively like an emulator. An emulator however, runs binaries designed for the hardware of another system directly, while a recomp runtime engine simulates just enough of the original hardware as a virtual environment to run the recompiled source code directly and natively. There is a distinction here in what the technology allows you to actually do.

Recompilation runtimes have the benefits of an emulator, where the virtualized environment allows snapshotting the entire game in a single, simple game state - which is used for save states, because it captures the full RAM, and VRAM context in one file. This means that even though we can't own the full game loop, and can't reliably build custom synchronization and hashing functions directly into the game's source code extending it into a full netplay game, we can snapshot and hash the entire gamestate itself for sync validation and enforcement. We can also leverage the snapshot ring used in rollback netcode, to create other emulation features, like rewind, and run-ahead (which is basically rollback offline, in order to reduce input latency).

But because we do have source code access, albeit it's comparable to running in a mineshaft with no lights to start, we can work towards decoding the machine script generated source code into more readable function names. We can create an index against the "magic number" source code using human readable names and descriptions, and leverage it for decompilation and source code modding. This allows creating unique behaviors in the games, and injecting new assets, behaviours, mapping new controls, just about anything a decomp port can do - except still being bound to the virtual environment of the recomp runtime.

This all means that recompilations have a MUCH easier time implementing netplay behaviors. We can hook the netplay functionality into the runtime engine itself, and use game states for sync validation and enforcement. This means we can create scalable netcode via the runtime engine on heavily modded game titles by reusing the same runtime engine and game state techniques on every title that runs in the engine.

The restriction here, is that we cannot extend the netplay experience too far beyond the recompilation runtime environment. The runtime environment has a limited number of virtual resources (RAM, VRAM) available to it, and we cannot easily break those limits. And keeping in mind the goal here of building scalability in netpcode for the entire ecosystem the runtime engine powers, we must maintain synchronization between all peers RAM and VRAM.

That doesn't mean we have to present the same things to each peer however. We can modify the source code to load and keep things in the RAM/VRAM and keep them there, and we can use flags to control the presentation of things to the users. So if we have enough room in the environment to maintain the game states of two or four split screen players in a single game, then we can keep all that in the RAM for each remote peer - while using conditional flags to control the presentation of it. We can create "splitless" screen multiplayer experiences, with even just a little bit of effort decoding and making sense of the source code functions.

Metal Warriors (SNES)

Screenshot_20260824_175821.png Here we have Metal Warriors, a cult classic SNES mech action/fighting game. Gameplay screen shot seen above, is the native gameplay experience. Split screen, with a very slim resolution per player. On a large map, with simple pointers to help direct players to find their opponenets in the map.

Here we have some examples, of a netplay experience with splitless screen multiplayer, specifically within certain scenes of the game.

Screenshot_20260824_175914.png A unified display at the title screens, character selects, etc.

Screenshot_20260824_175943.png When games begin, the game entire state is available to all players, but presentation of the game is split, and also widescreen is implemented as well by extending the range of objects being loaded and kept in the VRAM

Screenshot_20260824_180006.png New custom arrows exist along a slider at the top of the screen to present additional information that isn't available when looking at the other persons screen in combination with the simple pointers, giving relative distance/positioning, and height difference by showing the arrow larger + brighter, or smaller and darker for above or below in addition to how far left/right they are relative to the player position.

Because we can decode the source code and use mods as runtime code changes, we can create behaviors like this in the game. We could even add more players, new characters, game mechanics, anything a decomp port can do -- except within the limitations of the runtime engine, and the nature of game state netcode.

This means while we can build scalable rollback netcode across an entire platform of titles, we're stuck manipulating entire gamestates in the process, and exclusively not just individual objects that matter to the synchronicity, and there is not a real need or great advantage for gameplay with a central authoritative server for sync either, but a relay server for sending and validating legal inputs does have advantages for anticheat and network architecture simplification for lobby hosts.

The biggest restriction of this runtime environment with modified source code on netplay, is that truly deep netplay customization has limits, because of how everything must be captured in the VRAM and RAM of each player's virtual environment, it's essentially still spoofing local multiplayer with input-only network transmissions. So for example, if the game's runtime environment can only handle and support 2-4 players, that's going to be the effective max we can do here, because we often already have to extend what we load and keep in the VRAM/RAM already with mods like these. With a decomp port we can move way beyond that limit, because we can move off relying on game state hash verification and enforcement, to my granulated hashes of specific things to validate and enforce game states. Meaning we can have 30 players in Mario64, all in different levels, all doing their own thing, but in a recomp port, we would be more limited to everyone being in the same level, and probably having a struggle to implement even a small handful of players in a reasonable experience.

The distinct advantage here in a recomp is not the flexibility, but the scalability, since you can create a very reasonable and accepatble ideal network experience for gameplay here within the recomp runtime basically for free, since you're just leveraging the runtime environment's advantages. But for games and experiences that require something much greater and more integrated, then a decomp port is required. However, our work on static recomps, modding and decoding them, can contribute to the progress of decompilations, and over time, these restricted, "simple" easy win implementations can actually evolve, and migrate eventually out of the runtime environment into a fully ported environment.