Tetris Adding SRAM Saving Questions

Discussion of programming and development for the original Game Boy and Game Boy Color.
suFami
Posts: 16
Joined: Sat Aug 06, 2016 10:22 pm

Tetris Adding SRAM Saving Questions

Post by suFami »

Hey GBDev, for a while now I've been wanting to try and add SRAM saving to the original Gameboy version of Tetris, but have been reluctant to try since I've never added SRAM to a game before. I've done some reading on the Gameboy's dev side and have a few questions before I attempt to try it. I've dabbled in ASM for the SNES, but even that is at a beginner's level. Also, I'm aware that Tetris DX already has saving but it is very different to the original game (easier to get points, different algorithim, etc.).

A user at Romhacking.net, Capaneus, also wanted to add saving and has already done much of the hard debugging work locating where the high scores are read/written. He said he modified the cart type to include RAM and gave the game "8Kb of RAM (0x03)". I'm not sure what the 0x03 means?

He then modified the reads/writes for the high scores that were going to $D654 (A-Type high scores) and $D000 (B-Type High Scores) to write to $A654 and $A600 respectively.

Most GB games with RAM use MBC1 or MBC2, however two types of games on the list of cartridge types don't (8=ROM+RAM, 9=ROM+RAM+BATTERY).

Additionally, on the "Everything You Always Wanted To Know About GAMEBOY" Pan Docs document there is a blurb under "Memory Bank Controllers" that says:
"Small games of not more than 32KBytes ROM do not require a MBC chip for ROM banking. The ROM is directly mapped to memory at 0000-7FFFh. Optionally up to 8KByte of RAM could be connected at A000-BFFF, even though that could require a tiny MBC-like circuit, but no real MBC chip."

So since Tetris is a 32KByte game it can have RAM connected at A000-BFFF without the use of bank switching because of the info in the document and as shown by Capaneus putting the reads/writes for saving high scores in the A000 area. Is this correct to assume?

Capaneus then wrote that, "This works on some emulators as long as I seed the .sav file with zeros, however on more accurate emulators it seems the SRAM is not writeable. I've read some stuff about enabling SRAM with MBC1 but I dont know what assembler command I have to do and what specific address I have to write to. Then I have to somehow check the validity of the SRAM at boot, and zero it out if it appears corrupt."

Later he came back with "a very simple asm to enable SRAM:

ld a,0A
ld (0000),a",

but didn't know how to inject it.

However, he never posted again so I'm not sure if he ever completed it. I believe he got the info on how to enable SRAM from under Rom Types in the Pan Docs document, where it states: "Before you can read or write to a RAM bank you have to enable it by writing a 0A into 0000-1FFF area. To disable RAM bank operations write a 00 into 0000-1FFF area. Disabling a RAM bank probably protects that bank from false writes during power down of the GameBoy." Dwedit on GBDev also wrote in a non-related thread, "Looks like the sequence for reading the SRAM is:

Ram Enable: write 0A to 0000."

Some quick questions I have:

Is modifying the cart type to include 8Kb of RAM as simple as changing the cart memory info at 0147 (Cartridge type) from 00h to 09h and changing 00h to 02h at 0149 (RAM size) in a hex editor? Or is there a more complex process?

Is changing the high score reads/writes from $D000 (Type-B scores) and $D654 (Type-A scores) to $A000 and $A654 as easy as changing the values in a hex editor? Or would it require simple ASM that reads/writes to those addresses instead?

Where would the reading SRAM/enable RAM ASM need to go in the rom? Somewhere near the beginning of the boot up of the game after the Power Up Sequence (scrolling Nintendo logo)?

Also another quick Tetris question that isn't related to SRAM. The original v1.0 of the game has different A-Type music (Minuet) than v1.1 (Korobeiniki) which is what most people are familiar with and associate with Tetris as the Tetris theme. I've never cared much for the C-Type music and wonder if it would be possible to have the C-Type option play the A-Type music from v1.0 instead? Would it require something like copying the hex data from v1.0 and pasting it over C-Type's hex data in v1.1?

Thanks for the help and sorry for my lack of programming knowledge.
lidnariq
Posts: 11429
Joined: Sun Apr 13, 2008 11:12 am

Re: Tetris Adding SRAM Saving Questions

Post by lidnariq »

Is modifying the cart type to include 8Kb of RAM as simple as changing the cart memory info at 0147 (Cartridge type) from 00h to 09h and changing 00h to 02h at 0149 (RAM size) in a hex editor? Or is there a more complex process?
It should be, but evidently he found that some emulators don't honor the header value of 9 = ROM+RAM+battery, since apparently no released games ever used it.
Is changing the high score reads/writes from $D000 (Type-B scores) and $D654 (Type-A scores) to $A000 and $A654 as easy as changing the values in a hex editor?
Yes. (Note that the values are little-endian, so the lower byte of the two will show up first. Also note that the values may show up multiple times)
Where would the reading SRAM/enable RAM ASM need to go in the rom? Somewhere near the beginning of the boot up of the game after the Power Up Sequence (scrolling Nintendo logo)?
Anywhere there's enough space to insert a patch, much like any other ROM hacking. Find a gap, insert new code, insert calls from somewhere else to new code and back.
suFami
Posts: 16
Joined: Sat Aug 06, 2016 10:22 pm

