Gameboy Development Forum

Discussion about software development for the old-school Gameboys, ranging from the "Gray brick" to Gameboy Color
(Launched in 2008)

You are not logged in.

Ads

#1 2016-08-01 20:55:29

DonaldHays
Member
From: Seattle
Registered: 2016-08-01
Posts: 36
Website

New to Game Boy programming, made a version of Snake

Hi all,

For a couple of weeks now I've been playing with programming the Game Boy. I'm an iOS programmer professionally, and I wanted to try programming for a more limited system. Mobile phones are slower than desktop computers, but modern phones are still far more powerful than the Game Boy!

Last night I finished a version of Snake, the classic "eat food while avoiding your own tail" game.

http://donaldhays.com/projects/snake/img/screen.png

Webpage | ROM Direct Download | Source

It was a fun challenge. I chose Snake because of its moderate complexity level. It's no Metroid or Zelda or Mario, but it's far more complex than "Hello World".

I programmed this game in assembly, and juggling registers is pretty different from modern development. On modern systems you generally try to avoid having shared mutable state as much as possible, to make things easier to reason about. When you're programming registers directly on old hardware, on the other hand, it's all about shared mutable state, and you find yourself frequently saying, "now, which registers might get altered if I call this subroutine?" It's a big tradeoff between safety and speed. But I enjoyed it. It's a different sort of challenge. Instead of trying to focus on creating elegant APIs that encapsulate logic in a simple, clear, and safe way, I'm looking at opcode charts and seeing if I can trim a clock cycle off here or there.

I'm gonna try to get the project cleaned up a little bit (folder has various offshoot projects and such right now) and then throw it on a public GitHub repo in the next day or two. I stumbled upon this forum multiple times in searches for how to do different things on the Game Boy, and I have a few questions yet about how I did certain things and alternative ways to handle things, so hopefully the code I share and the questions I post will help other developers (and myself!) later.

Last edited by DonaldHays (2016-08-02 22:06:48)

Offline

 

#2 2016-08-02 13:24:02

bbsfoo
Member
Registered: 2016-02-08
Posts: 16

Re: New to Game Boy programming, made a version of Snake

thank you for sharing

Offline

 

#3 2016-08-02 22:06:14

DonaldHays
Member
From: Seattle
Registered: 2016-08-01
Posts: 36
Website

Re: New to Game Boy programming, made a version of Snake

bbsfoo wrote:

thank you for sharing

Thank you for checking it out!

I've posted the source to https://github.com/DonaldHays/snake-gb . I have a number of questions before starting another project, so first and foremost:

Argument-passing and register management. For the most part, I pass arguments directly in registers, and place return values in registers, and have doc comments that note which registers are "destroyed" by the routine (have their value changed to something that the subroutine does not specify). Routines are also obviously able to read data from memory. One thing I didn't do, but read a little bit about, was push arguments onto the call stack. I've noticed that there are some instructions that deal particularly with SP, and look to be meant to be used to handle arguments on the stack. Are there any articles or tutorials about using the stack for arguments? My Googling failed to turn up much other than really basic descriptions of, say, the LD HL, [SP+n] opcode. Or could somebody kindly walk through an example of pushing an argument onto the stack, calling a subroutine, reading the argument in the subroutine, and then returning and cleaning up the stack appropriately?

Offline

 

#4 2016-08-03 21:11:01

Shonumi
New member
Registered: 2016-07-03
Posts: 8

Re: New to Game Boy programming, made a version of Snake

DonaldHays wrote:

One thing I didn't do, but read a little bit about, was push arguments onto the call stack. I've noticed that there are some instructions that deal particularly with SP, and look to be meant to be used to handle arguments on the stack. Are there any articles or tutorials about using the stack for arguments?

The simplest form is using the PUSH instructions at the beginning of your subroutine. These will place register pairs (AF, BC, DE, and HL) at the current location of SP. Generally, they're saved to Zero Page RAM ($FF80 - FFFF) but you can change the Stack Pointer to Working RAM (probably even VRAM if there are parts you're not using). The GB basically makes copies of registers like that so you can recall them at the end of your subroutine with a POP command. So if all of your arguments fall into registers just do something like this:

Code:

CALL mysubroutine

...
...
...

mysubroutine:
   PUSH AF
   PUSH BC
   PUSH DE
   PUSH HL

   some code goes here
   we're done with subroutine

   POP HL
   POP DE
   POP BC
   POP AF

   RET

You have to make sure to get the order right. First in, last out. First registers to push are the last ones popped since the GB stack is descending. When you PUSH registers, you can change them to your hearts content, and when you POP them, they come back as their original values. Generally, you should only PUSH the registers you intend to destroy, since PUSH and POP instructions can eat up a lot of CPU cycles (not a problem on the GBC with Double Speed mode, I guess). Also, be aware that you should avoid changing the SP in between a PUSH and POP unless you really know what you're doing. PUSH and POP instructions automatically handle incrementing and decrementing the SP, so you don't have to do it manually. Hope this helps.

Offline

 

