Technical Overview
This document explains how the hack is built, for anyone who wants to understand or extend it.
Boot hook
The reset vector is redirected to custom code. That code first replays the original game's own boot-init bytes verbatim (CPU native-mode/stack/direct-page setup, the original PPU BG-mode/tilemap/CHR register writes, both of the game's own original init subroutine calls), then continues into the menu instead of jumping straight to the title screen. Because this runs before the title screen exists at all, there's no interaction with any other in-game screen - the menu owns the whole frame until "START GAME" hands control back to the untouched vanilla boot flow at the exact point it would normally have continued.
Menu rendering
The menu is drawn on the BG3 background layer, which is unused by the game at this point in boot. A small custom font (one tile per character, reusing the same tile format as the game's own shop-menu font) is DMA'd into VRAM, along with several palettes: normal text, a highlighted cursor color, and colors sampled directly from the real title screen's own art (used for the animated "ULTIMATE" title lettering). The whole 32×32 BG3 tilemap is explicitly cleared at boot - the original boot path never touches BG3 at all, so any register or VRAM region it doesn't set up itself has to be initialized from scratch.
Menu state (which row the cursor is on, each cheat toggle, the chosen track/direction/laps, per-row highlight color) lives in a dedicated block of WRAM (bank $7F) that vanilla code never reads or writes, so there's no risk of colliding with anything the game itself uses.
Persistent effects (NMI hook)
The same code also installs itself at both NMI vectors (native and emulation mode) for the lifetime of the ROM. Before "START GAME" is pressed, the NMI hook's only job is signalling the menu loop that a new frame has started. After "START GAME", every single frame it:
- Re-writes the player's money/nitro/stat values if their respective toggle is on, so spending or losing them in-game has no lasting effect - they're restored right back on the next frame.
- Applies the direction/track/lap configuration chosen in the menu.
- Writes the finish threshold for the chosen race length, and holds the player's lap counter on the final lap if "LAST LAP" is on.
- Patches the race HUD's border tiles in video memory, so the second lap digit has somewhere to show through - but only while a race is actually on screen.
- Counts the race clock past the point where the game's own timer stops.
The last three are described in their own sections below. Note that the two heavier pieces of work - producing both lap digits, and building the timer readout - are deliberately not done from here; they run inside the game's own render path instead, for reasons covered under the lap counter.
Race length (LAPS) and the finish condition
The vanilla game checks for a race finish with a hardcoded comparison against a fixed lap count, duplicated at two separate points in its own code. That comparison is patched to instead read a WRAM byte that the menu sets from whatever LAPS value was chosen, so any race length from 01 to 99 laps finishes correctly - not just the original hardcoded length.
Finishing an N-lap race requires the game's internal lap-position counter to actually reach N+1 (so a 4-lap race's true finish value is 5). That is what makes the win and placement logic fire, and it is left completely untouched - the counter is never frozen or clamped for game-logic purposes. Only what is drawn is adjusted, and since the readout now carries two digits it reads correctly for every race length the menu offers. At the maximum setting of 99 the true finish value is 100, which is three digits, so the readout shows 00 for that single instant before the race ends. That is a deliberate choice rather than an oversight: a third digit would not fit the panel, and the moment it appears the race is already over.
Freezing the last lap
Turning on "LAST LAP" stops the lap-position counter from advancing past whatever LAPS is currently set to, so the race behaves as if the player is permanently on the final lap - this works correctly at any race length from 01 to 99, not just the default.
One detail matters for correctness rather than display. Crossing the finish line trades a checkpoint-progress reset for a lap increment, and the final standings are sorted on lap count first and checkpoint progress second. Holding the lap counter down across that moment would keep the reset but cancel the increment, which sorts the player last and turns a win into a loss. The freeze is therefore gated on the game's own "this race has been decided" flag, so it stops the instant the race is settled and the game's untouched placement logic sees exactly the values it expects.
Two-digit lap counter
Two separate limits apply here: 4 is the gameplay limit - every vanilla race is four laps, and it is over the moment a fifth would start - and 9 is the rendering limit, because the vanilla HUD reads each racer's lap counter and draws it as a single digit. The one per-frame routine that performs that read is replaced. The replacement splits the raw counter into tens and ones with a short repeated-subtraction divide and writes both digits in the same call, deriving the ones digit from the remainder the divide has already produced.
Doing it in that one routine is deliberate. An earlier design called out to this code from the NMI handler instead, and that was confirmed on hardware to cause universal graphical corruption and broken input from the very first screen - traced to the call itself rather than to anything it did. Everything now happens inline in the render path, with no NMI involvement at all.
The tens digit also needs somewhere to be visible: the HUD panel's border artwork sits in front of it. Transparent windows are cut into copies of those border tiles and written into video memory while a race is on screen, on a DMA channel the game is not using at that moment. Whether to write them is gated by a keepalive byte that the digit routine refreshes every frame it runs. Leaving the race, or a start or finish animation taking over the screen, simply stops those calls; the byte decays to zero and the patching stops by itself, so nothing is written over a screen that is not the race HUD.
Race timer beyond 99.9 seconds
The vanilla race clock is total race time, not per-lap, and it saturates. Its storage is two bytes: one holding tens of seconds, the other a packed value carrying ones of seconds and tenths. Both stop at their maximum and the display sits frozen at 99.9 for the remainder of the race. A full sweep of work RAM across a long race found no other byte still increasing after the display froze, so there is no larger hidden value to read - the counter genuinely saturates, and any overflow has to be counted separately.
The hack latches the moment saturation begins, seeds its own counter at 99 so the number already on screen simply continues, and counts frames from there at 60 per second. The latch also makes the whole thing self-correcting: any frame on which the game is not saturated resets it, so a new race or a restart needs no special handling. The seed is offset by the six frames that remain between 99.9 and the hundredth second, because starting from zero made the switch to "100" arrive a full second late.
The readout becomes three whole-second digits, counting up to 999 seconds - that is 16 minutes and 39 seconds, longer than a 99-lap race takes to drive. Vanilla's timer row already contains four sprites: tens of seconds, ones of seconds, the decimal point, and tenths. The decimal point being a sprite of its own is what makes this cheap, because three digits can reuse those existing slots and none has to be created. Their positions are computed relative to the game's own live sprite data rather than from a fixed table, because the panel's origin moves from track to track while its internal spacing does not.
Two alternatives were built and rejected on hardware. A four-digit version left a visible transparent gap on the left when the leading digit was blank, and its rightmost digit was clipped by the panel frame. A minutes-and-seconds format was rejected because the dot would have meant two different things either side of the switch - a decimal point in "99.9" and a minute separator in "1.40" - so the number would appear to shrink and read as though the clock had reset.
The flagman's final-lap check
A short routine runs on every lap increment, from both of the places the game detects a finish-line crossing, and it starts the tower marshal's flag-wave animation when the crossing racer reaches the final lap. It decides that with a hardcoded comparison against the original game's own race length, and it holds the only write in the ROM that starts that animation.
Because this hack makes the race length adjustable, that constant became wrong: at longer race lengths every lap from the fourth onward satisfied it, so the flag was waved at every crossing for the rest of the race. The routine now compares against the live race length instead, one below the finish threshold the menu already sets, so the wave happens on the real final lap. At the original race length the comparison resolves to exactly the value that was hardcoded before, so the behaviour there is unchanged.
The corrected routine did not fit in the space the original occupied, so the original body is replaced by a call out to the free ROM area where the rest of the injected code lives, and the vacated bytes are filled with no-ops. The routine's last byte is deliberately left as the original return instruction, so that even an entry point the analysis did not find would return harmlessly instead of running into the animation code that follows it.
Racer names
The game keeps its four driver names in a single twelve-byte table that is copied wholesale into work RAM during race setup, three characters per driver, so a driver's name is simply its index into that block. Two of its four entries are rewritten in place, so in the patched ROM the table reads LND, ALP, M.T, I.S: entry 3 was M.T and becomes I.S, and entry 2 was JON and becomes M.T.
The table is not the roster you see on screen. Entries 0 and 1 are the two human-capable slots, so name entry overwrites them - LND and ALP are only defaults sitting in ROM, replaced by whatever the players type or by --- for a slot nobody claims. Entries 2 and 3 are the two slots that are always computer-controlled, and they are the only ones that appear exactly as written. That is precisely why both real drivers were put there: those two trucks are on the grid in every single race. Read live from work RAM during a one-player race, the four fields hold the player's own name, R.G, M.T and I.S.
The third change is the routine that names an unclaimed human slot, which writes R.G. Its rule is that a field which is still all dashes gets the name, but only if the other field is not also all dashes. That distinction matters: before name entry both fields are dashes and must be left alone, whereas afterwards exactly the slot nobody claimed is dashes - which is the case worth naming. It works whichever slot the human used, and does nothing at all when two players have both entered names, since then there is no unused slot. It is called once per race from the race-start code, in main-loop context, because a long call out to this bank from the NMI handler is one of the things confirmed fatal on hardware.
That routine's name cannot be chosen freely. The original game has a documented behaviour where a computer driver races much slower if the player enters the same initials as that driver, and this routine writes into the very work-RAM fields a player's entered initials occupy. So the filler name must never match a computer driver's name, or the patch would quietly cripple that truck. With the two real drivers now permanently on entries 2 and 3, both M.T and I.S are ruled out; R.G collides with neither, and the build asserts this rather than trusting it.
All three edits are the same length as what they replace: three characters for three characters in the table, and immediate operands for immediate operands in the routine. Nothing moves and no free space is consumed.
The period is worth a note for anyone editing these names. The game's own name font is not the font this hack appends for its menu, and the two disagree on where the punctuation lives: the character index that is a period in the added font is an exclamation mark in the game's own. Using the wrong one renders I!S. The correct index was confirmed by locating the game's font in the ROM, rendering the characters around it, and cross-checking them against the game's own existing text.
Track and direction selection
The vanilla game picks each race's track and direction by indexing a fixed 64-entry table with a single WRAM race counter that increments once per race and wraps every 64 races. Selecting a track and direction in the menu overrides the exact point where the game consumes that counter to start a race, computing the correct table index for the chosen combination before handing off to the game's own unmodified race-start logic - so the race that plays out afterward is identical in every other respect to a normal one. A small number of tracks have no reverse layout in the original game; selecting REVERSE on one of those has no effect, matching how the game already treats them.
Community-documented track data (reference)
Separately from this hack's own code, the vanilla SNES version's track-selection internals - the same 64-race cycle referenced above - have been documented by speedrunning community member KungFuFurby. All of it is reproduced here for reference, all addresses and offsets referring to the US ROM of Super Off Road:
| Address | What it is |
|---|---|
$7E001C | WRAM race counter - increments once per race, wraps every 64 races (counter & $3F) |
$008216 | ROM track-ID array, 64 bytes (one per race index) - bit 7 set (OR $80) means that race is driven in reverse |
$00C984 | ROM hay-bale hazard pointer array - an entry pointing to $CA04 means no hay bales are generated for that race |
The 64-entry track-ID array at $008216, in race order (race 1 → 64):
04 06 02 01 8A 86 82 80 05 01 03 00 84 07 05 8B 87 0A 03 8C 08 02 09 0B 00 0E 0C 86 0D 87 81 88 09 0F 83 0E 04 0A 0B 0F 85 80 08 0D 07 0C 8E 06 08 82 8F 8B 84 81 8D 83 88 8A 85 00 8E 8D 8C 8F
Each track ID, its debut race, and its reverse-direction variant's debut race:
| ID | Track | Debuts (normal) | Debuts (reverse) |
|---|---|---|---|
$00 | Blaster | Race 12 | Race 8 |
$01 | Big Dukes | Race 4 | Race 31 |
$02 | Sidewinder | Race 3 | Race 7 |
$03 | Cliff Hanger | Race 11 | Race 35 |
$04 | Fandango | Race 1 | Race 13 |
$05 | Hurricane Gulch | Race 9 | Race 41 |
$06 | Huevos Grande | Race 2 | Race 6 |
$07 | Wipeout | Race 14 | Race 17 |
$08 | Cuttoff Pass | Race 21 | Race 32 |
$09 | Boulder Hill | Race 23 | never used in the 64-race cycle |
$0A | Redoubt-about | Race 18 | Race 5 |
$0B | Rio Trio | Race 24 | Race 16 |
$0C | Leapin' Lizards | Race 27 | Race 20 |
$0D | Shortcut | Race 29 | Race 55 |
$0E | Pig Bog | Race 26 | Race 47 |
$0F | Volcano Valley | Race 34 | Race 51 |
Hay bales (course hazards, driven by the same race counter via the $00C984 pointer array) appear on races: 3, 6, 10, 12, 15, 17, 19, 24, 27, 31, 33, 36-38, 40, 42, 44, 45, 48-50, 52, 53, 55-61, 63, and 64.
How to spot the wraparound: you'll know the 64-race cycle has reset back to race 1 when Fandango is the next course driven after Volcano Valley in the opposite direction with hay bales present. Volcano Valley is driven four times total across the cycle - twice in each direction, twice with hay bales.
Source: KungFuFurby, "Super Off Road wraparound" forum thread on speedrun.com.