Re: Tetris Adding SRAM Saving Questions

Post by suFami »

Thanks lidnariq!

I attempted to do everything, but am having some problems.
I changed the rom cartridge type info at hex 0147 and 0149 and this worked except that when I load it into bgb I get an error message that says:
"Header checksum fails.
ROM checksum fails.
This rom would not work on a real gameboy."

However, it loads it anyways. I read in the Pan Docs document that 014D in the cartridge header is a Header Checksum (a complement check) and that if it is incorrect the game will not boot or run on a real Gameboy. However, it will load on a Super Gameboy. 014E-014F is a Global Checksum (I think what bgb is referring to as ROM checksum?) and even if it fails the game will still boot on real hardware. There is a formula to calculate the header checksum for 014D, however I cannot understand it all. In the document:
"Contains an 8 bit checksum across the cartridge header bytes 0134-014C. The checksum is calculated as follows:
x=0:FOR i=0134h TO 014Ch:x=x-MEM-1:NEXT
The lower 8 bits of the result must be the same than the value in this entry."
There's a thread on gbdev.gg8.se that explains the formula, but I still don't understand it. http://gbdev.gg8.se/forums/viewtopic.php?id=317

Anyways so I changed $D654 (Type-A high scores) in the rom to $A000. The $D654 and $D000 values only occur once in the entire rom in hex. So when I tried this in bgb it makes a sav file, but like Capaneus said all the saved high scores are filled with garbage symbols or FFFFFs. I did what he tried and zeroed out the sav file in a hex editor. This fixed it and the game properly saves the high scores. So I've got it working on an emulator, but my main purpose in adding SRAM would be to play it on real hardware and a Super Gameboy using an Everdrive GB flashcart.

I also tried putting the Enable SRAM ASM that is posted above into the rom, however I have a feeling I did this incorrectly.
I did it all in hex as I'm having difficulty understand WLA-DX.
Starting at 000B in hex (presumably this is free space since there's a lot of FF's) I put 3E 0A EA 00 00 CD B0 14 C9 which reads like:
3E 0A: ld a, 0A
EA 00 00: ld (0000), a
CD B0 14: call 14B0; this is the code I deleted to put in the new code I called from
C9: ret; return to original code

and at 061D in hex I changed CD B0 14 to CD 0B 00 to call to 000B in hex.
I have a feeling I did this wrong as I don't fully know how to write ASM.

Anywhere there's enough space to insert a patch, much like any other ROM hacking. Find a gap, insert new code, insert calls from somewhere else to new code and back.


Also when I said I didn't know where to put the ASM I meant where should I insert the call from somewhere else, like near the beginning of the rom before it reads $A000 or $A654? I put it at CD B0 14 just because it was the same amount of hex numbers as CD 0B 00. Also I don't really understand the JR or CALL commands, but looking at the rom it looks like you should RET from a CALL command? I can't get the no$gmb debugger or bgb debugger to work with setting breakpoints either.

So I was stuck at trying this on real hardware because of the 014D checksum fail until I remembered I could just try it in my Super Gameboy since the game will still boot even if the checksum fails. I renamed the zeroed out sav file to srm since this is what the Everdrive uses. The game boots fine, however the high scores are all FFFFFF's or garbage symbols and if I try to boot another rom it doesn't attempt to save the RAM so it seems the Everdrive might not recognize the SRAM or I did the ASM wrong. It does show the messed up scores though which is weird and makes me think it's seeing SRAM. I then remembered that the Everdrive isn't compatible with every Gameboy mapper. 09 (ROM+RAM+BATTERY) isn't on the supported list, however the game did boot so I wonder if it is possible to get working on the Everdrive. The supported mapper list doesn't even list ROM only, but of course these work so I wonder if it could work, it just not on the list of supported mappers.

MBC1 is supported of course, but I think converting the game to MBC1 is a little over my head and not worth the trouble just to save high scores. If I were to change the 09 in the cartridge header at 0147 to 03 (MBC1+RAM+BATTERY) would this also require changing the entire rom to take into account bank switching?
lidnariq
Posts: 11429
Joined: Sun Apr 13, 2008 11:12 am

Re: Tetris Adding SRAM Saving Questions

Post by lidnariq »

suFami wrote:"Header checksum fails.
ROM checksum fails.
This rom would not work on a real gameboy."
Because you increased the value in the header by 8+2=0xA, you should decrease the value of the checksum by 0xA, also. (This is easier for a human than recalculating the whole sum)

Another way of looking at this is: if you add all of the bytes in the header from file offset 0x134 through 0x14D inclusive, you should end up with -0x19 ≅ 0xE7 (which is the number of bytes covered by the checksum)
This fixed it and the game properly saves the high scores. So I've got it working on an emulator, but my main purpose in adding SRAM would be to play it on real hardware and a Super Gameboy using an Everdrive GB flashcart.
Well, if you're using an Everdrive GB you can also manually initialize the save RAM separately (as you did)