#5 2016-08-03 23:12:15

DonaldHays
Member
From: Seattle
Registered: 2016-08-01
Posts: 36
Website

Re: New to Game Boy programming, made a version of Snake

Thanks for taking the time to reply, Shonumi! Unfortunately, that's not quite what I was looking for. I apologize for failing to communicate clearly!

I didn't mean to ask about pushing and popping registers in general, but rather about pushing values onto the stack in order for a subroutine to use them as arguments. In my game I didn't run into any particular situations where I wanted to do that (everything I wanted to pass to my subroutines fit in registers), but imagine a situation where you want to pass a lot of data to a subroutine, like, for example, six arbitrary pointers. If you want to pass that much data to a subroutine, you don't have enough room to pass them in via registers. You could write them to a buffer in RAM and just pass the routine the address to the buffer, but let's ignore RAM techniques like that for now. The third way you could pass the arguments is by pushing the pointers onto the stack, and then calling the routine, which then examines the pointers on the stack. Instructions like LD HL, [SP+n] appear to be built specifically for this purpose. So my inquiry was asking about that, if there's any material written about using the stack as means of passing arguments.

Offline

 

#6 2016-08-04 01:10:43

Shonumi
New member
Registered: 2016-07-03
Posts: 8

Re: New to Game Boy programming, made a version of Snake

Yeah, I had a feeling you were talking about data larger than the register set. Don't worry, I can help there too (or at least try). Let's say you want to have your subroutine access data at six different pointers, and you want to access those pointers via the stack. The easiest way I can see still involves PUSH and POP. Before calling the subroutine, you PUSH the pointers to the stack, then you POP them one by one like so:

Code:

LD HL, $DEAD
PUSH HL
LD HL, $CODE
PUSH HL

... repeat for the rest of the pointers

CALL mysubroutine

...

mysubroutine:
    POP HL
    ... do something with the pointer
    ... to process the rest of the pointers, keep calling POP HL below here or loop back to start if your code needs to

    RET

LD HL, [SP+n] basically loads the 2-byte value found at SP + n (where n is a 1 byte, signed immediate). It's just like LD HL, nn but the address it looks to is SP (+128 or -127 bytes depending on n). In theory, POP HL, and LD HL, [SP+n] can be made to do the same thing. The caveat is that with LD HL, [SP+n], you have to manage the stack and the stack pointer yourself. If you chose the wrong value of n, you could mistakenly read the wrong value. LD HL, [SP+n] is good if you know what the stack looks like and if you need to access the stack in a non-descending order. For example, if you prefer an ascending stack, LD HL, [SP+n] would be appropriate. If not, PUSH/POP is preferred, in my humble opinion. Here's some psuedo code using LD HL, [SP+n] anyway if you're really still interested in it (note, I dunno how your assembler handles stuff, so check your documentation on how to properly code an LD HL, [SP+n] instruction):

Code:

mysubroutine:
    LD HL, [SP+0]
    ... Grab the 16-bit value at SP. If we're on a descending stack, the next "n" will have to be -2
    ... Assume we do something with HL now

    LD HL, [SP-2]
    ... This may seem hard to iterate properly because "n" is an immediate value, not a register
    ... One potential solution is to change SP rather than "n", e.g. subtract 2 from SP, then loop back to LD HL, [SP+0]

    LD HL, [SP-4]
    ... Repeat for the rest of those six pointers

    RET

But anyway, yeah, there's no limit to what you can PUSH to the stack, as long as you have the RAM. You could PUSH 12, 24, or 64 pointers, then POP them all off one-by-one. The downside about PUSH and POP is that they always work on register pairs, so it's always working on 16-bit values, which can quickly eat up memory if you're not careful. To be fair, LD HL, [SP+n] also operates solely on a 16-bit level. Now, it is possible to manually write values to your stack like so:

Code:

LD HL, $0000
ADD HL, SP

... Grab some value into A, write it into HL, which is also the stack pointer
... Decrement HL to simulate a descending stack

LD A, $80
LD (HL), A
DEC HL

Obviously manually writing single bytes to the stack like that gets expensive for cycles, and it's a lot more code. Perhaps this will give you some more insight into arguments on the stack.

Last edited by Shonumi (2016-08-04 01:19:54)

Offline

 

#7 2016-08-04 16:45:05

DonaldHays
Member
From: Seattle
Registered: 2016-08-01
Posts: 36
Website

Re: New to Game Boy programming, made a version of Snake

Yeah, that's exactly what I was looking for, thanks, Shonumi!

I wonder how common such techniques were in practice. I mean, they made an instruction dedicated to it. GBDK's output makes copious use of it for argument passing. But in games that are written in assembly from the start, I wonder how many do those sort of arguments-on-the-stack things, vs how many only use registers and RAM. Maybe the instruction is there primarily to accommodate compilers of higher-level languages?

Also, since calling a subroutine pushes the return address to the top of the stack, such stack reading would need to take that into account, right? In your first example, when you pop HL in `mysubroutine`, HL will then hold the return address, not $CODE, right?

