No-computation "RNG" by baking random numbers into the ROM

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
No-computation "RNG" by baking random numbers into the ROM
by on (#148496)
Newbie to SNES assembly, so pardon me if this is already well-known but: I wanted some random bytes for my game, but didn't really want to waste valuable CPU time with computing random bytes at runtime using an actual PRNG algorithm. I'm sharing my trick here in case it's helpful for others.

The gist is that I fill a ROM bank with random bytes using the assembler, and then call a GetRandomByte macro that fills A with the next random byte, and updates a pointer for the next time GetRandomByte is called. The code snippet is here:

Code:
.define randomBytePtr $1A  ; Pick any free 2-byte address in your game's RAM.

; Stores result to A.
; Assumes 16-bit X & 8-bit A.
; Modifies X.
; Updates randomBytePtr.
.MACRO GetRandomByte
    ldx randomBytePtr
    lda $028000, X  ; $028000: beginning of ROM bank 2.
    inx
    cpx #$8000  ; This is the size of the entire ROM bank.
    bne +
    ldx #0
+
    stx randomBytePtr
.ENDM

; Fill an entire bank with random numbers.
.SEED 1
.BANK 2 SLOT 0
.ORG 0
.SECTION "RandomBytes"
.DBRND 32 * 1024, 0, 255
.ENDS


This "wastes" an entire bank by filling it with random numbers, but I wasn't using that bank anyways (and if you wanted, you could easily change the code so that it only used 1024 bytes, or whatever.) You could also change it to be a subroutine instead of a macro, if you care more than I do about code size. :)
Re: No-computation "RNG" by baking random numbers into the R
by on (#148502)
mcmillen wrote:
I wanted some random bytes for my game, but didn't really want to waste valuable CPU time with computing random bytes at runtime using an actual PRNG algorithm.

How many random bytes do you need in a frame? Greg Cook's CRC16 is a 16-bit linear feedback shift register that takes about 70 cycles to produce 8 pseudorandom bits.
Re: No-computation "RNG" by baking random numbers into the R
by on (#148505)
Thanks! I'll save that for the future :)

I definitely don't need many bytes per frame, but my main concerns here were: 1) easy to understand and 2) given that I don't know how much CPU I'll need, no need to be wasteful about it now, whereas I can't currently imagine running out of memory :)
Re: No-computation "RNG" by baking random numbers into the R
by on (#148506)
I guess on the NES it's different, as there's a culture of making all your code fit into 32K.
Re: No-computation "RNG" by baking random numbers into the R
by on (#148511)
The problem I see with your approach is that it could lead to things seeming very deterministic and not-random. Since you will always pull out the same numbers in the same order, the only way you'll get any variation in your game is by changing the order in which things request random numbers, which I can think of many situations where that wouldn't happen leaving you with a scripted fight that goes the exact same way every time.

The only game whose RNG calculation I can speak to is Dragon View, and it does it like this:
Code:
8187be jsl $80e3bc   ;Subroutine assumes already in 8-bit A mode
80e3bc phb
80e3bd lda #$80
80e3bf pha
80e3c0 plb      ;Switch to bank $80
80e3c1 lda $2137   ;Latch screen draw pixel position
80e3c4 lda $213c
80e3c7 pha
80e3c8 lda $213c   ;double read as it has a first/second-read flip-flop system (in $213F)
80e3cb pla      ;second read is discarded
80e3cc adc $a5
80e3ce sta $a5   ;$A5 is the RNG variable.  So add in the lo byte of horizontal scanline location
80e3d0 lda $213d
80e3d3 pha
80e3d4 lda $213d   ;same double-read business as $213C
80e3d7 bit #$01   ;test highest bit ($213C and $213D are allegedly 9 bits long)
80e3d9 bne _dontAddVerticalByte
80e3db pla
80e3dc adc $a5
80e3de sta $a5   ;add in the lo byte of vertical scanline position IFF hi byte bit0 is not set.
80e3e0 bra _doneRNGCalc

_dontAddVerticalByte:
80e3e2 pla

_doneRNGCalc:
80e3e3 lda $a5
80e3e5 plb
80e3e6 rtl

Doesn't seem like the fastest code I've ever seen but it'll definitely make things nice and random provided your RNG routine isn't being called first thing in a frame or at a totally deterministic time. Generally speaking, if your joypad routine comes before your RNG calls just what the player is pressing at that moment should ensure enough randomness in the screen position by the RNG call. And since every time it adds into the previous number, it becomes basically impossible to get stuck in a situation where you keep seeing the same reactions.

Your proposal just kinda strikes me as a glorified version of this, and while I can see the motivation for and the advantages of that method, I just don't feel like it'd be "random" enough...
Re: No-computation "RNG" by baking random numbers into the R
by on (#148517)
tepples wrote:
I guess on the NES it's different, as there's a culture of making all your code fit into 32K.

Probably because of mappers in case you want to get it on a real cartridge.

And yeah, if you just need something that looks random (without really having to be random, e.g. stuff for some quick effects), a look-up table is probably easier to implement than an actual PRNG. So you lose memory in favor of making it easier and faster to do.
Re: No-computation "RNG" by baking random numbers into the R
by on (#148521)
In one demo I made for Game Boy Advance, I stored a permutation of the numbers 0 to 255 in ROM, derived from the S-box of the AES cipher. I used it to compute horizontal offsets for one of the raster effects, where the display would fuzz out (like a bad analog TV connection) and then come back into focus. Because it was called for every scanline 30 times a second on a 16.8 MHz ARM that was already running other distortion effects and animation logic, it had to be wicked fast. So I used the "ordinary" RNG once per frame to find an 8-bit starting offset into this table and then stepped through one line of the table per GBA scanline.
Re: No-computation "RNG" by baking random numbers into the R
by on (#148536)
If space isn't an issue, your table-based PRNG is at least as good as the PRNG used to generate the table, and close to optimally fast, I suppose. There's nothing at all wrong with it as a technique, as long as the space comes "free".

If it's not, a single tick of an LFSR PRNG would take a similar amount of cycles and lines of code as your lookup, and requires no lookup table. To get good entropy, you should really tick an LFSR once per bit needed before using the result, but even a single tick provides a lower-entropy random number that in a lot of cases may be very usable. (Lets you trade speed for entropy on a case-by-case basis.)
Re: No-computation "RNG" by baking random numbers into the R
by on (#148540)
rainwarrior wrote:
To get good entropy, you should really tick an LFSR once per bit needed before using the result

Agreed. That's why I use Greg Cook's code, which is equivalent to eight ticks per call. Before I found that, I had used an LFSR where I passed the number of ticks in Y.
Re: No-computation "RNG" by baking random numbers into the R
by on (#148548)
Khaz wrote:
The problem I see with your approach is that it could lead to things seeming very deterministic and not-random. Since you will always pull out the same numbers in the same order, the only way you'll get any variation in your game is by changing the order in which things request random numbers


This depends on the application -- for something that "eats" a different number of random bytes per-frame depending on player action, it'll only appear deterministic as long as the player keeps doing the same input on the same frames. For something less twitch-based (like an RPG or interactive fiction, where every instance of "randomness eaten" is easy to control), that'd be more of a concern. Fact of the matter is, all PRNGs have to be seeded somehow, and either they have a static seed (like this does), and produce the same bytes in the same order, or set the initial seed (or periodically update the internal state) based on some unpredictable signal such as a timer, which you could also do here.

For example, you could also set the initial "next random byte" pointer based on the numVBlanks count at the time the player clicks 'start game'"... which might also be deterministic if they get really good at pressing Start on the first possible frame, and reset the system after every play.. but if they're that obsessed about exploiting my RNG, I think I should just be happy that they're playing my game so passionately :)
Re: No-computation "RNG" by baking random numbers into the R
by on (#148551)
Well, all PRNG create a repeating series. That's why they're called PRNG and not RNG.

You can reduce determinism, though, by allowing the game to tick the PRNG in response to user input that is expected to be unpredictable. Typically this is triggered by events that are difficult for a human to time. For example, when a user presses A to make a choice in a menu, you could tick the PRNG every frame while the button is held. Will they hold it for 4 frames, or 10, or 80?

A TAS can always exploit this, but exploiting PRNGs is a big part of TAS.

Final Fantasy IV on the GBA tried to seed its PRNG based on how long you waited on the title screen after reset. Unfortunately, they seeded it with the number of seconds, rather than the number of frames, allowing me to easily exploit it without any special tools.
Re: No-computation "RNG" by baking random numbers into the R
by on (#148559)
On the Mega Drive you can avert it in two ways: the initial values in RAM are undefined because it's DRAM (they're defined on soft reset but unpredictable enough to still be useful), and the initial value of the HV counter (the beam can start at any point, and on soft reset the reset could have happened at any point as well). Both are useful for determining an initial seed, and the latter can also be used to turn the PRNG into an actual RNG (since you really can't tell for sure what the HV counter will be even for code that runs about the same amount of cycles).

Does the SNES have similar stuff?
Re: No-computation "RNG" by baking random numbers into the R
by on (#148560)
The SNES' H/V counter at reset is deterministic.

The only really random output I know of is caused by a counter glitch in the SNES SMP timers, which was fixed for the SNES Jr.

Best bet is probably to upload a 64KB SMP program and then clock the H/V counters. Different oscillators fluctuate based on age, temperature, cosmic rays (probably not, but...), etc. So this is a good source of generating a randomized seed.

Another easy trick is to store the LFSR in SRAM, if it's available.
Re: No-computation "RNG" by baking random numbers into the R
by on (#148566)
rainwarrior wrote:
Well, all PRNG create a repeating series. That's why they're called PRNG and not RNG.

You can reduce determinism, though, by allowing the game to tick the PRNG in response to user input that is expected to be unpredictable. Typically this is triggered by events that are difficult for a human to time. For example, when a user presses A to make a choice in a menu, you could tick the PRNG every frame while the button is held. Will they hold it for 4 frames, or 10, or 80?

A TAS can always exploit this, but exploiting PRNGs is a big part of TAS.

Final Fantasy IV on the GBA tried to seed its PRNG based on how long you waited on the title screen after reset. Unfortunately, they seeded it with the number of seconds, rather than the number of frames, allowing me to easily exploit it without any special tools.

For a more in-depth treatment of PRNG-defeats, see here, with more articles linked at the end.

On the subject of human manipulation, title-screen variance can be easy if the programmers allow buffering or simply holding the button to count. Waiting for an edge (or pair of edges) in buttons-pressed makes it harder to time an action correctly than just waiting for a valid is-pressed.
Re: No-computation "RNG" by baking random numbers into the R
by on (#148571)
My suggestion was not just for the title screen or startup. You can collect entropy from user input nearly any time they have to interact with the game. It's not just important to start with a random seed, but to break the predictability of the sequence once begun.

Techniques like reading RAM on startup are often only good for an initial seeding, and useless on emulators, but user input is an endless fountain.
Re: No-computation "RNG" by baking random numbers into the R
by on (#148577)
byuu wrote:
The SNES' H/V counter at reset is deterministic.

So the S-CPU and the S-PPU are synchronized? (otherwise where would be at least a bit of variance)

Is RAM still undefined on power on or is it always filled with FF or what? Same question for video RAM, for that matter (you can read back from it during blanking, right?).
Re: No-computation "RNG" by baking random numbers into the R
by on (#148580)
In the previous topic about the Pretendo demo, we found a couple entropy sources in the NES to set the PRNG's seed. One is readback of uninitialized OAM, which is 27*64=1728 bits of DRAM, but that doesn't work reliably on pre-2C02G PPUs. Another is the analog result of bus conflicts inside the PPU when trying to read back from VRAM while rendering is turned on, which is what Pretendo uses. I don't know which also apply to the Super NES.

Another that will probably apply more cleanly to the Super NES is reading the controller dozens of times per frame. This can be done "in the background" with a scanline interrupt, such as the NES's MMC3 IRQ or the Super NES's VTIME IRQ, with a programmable cycle timer such as the NES's FME-7 IRQ, or by (ab)using the NES DMC as a timer. The latter produces a 4.2 kHz timebase (roughly one tick per 3.8 lines) during which player 1 A and Start can be read in a DMC IRQ handler. Fortunately, autoreading isn't the only way to read a Super NES controller, as bit banging ultraslow ports $4016/$4017 still works the same way as it did on the NES.
Re: No-computation "RNG" by baking random numbers into the R
by on (#148640)
Tecmo super bowl generates 3 psuedo-random numbers every frame by using prime numbers. It stores them in 3B,3C,3D. It adds the prime number to itself once a frame.

Code:
update_randoms:

$3B= random 1
$3C= random 2
$3D= random 3

LDA *$3B                    ; random1 += 131
CLC                           
ADC #$83                   
STA *$3B                     
LDA  *3C                    ; random2 += 13
ADC #$0D
STA *3C
LDA  *3D                   ; random 3 += 17 
ADC #$11
STA  *3D
RTS


There are also functions to update each random number individually as well as other routines to make the number "more random" since sometimes you need to use multiple random numbers in the same frame.


Code:
L_D8F7 (MORE RANDOM)
   JSR update_randoms
   LDA *$3B
   AND #$03
   BEQ @Loop3
   CMP #$01
   BEQ @Loop2
   CMP ##$02
   BEQ @Loop1
@Loop0:
   LDA *$3D
   RTS
@Loop1:
   LDA *$3C
   RTS
@Loop2:
   LDA *$3D
   CLC
   ADC *$3C
   RTS
@Loop3:
   LDA *$3D
   CLC
   ADC  *$3C
   ADC *$3B
   RTS



Code:
L_12_9F83:      (this routine is used often in the part of the game that simulates game scores and season stats in a skip mode)            
   LDA $3B                  
   ADC $3B                  
   CLC                  
   ADC $3B                     
   CLC                  
   ADC #$11               
   STA $3B                  
   LDA $3C                  
   ADC $3C                  
   CLC                  
   ADC $3C                  
   CLC                  
   ADC $3C                  
   CLC                  
   ADC $3C                  
   CLC                  
   ADC #$13               
   STA $3C                  
   LDA $3D                  
   ADC $3D                  
   CLC                  
   ADC $3D                  
   CLC                  
   ADC $3D                  
   CLC                  
   ADC $3D                  
   CLC                  
   ADC $3D                  
   CLC                  
   ADC $3D                  
   CLC                  
   ADC #$AD               
   STA $3D               
   RTS