The common way to solve this problem is to use some kind of flag in RAM, maybe the string "Save RAM has been initialized". It won't arise randomly, so you can be confident that if the string is present, the value is good, and if it's absent, you should manually initialize it and the two locations used by the game.
I also tried putting the Enable SRAM ASM that is posted above into the rom, however I have a feeling I did this incorrectly.
The SRAM enable should only be pertinent if you are using the MBC1/5. I would naïvely assume that a hypothetical cart with just RAM and ROM and battery would have no protection register at all.
where should I insert the call from somewhere else, like near the beginning of the rom before it reads $A000 or $A654? I put it at CD B0 14 just because it was the same amount of hex numbers as CD 0B 00.
Where did you find "CD 0B 00" ?
Also I don't really understand the JR or CALL commands
JR is "change current instruction address by a signed 8 bit number". CALL is "push the current instruction address to the stack, and then change the current instruction address to the value specifed"
but looking at the rom it looks like you should RET from a CALL command?
Usually, but not always.

As a space-saving technique, you can hijack an existing CALL insert your extra code and use a "tail call" to avoid duplicating the original call. In this case, your new code would be the equivalent of

Code: Select all

CALL stub ; <-- patched address
[...]
stub: LD A, 0A
LD (0000), A
JP original
The game boots fine, however the high scores are all FFFFFF's or garbage symbols and if I try to boot another rom it doesn't attempt to save the RAM so it seems the Everdrive might not recognize the SRAM
It does seem not unlikely that the Everdrive doesn't support ROM+RAM+battery. Can you find any development notes about this?
MBC1 is supported of course, but I think converting the game to MBC1 is a little over my head and not worth the trouble just to save high scores.
The Gameboy mappers are very simple (especially in comparison to the Famicom mappers). All you'd have to to also write the correct value (1) to the register at $2000 as well as the current write to $0000.
nitro2k01
Posts: 252
Joined: Sat Aug 28, 2010 9:01 am

Re: Tetris Adding SRAM Saving Questions

Post by nitro2k01 »

suFami wrote:He then modified the reads/writes for the high scores that were going to $D654 (A-Type high scores) and $D000 (B-Type High Scores) to write to $A654 and $A600 respectively.
This seems like the simple, elegant solution, but this would not work with 1) a cart without any external RAM (maybe a small problem in practice?) 2) if the RAM was not initialized. A better method would arguably to check if the external SRAM is valid and exists upon startup and then copy it to the original location in work RAM, and also update external SRAM when needed.
suFami wrote:Most GB games with RAM use MBC1 or MBC2, however two types of games on the list of cartridge types don't (8=ROM+RAM, 9=ROM+RAM+BATTERY).

Additionally, on the "Everything You Always Wanted To Know About GAMEBOY" Pan Docs document there is a blurb under "Memory Bank Controllers" that says:
"Small games of not more than 32KBytes ROM do not require a MBC chip for ROM banking. The ROM is directly mapped to memory at 0000-7FFFh. Optionally up to 8KByte of RAM could be connected at A000-BFFF, even though that could require a tiny MBC-like circuit, but no real MBC chip."

So since Tetris is a 32KByte game it can have RAM connected at A000-BFFF without the use of bank switching because of the info in the document and as shown by Capaneus putting the reads/writes for saving high scores in the A000 area. Is this correct to assume?
The 8 and 9 values are a bit ambiguous. They were probably included for completeness, but no officially released games use those, so there are no authoritative examples of what such a circuit would look like on a cart.

The problem: All the real MBCs have a SRAM protection register that you need to write $0A to in order to mgain write access to SRAM. But what would a simple bodged-on SRAM chip look like? The minimal circuit would be a couple of logic gates for controlling the chip enable pin of the SRAM to map it to the correct position in memory. This would work... until you turn off the Gameboy. At this point, when power is dying out, the CPU crashes because it starts reading $FF on all memory positions, which is an opcode which is a shorthand call. This makes the CPU repeatedly push the value 00 39 to the stack, eventually reaching the external SRAM and corrupting it.

One option would be to add a low voltage detection circuit to stop the chip from being enabled when power is cutting out. Or, to add some sort of circuit to protect the SRAM, like on a real MBC.

As for an emulator or more intelligent flashcart like the Everdrive, they could choose to not support this mode because it's ambiguous and rarely used in practice. The over all point is that however you look at it, the implementation details are ambiguous.

A much better way to handle this would be to use a header value that corresponds to an actual MBC, which should work well everywhere.
suFami wrote:Also another quick Tetris question that isn't related to SRAM. The original v1.0 of the game has different A-Type music (Minuet) than v1.1 (Korobeiniki) which is what most people are familiar with and associate with Tetris as the Tetris theme. I've never cared much for the C-Type music and wonder if it would be possible to have the C-Type option play the A-Type music from v1.0 instead? Would it require something like copying the hex data from v1.0 and pasting it over C-Type's hex data in v1.1?
Something like that, yes. If you wanted to have all 4 choices available, you would also have to modify the GUI so you could actually select any of the songs.
suFami
Posts: 16
Joined: Sat Aug 06, 2016 10:22 pm

Re: Tetris Adding SRAM Saving Questions

Post by suFami »

Well, if you're using an Everdrive GB you can also manually initialize the save RAM separately (as you did)