Last edited by DonaldHays (2016-08-04 16:45:17)

Offline

 

#8 2016-08-04 17:43:58

AntonioND
Member
Registered: 2014-06-17
Posts: 134
Website

Re: New to Game Boy programming, made a version of Snake

"ld hl,[sp+n]" doesn't exist, it's "ld hl,sp+n". It just loads to HL the value of SP+n, not the value in that address.

That kind of instruction is used a lot by GDBK, not so much by hand-made assembly code. I sometimes use it, but I try not to and divide my function into smaller ones if I think it's getting to complex.

PS: Some assemblers also accept "jp [hl]", which is incorrect, it should be "jp hl". And the assembler used by GBDK has a crazy way of writing some instructions like "add sp,n" or "ld hl,sp+n".

Offline

 

#9 2016-08-04 19:56:22

Shonumi
New member
Registered: 2016-07-03
Posts: 8

Re: New to Game Boy programming, made a version of Snake

AntonioND wrote:

"ld hl,[sp+n]" doesn't exist, it's "ld hl,sp+n". It just loads to HL the value of SP+n, not the value in that address.

That kind of instruction is used a lot by GDBK, not so much by hand-made assembly code. I sometimes use it, but I try not to and divide my function into smaller ones if I think it's getting to complex.

PS: Some assemblers also accept "jp [hl]", which is incorrect, it should be "jp hl". And the assembler used by GBDK has a crazy way of writing some instructions like "add sp,n" or "ld hl,sp+n".

Whoops! My bad, you're correct. All of my GBZ80 code is hand-made as hexadecimal values, so I suppose I overlooked that detail. The syntax should have been a red-flag. I rarely use that instruction any way, probably also explains why I didn't catch it. In that case, everyone can ignore the latter half of my previous post.

DonaldHays wrote:

I wonder how common such techniques were in practice. I mean, they made an instruction dedicated to it. GBDK's output makes copious use of it for argument passing. But in games that are written in assembly from the start, I wonder how many do those sort of arguments-on-the-stack things, vs how many only use registers and RAM. Maybe the instruction is there primarily to accommodate compilers of higher-level languages?

Using the stack like that is probably more common on the GBA where the stack itself is much more flexible and you get way more registers to play with. As I said before stack operations are 16-bit, so they can take up a lot of cycles if you're not careful. If you did have something like that, I'd suspect it'd be on the GBC where you can take more liberties with the system.

DonaldHays wrote:

Also, since calling a subroutine pushes the return address to the top of the stack, such stack reading would need to take that into account, right? In your first example, when you pop HL in `mysubroutine`, HL will then hold the return address, not $CODE, right?

Yeah, totally glossed over that little bit. At the beginning of the routine, you should find a way to save the original SP (if you don't know how much you're going to change it), then move it so that it points to $C0DE. Keep calling POP and when you're done, restore SP. The easiest method would be to use ADD SP+n to subtract and add values to SP ("n" here is still a signed byte). E.g.

Code:

... Assume SP is at $C006
... Assume we only PUSH $DEAD $C0DE

LD HL, $DEAD
PUSH HL
LD HL, $C0DE
PUSH HL


CALL mysubroutine
    ... SP is now at $C000, with $C000-$C001 being the return address, $C002-$C003 being $C0DE, and $C004-$C005 being $DEAD
    ... At the beginning move SP ourselves
    ADD SP + 2

    POP HL
    ... Do something with the pointer

    POP HL
    ... Do something again

    ADD SP -6
    RET
    ... SP will be $C006, but we need $C000 before we return

Last edited by Shonumi (2016-08-04 19:58:03)

Offline

 

#10 2016-08-04 21:44:55

DonaldHays
Member
From: Seattle
Registered: 2016-08-01
Posts: 36
Website

Re: New to Game Boy programming, made a version of Snake

AntonioND wrote:

"ld hl,[sp+n]" doesn't exist, it's "ld hl,sp+n". It just loads to HL the value of SP+n, not the value in that address.

Oh my. That makes sense, and I didn't even think of that. HL is 16-bit, and SP+n would be a 16-bit address, but [sp+n] would be an 8-bit value, dereferenced from a pointer, so it doesn't really make sense to be putting that in a 16-bit register. So, yeah, should have thought of that. I was thrown off by the CPU instruction sheet on devrs.com , and the 1.0 version of the Game Boy crib sheet (which, looking at now, was fixed in the 1.1 version, but I didn't download that because I thought it was just a color version of the same document, not something that had fixes from the older version).

That adds an extra step to working with stack arguments, since after using that instruction you'll only have an address to stack memory, and you need to take an extra step to dereference whatever is pointed at underneath.

Thanks for pointing that out!

Offline

 

#11 2016-08-05 13:54:20

AntonioND
Member
Registered: 2014-06-17
Posts: 134
Website

Re: New to Game Boy programming, made a version of Snake

Yeah, the homebrew GB documentation isn't the most accurate in the world...

Offline

 

Board footer

Powered by PunBB
© Copyright 2002–2005 Rickard Andersson