aboutsummaryrefslogtreecommitdiff

Prelude

This guide is just a set of rules I have been cooking up while hacking on the NES/Famicom. These are rules which I believe that have made things easier for me on NES/Famicom development, but it's not a general recommendation nor something to be enforced. That is, it's just a set of ideas I try to follow in order to be consistent mainly with myself. Hence, it's not complete and I can still be persuaded away from things I say here. All in all, take all of this with a grain of salt and assume that if I write something that looks fishy, maybe it's just that I don't know any better. If that's the case, then just drop me an email. That is to say, discussions are welcome, even if I can always just say no.

Last but not least: get to know the NES/Famicom first! This is not a reference guide nor a list of pitfalls that should be avoided. For all of this, just refer to the NESDev wiki.

Programming language

There are multiple ways to code on the NES/Famicom. I have seen helpful and insightful projects which have used C as a programming language. That being said, the NES/Famicom is really scarce when it comes to resources. Hence, if you can, you should try to make the most out of it and write everything in MOS 6502 assembly.

This is not a comment against C, but as clever and helpful as tools like cc65 can be, they can never quite reach the level of optimization over what the machine is executing as assembly. Couple that with the fact that MOS 6502 assembly is not that hard to learn, and that most learning resources are also given in assembly.

That being said, one good argument could be made that you could write the most performance critical bits in assembly and leave the business code in C so it's easier to understand. That certainly is a possibility but in the end the mixing of both languages can go wrong in different and subtle ways, makes building, linking and debugging more complex, and, in my humble opinion, for not too much to gain. Or at least that's how I feel, and programming in C might just be fine for some people, but I prefer doing everything in assembly on NES/Famicom development.

That's why for the rest of this guide we will be talking about assembly and never about C or any other language. For the 6502 family of processors there are multiple assemblers with slightly different syntax. Here I stick with ca65.

Source code layout

Encoding

Use UTF-8 as the source file encoding. For the code itself you should stick to good ol' ASCII, but for comments and sharing your code around, just use UTF-8 which is supported virtually everywhere.

Indentation, tabs vs spaces

Use only spaces for indentation, no hard tabs. Each indentation level is 4 spaces long. Hence:

bad:
  lda #1

good:
    lda #1

Introduce a new indentation level after any label:

function1:
lda #1       ; bad!

function2:
    lda #1   ; good!

That being said, if the label exists in relation to another one, then it ought to be kept at the same indentation level as the other label. For example, labels for control flow inside of a function:

foo:
    lda #1
    @label:     ; bad!
    jmp @label

foo:
    lda #1
@label:         ; good!
    jmp @label

That is, @label exists in relation to foo, and so it should be at the same indentation level. The same would apply to a function which has some pre-computed data for some of its logic:

function1:
    ldx #$00
    lda data, x
    ;; Do something
    rts
data:
    .byte $01, $02

The rationale for this is that labels ought to be clearly visible, and by being at a different indentation level as instructions they certainly stick out. Moreover, note that every instruction for a function is kept at the same indentation level regardless of its flow. The recommended @ prefix for labels (see Naming conventions) further disambiguates with the name of the function as well.

One could argue that labels for control flow could be put at the same indentation level, and hence you would be able to clearly denote where loops or branches are inside of a given function. Hence, having something such as:

function1:
    lda #1
    @loop:
        ldx #00
        jmp @loop
    rts

I have the following disagreements with this approach:

  1. It artificially makes it look like a higher-level programming language (e.g. there is no lexical scoping).
  2. I feel like labels (e.g. @loop in the example above) are easier to miss.
  3. Having multiple indentation levels is already a code smell: avoid too much complexity on your functions as you ought to be as performant as possible.

In a similar spirit, introduce a new indentation level inside of .proc, .macro, .repeat, .if and similar control statements which expect a block of code inside. Thus:

.proc bad
lda #1
.endproc

.proc good
    lda #1
.endproc

Last but not least, and again for clarity's sake, labels are to be put on their own line:

bad: lda #1
good:
    lda #1