The common way to solve this problem is to use some kind of flag in RAM, maybe the string "Save RAM has been initialized". It won't arise randomly, so you can be confident that if the string is present, the value is good, and if it's absent, you should manually initialize it and the two locations used by the game.
This seems like the simple, elegant solution, but this would not work with 1) a cart without any external RAM (maybe a small problem in practice?) 2) if the RAM was not initialized. A better method would arguably to check if the external SRAM is valid and exists upon startup and then copy it to the original location in work RAM, and also update external SRAM when needed.
Hmm, so you're both saying that I need to initialize the RAM. I thought that was the point of the enable SRAM ASM. So I need additional code to initialize the RAM? I thought lidnariq said I did this... Can you give me a hint as to how to check if external SRAM is valid (what would this code look like)?
Where did you find "CD 0B 00" ?
I just looked for free space in a hex editor where there's lots of FF's.
JR is "change current instruction address by a signed 8 bit number". CALL is "push the current instruction address to the stack, and then change the current instruction address to the value specifed"
So I'm guessing CALL would be the right ASM? From what you guys wrote though, I don't even need the enable SRAM ASM if I'm not using MBC's.
It does seem not unlikely that the Everdrive doesn't support ROM+RAM+battery. Can you find any development notes about this?
There aren't any development notes. Just a list of supported mappers on krikzz's official Everdrive store.
As for an emulator or more intelligent flashcart like the Everdrive, they could choose to not support this mode because it's ambiguous and rarely used in practice. The over all point is that however you look at it, the implementation details are ambiguous.
What's weird is that the bgb emulator see's the save file and works fine after I seeded the save file with 00's in a hex editor. High scores save flawlessly without any garbage symbols or FF's. This also works WITHOUT the enable SRAM ASM (which I don't believe I injected or wrote correctly anyways).
A much better way to handle this would be to use a header value that corresponds to an actual MBC, which should work well everywhere.
So it's looking like my only option to have saving on real hardware with a flash cart would be to convert the rom to MBC1, which is way over my head. Lidnariq wrote a little on how above, but I'm still confused if this would mean the whole rom would have to be tinkered with so it could know when to switch banks to get access to all the code.
As for the music, I don't really want to change the GUI or anything, just replace v1.1 Type-C music with v1.0 Type-A music. Do you or lidnariq have any helpful tips on using bgb's debugger (I can't see to get breakpoints to work at all) or what I should be looking for in disassembled code? Like a music register that gets written to which would lead to the music's code in a hex editor?

Thanks for your patience with me. I bet it can be a little frustrating at how dumb I am with this, but I've never learned any programming so this is all very foreign to me. A comparison I would make is like if I was trying to write an essay in Spanish and only had basic understanding of the language. That's sort of what trying to understand all of this GB/Z80 programming stuff is.
If the only way to save on real hardware is MBC1 conversion, I'll probably call it a day. I at least have a working saveable version with the bgb emulator. I've been taking pictures of my high scores like everyone else for a while, so not a big deal. I would like to attempt the music transfer though if it's not too difficult. If it's a copy/paste type of problem I want to learn enough to get it done.
lidnariq
Posts: 11429
Joined: Sun Apr 13, 2008 11:12 am

Re: Tetris Adding SRAM Saving Questions

Post by lidnariq »

It sounds like, as long as you're wedded to using the EverdriveGB, you're probably restricted to having to make a token mapper hack to be compatible with the MBC1 registers.

That said, I'm pretty certain pretending to use an MBC1 is nowhere near as big of an endeavor as you're fearing it is. I'm pretty certain it's just a matter of inserting the fifteen bytes I suggested earlier and hoping that the Everdrive GB's SRAM handler is robust to when the game turns off.
nitro2k01
Posts: 252
Joined: Sat Aug 28, 2010 9:01 am

Re: Tetris Adding SRAM Saving Questions

Post by nitro2k01 »

suFami wrote:Hmm, so you're both saying that I need to initialize the RAM. I thought that was the point of the enable SRAM ASM. So I need additional code to initialize the RAM? I thought lidnariq said I did this... Can you give me a hint as to how to check if external SRAM is valid (what would this code look like)?
The point of the ASM you're referring to is to unlock the SRAM so it can be accessed. Unlocking SRAM does not clear SRAM. The point of checking for validity of SRAM is to make sure that the highscore list doesn't contain garbage when starting up for the first time.

Here's some code improvised from the top of my head. It will store the string TETRIS at some address in SRAM to indicate that SRAM is valid.

Code: Select all

; Enable SRAM.
  ld a,$0A
  ld [$0000],a

; Check for validity of SRAM by looking for the string TETRIS.
  ld de,$0134   ; Use the copy of the string that is already in the ROM header.
  ld hl,$A000    ; Bottom of SRAM.
.compareloop
  ld a,[de]     ; Load character from the string in ROM.
  or a     ; Zero check. If we reached the end of the string, the signature is valid.
  jr z,.comparepass
  cp [hl]  ; Compare with the SRAM byte.
  inc de   ; Safe. 16 bit inc preserves flags.
  inc hl   ; Safe. 16 bit inc preserves flags.
  jr z,.compareloop  ; Continue checking the string?
  
  ld de,$0134   ; Use the copy of the string that is already in the ROM header.
  ld hl,$A000    ; Bottom of SRAM.
.copysigloop
  ld a,[de]     ; Load character from the string in ROM.
  ld [hl+],a   ; Copy the byte to SRAM and post-increment the RAM point.
  inc de   ; Increment the ROM pointer.
  or a     ; Zero check. If we reached the end of the string, clear the rest of SRAM instead
  jr nz,.copysigloop  ; Continue copying the string?
  
.clearsramloop
  xor a  ; Set the value of a to 00.
  ld [hl+],a  ; Clear the byte to SRAM and post-increment the RAM point.
  ld a,h   ; High byte of pointer
  cp $C0  ; When H=$C0 we have reached outside of SRAM.
  
.comparepass
  ; The signature matched, or SRAM was cleared. Now copy the highscore table from SRAM to work RAM
  ld  hl,$b000
  ld  de,$d000
  ; Restore the table from the copy in SRAM
.restorehiscoreloop
  ld a,[hl+]
  ld [de],a
  inc de
  ld a,h
  cp  $c0  ; Has the source address reached outside $b000-$bfff?
  jr nz,.restorehiscoreloop

  ; Disable SRAM access and return.
  xor a
  ld [$0000],a
  ret

This code could probably be improved in various ways. It's just a quick example. It assumes that the whole area $D000-$DFFF is used for highscores and restores that from SRAM $B000-$BFFF. It has no code for saving highscore data to SRAM. That's left as an exercise for the reader.
suFami wrote:What's weird is that the bgb emulator see's the save file and works fine after I seeded the save file with 00's in a hex editor. High scores save flawlessly without any garbage symbols or FF's. This also works WITHOUT the enable SRAM ASM (which I don't believe I injected or wrote correctly anyways).
Yes, after you've seeded the save file with zeroes! Which is not a natural situation. So it seems BGB emulates ROM+RAM+BATTERY as if there's no MBC, so the enable command is not needed. A different emulator may emulate this mode as having a MBC, or not emulate it at all. This is why you should choose a value that corresponds to a real MBC such as 03h MBC1+RAM+BATTERY or 1Bh MBC5+RAM+BATTERY.

Btw, each level, and for B type, each combination of level and "high" has its own highscore table. I'm pretty sure the SRAM positions that you changed the highscore table to in the original post would create overlap between some of the tables!
suFami wrote:So it's looking like my only option to have saving on real hardware with a flash cart would be to convert the rom to MBC1, which is way over my head. Lidnariq wrote a little on how above, but I'm still confused if this would mean the whole rom would have to be tinkered with so it could know when to switch banks to get access to all the code.
No, it's not over your head. You need to do the SRAM unlock command as before. But assuming you can fit the code in the existing ROM space without embiggening it (which btw is a perfectly cromulent word) all you need to do in addition is to write 01 to address 2000 to give the same result as ROM only. But guess what, Tetris was designed to be "forward compatible" and already includes this write on address $02D3 in ROM version 1.0 or $0252 in ROM version 1.1. So you actually literally have to do nothing more than change the header value for cart type.
suFami
Posts: 16
Joined: Sat Aug 06, 2016 10:22 pm

Re: Tetris Adding SRAM Saving Questions

Post by suFami »

The point of the ASM you're referring to is to unlock the SRAM so it can be accessed. Unlocking SRAM does not clear SRAM. The point of checking for validity of SRAM is to make sure that the highscore list doesn't contain garbage when starting up for the first time.
Thank you for the explanation. After reading that it seems obvious to me that unlocking the SRAM doesn't necessarily mean the SRAM is usable. I guess I just assumed that if SRAM was unlocked it would be there and fine. So after the SRAM is unlocked it needs to be checked that it is valid and if not it should be zeroed? Wouldn't this erase a previous save if this wasn't the first time playing? Or does the SRAM need to be zeroed out just once (the first time the game has ever been turned on) and the SRAM information should be fine every time after that as long as the battery doesn't go out?
Here's some code improvised from the top of my head. It will store the string TETRIS at some address in SRAM to indicate that SRAM is valid.
Whoa that's really impressive that you could improvise all of that. I can barely comprehend the code, even with the comments. Thanks for the comments, btw. I think I am going to take a few days to read up on Gameboy programming and really try to understand this code inside and out. I don't understand what is probably basic stuff. Like these lines:

Code: Select all

inc de   ; Safe. 16 bit inc preserves flags.
inc hl   ; Safe. 16 bit inc preserves flags.
I have no idea why that's needed. If I can study enough then I believe I may be at the point where I am able to write my own code to save high scores to SRAM.

Does anyone have a good reference for understanding Gameboy opcodes or some tutorials that you may be familiar with? I'm finding lots of opcode lists, but they never define what the opcode actually does. For instance, lidnariq wrote above that a CALL is "push the current instruction address to the stack, and then change the current instruction address to the value specifed", however putting that into a search bar doesn't turn up a document that defines Gameboy opcodes so I assume lidnariq wrote that himself.
This code could probably be improved in various ways. It's just a quick example. It assumes that the whole area $D000-$DFFF is used for highscores and restores that from SRAM $B000-$BFFF. It has no code for saving highscore data to SRAM. That's left as an exercise for the reader.
Btw, each level, and for B type, each combination of level and "high" has its own highscore table. I'm pretty sure the SRAM positions that you changed the highscore table to in the original post would create overlap between some of the tables!
Am I correct to assume that instead of changing the reads/writes from say $D654 to $A654 like I did in the first post to the way it's handled in this code is because of the possible table overlap?
But assuming you can fit the code in the existing ROM space without embiggening it (which btw is a perfectly cromulent word) all you need to do in addition is to write 01 to address 2000 to give the same result as ROM only. But guess what, Tetris was designed to be "forward compatible" and already includes this write on address $02D3 in ROM version 1.0 or $0252 in ROM version 1.1. So you actually literally have to do nothing more than change the header value for cart type.
You're right that is very simple. I've changed the cart type to 03h. How did you know and find out that a write to $0252 meant this could easily be converted to MBC1 because of being forward compatible? Is this just something you picked up from programming experience?
I looked over the rom and unfortunately it does not look like there is not much free space at all. The most free space I could find in one place was right at the beginning of the rom at 00E0-00FF in hex. Honestly, I only care about saving Type-A (points) high scores. I've never cared about Type-B high scores or know many people that do. I wonder is it possible to use the space where the high score tables for Type-B are to fit in the code? This would be all the bytes from $D000-$D653 (right before Type-A's tables start at $D654)? Thanks again for the help.
lidnariq
Posts: 11429
Joined: Sun Apr 13, 2008 11:12 am

Re: Tetris Adding SRAM Saving Questions

Post by lidnariq »

suFami wrote:Does anyone have a good reference for understanding Gameboy opcodes or some tutorials that you may be familiar with? I'm finding lots of opcode lists, but they never define what the opcode actually does. For instance, lidnariq wrote above that a CALL is "push the current instruction address to the stack, and then change the current instruction address to the value specifed", however putting that into a search bar doesn't turn up a document that defines Gameboy opcodes so I assume lidnariq wrote that himself.
Yeah, I did.

The Gameboy's CPU is somewhere between an Intel 8080 and a Zilog Z80, but I found the technical documentation for the Z80 to be pretty readable: http://www.zilog.com/docs/z80/UM0080.pdf

(Just be careful and pay attention to what the GBZ80 is missing.
suFami
Posts: 16
Joined: Sat Aug 06, 2016 10:22 pm

Re: Tetris Adding SRAM Saving Questions

Post by suFami »

Thank you for the link lidnariq. Looks detailed and just what I need to read up on.
nitro2k01
Posts: 252
Joined: Sat Aug 28, 2010 9:01 am

Re: Tetris Adding SRAM Saving Questions

Post by nitro2k01 »

suFami wrote:Thank you for the explanation. After reading that it seems obvious to me that unlocking the SRAM doesn't necessarily mean the SRAM is usable. I guess I just assumed that if SRAM was unlocked it would be there and fine. So after the SRAM is unlocked it needs to be checked that it is valid and if not it should be zeroed? Wouldn't this erase a previous save if this wasn't the first time playing? Or does the SRAM need to be zeroed out just once (the first time the game has ever been turned on) and the SRAM information should be fine every time after that as long as the battery doesn't go out?
That's what the magic signature is for. The code I wrote checks if the string TETRIS is present at the start of the SRAM memory space. It's very unlikely that this memory would say TETRIS by random chance, so this is taken to mean that the data is valid.
suFami wrote:

Code: Select all

inc de   ; Safe. 16 bit inc preserves flags.
inc hl   ; Safe. 16 bit inc preserves flags.
I have no idea why that's needed. If I can study enough then I believe I may be at the point where I am able to write my own code to save high scores to SRAM.
From which angle are you asking?
Those instructions increment the value of de and then hl. In another language this would be something like de=de+1; hl=hl+1; This is the code that compares if the TETRIS string is valid, one byte at a time.

If you're asking what safe means... Right before those lines, the code does: "cp [hl]" This means "compare A to the byte at the address that HL points to. It stores the result of this operation in the z register. z is 1 if the compare is equal, or otherwise 0. (Actually, compare with x is just A-x where the result is discarded. So if A=x, then A-x=0 and the zero flag is set.)

So here comes the trick. You need to increment the registers after comparing the data at HL and DE if you don't want to miss the first byte. (Unless you're doing some trick like starting at one memory position early to compensate for an increment.) A normal 8 bit inc or dec will affect the z flag. (z flag is set if the result of the operation is zero.) So then, the result of the comparison is destroyed. But inc/dec on a 16-bit register pair doesn't affect flags at all, so they are "safe" in this regard.
suFami wrote:Does anyone have a good reference for understanding Gameboy opcodes or some tutorials that you may be familiar with? I'm finding lots of opcode lists, but they never define what the opcode actually does. For instance, lidnariq wrote above that a CALL is "push the current instruction address to the stack, and then change the current instruction address to the value specifed", however putting that into a search bar doesn't turn up a document that defines Gameboy opcodes so I assume lidnariq wrote that himself.
Go to ASMSchool. And Pan Docs as you probably know.
suFami wrote:Am I correct to assume that instead of changing the reads/writes from say $D654 to $A654 like I did in the first post to the way it's handled in this code is because of the possible table overlap?
The overlap would come from the specific addresses you mentioned:
suFami wrote:He then modified the reads/writes for the high scores that were going to $D654 (A-Type high scores) and $D000 (B-Type High Scores) to write to $A654 and $A600 respectively.
Why he would choose $A600 is beyond me. This would mean that there are only $A654-$A600=$54 bytes for the type B table which is obviously too little. Just changing $D654 to $A654 (as already suggested) and $D000 to $A000 (different!) should be enough to solve that problem.
suFami wrote:You're right that is very simple. I've changed the cart type to 03h. How did you know and find out that a write to $0252 meant this could easily be converted to MBC1 because of being forward compatible? Is this just something you picked up from programming experience?
Not a write to $0252. A write at $0252. You can see this code in the BGB debugger by pressing ctrl+G (go to) and entering 0252. The write is to $2000 which selects which ROM bank is present in the selectable ROM area, $4000-$7FFF.

One way you can find this is: You first figure out from for example Pan Docs that $2000 is a (write-only) register for selecting a ROM bank. This is documented in the "Memory Bank Controllers" section. Then in the debugger, you choose debug, access breakpoints and enter 2000. Or if you want to be sure to not miss any writes, you could enter 2000-3FFF which is the full range for this register. Writing a byte to any address in this range does the same thing, change the ROM memory bank.
suFami wrote:I looked over the rom and unfortunately it does not look like there is not much free space at all. The most free space I could find in one place was right at the beginning of the rom at 00E0-00FF in hex. Honestly, I only care about saving Type-A (points) high scores. I've never cared about Type-B high scores or know many people that do. I wonder is it possible to use the space where the high score tables for Type-B are to fit in the code? This would be all the bytes from $D000-$D653 (right before Type-A's tables start at $D654)? Thanks again for the help.
It's correct that there's very little space. But on the other hand that space only needs to fit the code, not the highscore backup which is stored in SRAM. The size of the code can be the same whether it's copying 1 or 1000 bytes, since that happens in a loop.
suFami
Posts: 16
Joined: Sat Aug 06, 2016 10:22 pm

Re: Tetris Adding SRAM Saving Questions

Post by suFami »

The code I wrote checks if the string TETRIS is present at the start of the SRAM memory space. It's very unlikely that this memory would say TETRIS by random chance, so this is taken to mean that the data is valid.
I guess what I'm trying to ask is what happens if the TETRIS string is not found (though I understand this is unlikely)? SRAM would be cleared and if so would that overwrite a previous save file?
Another question I forgot to ask in regards to the string is how does the code know when it hits the last letter S in the string of TETRIS? I see

Code: Select all

ld de,$0134
which loads the string, but Pandocs says the ASCII title at $0134 can go all the way to $0143 if the title is long enough so how does the code know to stop after 6 characters? I see this part in the code:

Code: Select all

ld a,[de]     ; Load character from the string in ROM
but does it know it hit the end by hitting 00's?
From which angle are you asking?
I wasn't saying I didn't understand why it was needed in the code like it was unecessary or anything, just that I didn't know why it was needed in the sense that I was just using it as an example of simple code that I didn't understand or know why it was in there. A way to write that I probably should be able to know those things in the code if I want to be adding SRAM support.
Thank you for the explanation though. Even with the explanation I'm not sure I fully wrap my head around it, but I will study up. Registers and flags and 8bit/16bit and those types of things are the parts of ASM I understand the least right now.
Go to ASMSchool.
Awesome link! This is just what I need to read up on. What's sad is I have been to that old Gameboy dev site these past few days and didn't even notice the ASM section.
Why he would choose $A600 is beyond me.
My apologies, I wrote it wrong in that post. $A600 is supposed to be $A000. I think because I had just written $A654 I put a 6 in there on accident. $D654 to $A654 and $D000 to $A000. So there's no table overlap.
Hmm, would it be easier just to change the values in hex like I have been doing (which was working in the emulator before the MBC1 cart type change) instead of writing new code that would copy the high score tables from $D654/$D000 to SRAM and back from SRAM to those values (at startup)? Would seem much easier than writing code if an easy hex change will do the trick.
Then in the debugger, you choose debug, access breakpoints and enter 2000. Or if you want to be sure to not miss any writes, you could enter 2000-3FFF which is the full range for this register. Writing a byte to any address in this range does the same thing, change the ROM memory bank.
I think I am finally using the debugger correctly, thank you. I added 2000 to access breakpoints and clicked enabled. Then I ran the game and set "Toggle Breakpoints". It jumped me to the code you said (I guess $0254 is apart of it too?):

Code: Select all

0252: ld a, 01
0254: ld (2000), a
Transfer 01 to the accumulator, and then transfer accumulator to 2000. This takes care of the write to $2000.
It's correct that there's very little space. But on the other hand that space only needs to fit the code, not the highscore backup which is stored in SRAM. The size of the code can be the same whether it's copying 1 or 1000 bytes, since that happens in a loop.
The most free space in one area is from 00DA-00FF, which is only 38 bytes. Not enough to fit the code you wrote or additional code I would write to save high scores to SRAM. So if I could use the space where Type-B's score table would go (which I don't care about saving Type-B scores) I might have room to fit the code. Or I could just scatter the code throughout the rom where there's room, correct? Like the clearsramloop can be in a different place than comparepass? Since they are JR'd anyways?
Also if just changing $D654/$D000 to SRAM values ($A654/$A000) in hex so that the high scores are read/written to SRAM directly will work then could parts of the code be deleted like the comparepass part of your code which you put in the comment copies the high score table from SRAM to work RAM. This wouldn't be needed anymore because the high scores are being read/written to SRAM directly? This would clear up space for code. I think I might be misunderstanding exactly what SRAM is or work RAM or both. They both take up A000-BFFF?

Thanks again for the help.
nitro2k01
Posts: 252
Joined: Sat Aug 28, 2010 9:01 am

Re: Tetris Adding SRAM Saving Questions

Post by nitro2k01 »

suFami wrote:I guess what I'm trying to ask is what happens if the TETRIS string is not found (though I understand this is unlikely)? SRAM would be cleared and if so would that overwrite a previous save file?
Yeah, but that's the whole point. If you can't trust those 6 bytes to retain their value from last time, how can you trust the highscore table to still be correct? Then the memory chip is broken, or the cartridge battery has run out, or something else has overwritten it.

But what I said was the opposite, though. It's unlikely that the memory would be TETRIS by chance if nothing set the memory to those values.
suFami wrote:Another question I forgot to ask in regards to the string is how does the code know when it hits the last letter S in the string of TETRIS? I see

Code: Select all

ld de,$0134
which loads the string, but Pandocs says the ASCII title at $0134 can go all the way to $0143 if the title is long enough so how does the code know to stop after 6 characters? I see this part in the code:

Code: Select all

ld a,[de]     ; Load character from the string in ROM
but does it know it hit the end by hitting 00's?
This is even commented in the code:

Code: Select all

  or a     ; Zero check. If we reached the end of the string, the signature is valid.
  jr z,.comparepass