;; Yes, data too.
bad: .byte $02
good:
    .byte $02

Line endings and the likes

Let's get simple statements out of the way:

  • Limit lines to 80 characters.
  • No trailing whitespace.
  • Use Unix-style line endings.
  • End each file with a newline.

I will not bother to further explain on the above, as others have wasted more time on this than me on these arguments. It can be easily configured through your editor (and this repository also holds an .editorconfig to help you on this). If your editor doesn't support some of these options, just replace it.

Numeric literals

In 6502 assembly you can express numeric literals in decimal, hexadecimal and binary formats. One useful rule I have been developing over the course of programming in 6502 assembly is:

  1. Prefer the hexadecimal format: debuggers, emulators, ROM dumps, and related tooling will default to this format.
  2. Use the decimal format for simple numbers which can be trivially translated into hexadecimal format, but which are more simple to write this way (e.g. lda #0).
  3. Use binary format for bitmap masks, or other arrangements where each bit has been set/unset following a very strict order (e.g. preparing the value for a PPU register). As for masks, it might be quite trivial to mentally parse which bits are set/unset with something like lda #$81, but something like lda #$AC might take more time to mentally parse than the more explicit lda #%10101100.

Allocation conventions

I am not going to reinvent the wheel here: just stick to the comments on the NESDev wiki, or the sample RAM map on how to allocate memory on the NES/Famicom.

In general, you should be very mindful when placing your data, and note that because of the MOS 6502 architecture, there is a noteworthy difference between placing data on the zero page or not. Hence, just to reiterate: ensure that the data you use more often is placed on the zero page. Note that the Calling conventions further reiterate on this fact.

Mainly because of this, and in stark contrast to many other code bases, avoid using the .res control statement for "variables". Hence:

bad_var:
    .res 1

good_var = $01

Using the .res control statement has two main benefits:

  1. The compiler can enforce that you don't go over the capacity for a given segment.
  2. You can add/remove variables without too much hassle.

But it also has its drawbacks:

  1. You don't know where data is placed. This is important when debugging, where you have to watch for a specific address. Hence, you'd need to manually compute anyways the address for a variable (multiple times if you have added/removed variables since the last time), while for good_var you already know where it is located.
  2. The .res statement guarantees that the data will be zero'ed out (or with the given optional fill value). This is not possible for variables, as "memory" will not be a part of the ROM file (for obvious reasons). Hence, you still need to take care of initializing these variables. By using the .res statement you are being misleading on how things work. This is because the .res statement is meant to be used for stuff that will actually appear on the ROM file, not for "variables" in memory.

All of that being said, the first benefit that we pointed out is not to be overlooked, but it can arguably be achieved via tooling as well. That's what I'm doing with the "address sanitizer" in tools.nes.

Naming conventions

Use snake_case everywhere.

badThing:
    .byte $01

good_thing:
    .byte $01

Only use upper case for macros or regular constants (as detailed below). For the rest of your code stick to lower case and allow syntax highlighting on modern editors do the rest. Hence:

LDA #01     ; bad!
lda #01     ; good!

One good idea for .macro is to use them to define pseudo-instructions, as it's done in other assembly languages like RISC-V. For example, on these scrolling examples there is a macro for the pseudo-instruction JAL, which makes explicit when a jmp has been used instead of jsr for reducing the stack usage on tail calls. In this scenario, you can find code like this:

.proc foo
    ;; Previous code that might jump/branch to @end.

    lda Some::Variable
    bne @end
    JAL prepare_next_column
@end:
    rts
.endproc

Here the casing makes explicit that JAL is actually a pseudo-instruction, and so that it follows somewhat different rules than the ones by its side.

Use the @ prefix for named labels which affect the control flow

Named labels which are part of the control flow are to use the @ symbol as a prefix for their names. This clearly denotes which labels are actually part of the control flow. Thus, labels which reference a piece of data should not be prefixed with @, but labels which are part of the flow of branching/jumping should be prefixed accordingly.

;; bad
loop:
    ldx #0
    lda @data, x
@data:
    .byte $00

;; good
@loop:
    ldx #0
    lda data, x
data:
    .byte $00

Use memory-explicit prefixes for variables

Just to reiterate over what was said on Allocation conventions: be very mindful on where data is placed. On this, the name of "variables" can also help out, and more so on code that is accessing it but it's far from where it was initialized or declared. Hence, the context might have been lost and you might not be fully aware on what kind of data you are operating on. So, use the following prefixes:

  • zp_ for variables on the zeropage.
  • wr_ for variables on "Working RAM".
  • m_ for the rest.

Consider the following code:

lda <zp_metatiles

By simply looking at this you quickly know that:

  1. It's on zeropage, because it's information that will be retrieved often.
  2. It's not random data that you have on ROM space.

Even if the name turns out to not be too flashy, it's imperative when writing assembly code to be as clear as possible.

UPPER_CASE for macros and regular constants

In a similar spirit as many code styles for C, use UPPER_CASE for macros, as that makes it more apparent what they are.

.macro Bad
    lda #1
.endmacro

.macro GOOD
    lda #1
.endmacro

In a similar way, there are assignments which are simply a way to name regular numeric constants. This differs to how "variables" are assigned as they are not memory addresses that the CPU can directly access. Hence, for constant numbers use upper case in the same way you would in C.

bad_constant  = 1
GOOD_CONSTANT = 1

Calling conventions

Do you really need this function?

First and foremost, ask yourself whether you really need to have some block of code as a function. This is because instructions like jsr and rst come at a cost. The cost might be well worth it if it's a function you call several times and which is at least medium sized. But some other times that particular function might actually not be that desirable and a macro might suffice.

Hence, before evaluating how you are going to call a function, ask yourself if you really need a function to begin with.

Refrain from a pure calling convention

Since you need to be as optimal as possible, sometimes using just registers will suffice, while other times using the memory might be needed. In any case:

  1. Be consistent on your calling convention.
  2. Documentation is important. Don't be afraid to clearly denote which registers or memory addresses might be touched after calling a given function
  3. Do not be afraid to hint on the function's name how you can call it. This is better explained in the following sections.

Don't expect resources to be preserved across calls

As a general rule, as a caller don't expect values on registers or memory regions to survive to a function call. If there is important information to be kept or updated across calls, make sure to shadow it on memory addresses which are guaranteed to not be touched (on that check on the function's documentation!). Another option is to use the stack, but do so with caution as stack overflows are a real danger here.