It ORs the byte with itself, and if the result is zero, the byte must be the first 00 byte after the string.
suFami wrote:Hmm, would it be easier just to change the values in hex like I have been doing (which was working in the emulator before the MBC1 cart type change) instead of writing new code that would copy the high score tables from $D654/$D000 to SRAM and back from SRAM to those values (at startup)? Would seem much easier than writing code if an easy hex change will do the trick.
Like I tried to explain before, that is emulator specific behavior. It may work on BGB but not in any other emulator or flash cart. MBC1 has a well-known behavior but ROM+RAM+BATTERY doesn't. This does mean that you need to enable and disable SRAM before and after accessing it. Especially if you want it to work on flash carts, since they generally don't even know or care about the ROM header, but will protect the memory regardless. You don't need to write code which copies data to and from SRAM, but at a minimum, you should add code which enables SRAM before writing a new highscore, and disables it when done. But even then, that is a bad solution because you can never assume that SRAM is zero filled on startup.
suFami wrote:The most free space in one area is from 00DA-00FF, which is only 38 bytes. Not enough to fit the code you wrote or additional code I would write to save high scores to SRAM. So if I could use the space where Type-B's score table would go (which I don't care about saving Type-B scores) I might have room to fit the code. Or I could just scatter the code throughout the rom where there's room, correct? Like the clearsramloop can be in a different place than comparepass? Since they are JR'd anyways?
How would you use type B score table for code? It's in SRAM and then you would have to distribute your ROM with a sav file and rely on the SRAM being correct and never changing, by for example running the ROM without the correct sav.

But yes, you can scatter code throughout unused areas. Let's see.

Code: Select all

$000B-$0027: 29 ($1d) bytes
$0034-$003F: 12 ($c) bytes
$00DA-$00FF: 38 ($26) bytes
$7FC7-$7FEF: 41 ($29) bytes
$7FF6-$7FFF: 12 ($a) bytes
My code (even if not complete) is 56 ($38) bytes big so it should fit.

As a side note, when you look at the code, you start noticing things. At $7FF0, you have two jumps, which look like they may be an afterthought of some kind. Also, in version 1.0 of the ROM, a lot of space from 005B and onward is free. In version 1.1, they have moved the serial interrupt code there. The serial interrupt (address $0058) jumps to the next address after it, "jp $005B". A completely unnecessary jump. The code could have continued right there with push AF, push HL and so on but maybe this was for the convenience of being able to move it around without having to remove and add an extra jump all the time in the source code if they moved the rest of the code.

But anyway, the point is that they seemed to be running out of space since they moved the code there.
TravistyOJ
Posts: 2
Joined: Wed Aug 24, 2016 7:30 am

Re: Tetris Adding SRAM Saving Questions

Post by TravistyOJ »

Hey guys, I am Capaneus on the romhacking forum, the guy who started working on this a couple of years ago, and unfortunately got sidetracked by other projects/life. Coincidentally, I thought about this a couple of days ago and found this thread. First, I thought I'd do a little more experimentation, and after fixing the checksums, I've found my original changes do work on real hardware on an EverdriveGB with game type 09 (ROM+RAM+BATTERY) so long as I seed the .srm file with zeros. Obviously, that's just part of the solution as a real SRAM game would be able to initialize the ram (everdrive seems to present RAM as FF's, which I imagine is what a brand new game would have on it's SRAM. I'm completely new to asm, so I imagine you are already farther along on that front, but I wanted to pass along my discovery.
Post Reply