Reserve return values and arguments into zeropage

It is generally a good idea to reserve some bytes for argument passing. For example, one might reserve $00-$04 to variables named like zp_arg0 to zp_arg4. These memory arguments can then be consumed/owned in different ways. In any case, be consistent and document this clearly on each function.

Using the x or y registers for code that has to "select"

One handy convention is to use the x or y registers as selectors. Refer to something like this:

ldx #0
jsr read_controller_x

Reading from one controller or the other is the same but the x register can be used to select which controller to read. In general, this is a pattern in which the x or the y registers are used as a "select" for the function's code. This pattern can also be reproduced in functions that perform bank switching, for example, as a way to index the bank to be selected.

This can be further documented by adding a suffix into the function's name, as it's done in the above example. In this example, adding a suffix like _x is already telling the caller that the x register is involved on this scenario.

Flow of control

Use .proc for functions

The .proc control statement guarantees a new lexical scope, so named labels will not clash with named labels from other scopes. There are (almost) no downsides to it.

bad:
    rts

.proc good
    rts
.endproc

I say "(almost)" as I have seen at least one situation in which discarding any kind of scoping could be useful if performance was paramount. That is, imagine a 16-bit pointer to a function that can be set to different handlers for enemy movement. This could be done in multiple ways, but the most performant way would be to just jmp (pointer), and then let each handler perform a jmp @known_return_address instead of a mere rts. Hence, if you are on corner cases like these, I think it's fair to go the optimal route. In other cases, prefer readability.

Moreover, note that tooling might perform better if the programmer states explicitely that this is indeed a subroutine and not a mere label. This way, tooling can inspect where a subroutine starts and ends, and aid the programmer in different ways.

Otherwise, labels should only be used for control flow and referencing data on ROM space.

Avoid too many anonymous labels

Anonymous labels are fine when you have situations such as:

    lda #whatever
    beq :+
    ldx #$FF
:
    inx

Compilers like cc65 allow for branching into multiple anonymous labels ahead/back, but having statements like beq :+++ can quickly become troublesome, and more so if they are multiple instructions apart. Hence, stick to one or two + or - characters maximum. In the same spirit, consider not having too many anonymous labels in your code:

  1. They are more difficult to track than named ones.
  2. They say nothing about your control flow.

Hence, anonymous labels are meant for quick if-else clauses or similar minimalistic cases in which they are easy to tell apart. Otherwise refrain from using them and give them a name.

When to .macro and when to .proc

Try to find a good balance between .proc and .macro, as they both have benefits and drawbacks. In particular, if a .proc is just a couple of instructions long, this is already a code smell as jsr and rts instructions are not for free, but at the same time having a .macro used in many places and that unrolls into several instructions might also not be desirable. Hence, be mindful, apply common sense, and measure things when in doubt.

Scopes and macros

Using .scope is a good thing and you should take advantage of it as much as possible. But bear in mind that .macro statements disregard scopes as they will always be placed in the global scope. For this reason you should always write .macro statements in the global scope as embedding them into .scope or .proc is simply misleading.

Managing assets

Order of pattern tables

The NES/Famicom is quite open on which pattern table you are using for background tiles and which for sprite tiles. That being said, prefer using background tiles for the first pattern table and sprite tiles for the second pattern table. This is because some mappers like MMC3 have some IRQ quirks that can be avoided by following this order.

Project layout

This is a tough cookie and it mostly boils down to how your game is structured. That being said, there are certain things you should consider.

Build system

Make sure you have a Makefile at the root of your project. It's far easier for someone to simply pull your project, call make and have the ROM file in some out directory. Avoid custom or complex build systems, as that "someone" might just be you in the future and using another computer.

You could also envision some dependency tooling (e.g. CMake or Autotools) or something like that, but in all fairness building a project for the NES/Famicom shouldn't be that hard.

Last but not least, using a Makefile is much preferrable to scripts tailored to specific shells or operating systems. Hence, avoid build.sh, build.bat and similar nonsense.

Vendoring

Vendor all your dependencies in a vendor directory. Make sure that you can track where these dependencies come from, and at which revision they were pulled. For situations like this git submodule, even if not perfect, works wonders.

If you have specific patches for a given dependency, you can do two things:

  1. If it is as simple as calling sed to replace some memory address or something like that, embed it into your build system.
  2. If it's not that easy, fork that project, work with git (i.e. commit your patches), and add this fork as a dependency.

Again, you should strive to have a clear and easy build system: calling make should really be all that is needed to build your project. Again, your future self will appreciate it.

Separate library code and business code

This is something quite hard to achieve, but try to weed out code that is not strictly from your game in an include or lib directory. This way you can reuse this code for other games. For example, aliases for PPU addresses are good candidates: having a PPU::m_address with the value $2006 is helpful in any given game, for example.

That being said, don't go over the top. Certainly a function like reset can be quite similar in many games, but some might need specific tweaks for specific mappers, for example. Having a myriad of .ifdef or similar is not desirable.

Assets

Leave your assets into a specific directory named assets or similar. That is, do not put .chr files in the same directory as code or at the root of your project.

Moreover, some tools like NEXXT will save a session.nss file to your assets directory. Do not remove it nor ignore it via git, as it will help you everytime you go back to your chr files.

As a cherry on top, if you want tools to be more accurate on the statistics of your projects, you might want to to take a look at the given .gitattributes file. This file is telling git (and related porcelain/tools) to consider anything under the assets/ directory to be generated. This allows tools like linguist to perform more accurate reports.

License

This work is licensed under Creative Commons Attribution 4.0 International.