aboutsummaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mikisabate@gmail.com>2025-02-08 22:55:01 +0100
committerMiquel Sabaté Solà <mikisabate@gmail.com>2025-02-09 22:25:27 +0100
commit95f19f4400b77e4008676731f34b1ea88d07054f (patch)
tree478028fd61f2a3d4a7405af48f05887c4755527b /README.md
downloadstyle.nes-95f19f4400b77e4008676731f34b1ea88d07054f.tar.gz
style.nes-95f19f4400b77e4008676731f34b1ea88d07054f.zip
Initial commit
Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
Diffstat (limited to 'README.md')
-rw-r--r--README.md517
1 files changed, 517 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..20d9319
--- /dev/null
+++ b/README.md
@@ -0,0 +1,517 @@
+# Prelude
+
+This guide is not something too serious. It's not a guide meant to be enforced,
+nor something to even be trusted. On the contrary, it's 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 definitive. 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 and I might be able to be convinced via a [Github
+issue](https://github.com/mssola/style.nes/issues). That is to say, discussions
+are welcome, even if I can always just say no.
+
+Last but not least: get the 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](https://www.nesdev.org/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](https://github.com/cc65/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 or debug. That certainly is a possibility, but in the end
+the mixing of both languages can go wrong in different and hidden ways, makes
+building and linking more complex, and, in my humble opinion, for not too much
+to gain. Hence, just stick with assembly and you should be fine.
+
+That's why for the rest of this guide we will be talking about assembly and
+never about C or any other languages.
+
+# 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:
+
+``` assembly
+bad:
+ lda #1
+
+good:
+ lda #1
+```
+
+A new indentation level is to be given after the start of a function:
+
+``` assembly
+function1:
+lda #1 ; bad!
+
+function2:
+ lda #1 ; good!
+```
+
+Labels, though, are to be kept at the previous indentation level:
+
+``` assembly
+function1:
+ lda #1
+ @label: ; bad!
+ jmp @label
+
+function2:
+ lda #1
+@label: ; good!
+ jmp @label
+```
+
+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. The
+recommended `@` prefix for labels (see [Naming
+conventions](#naming-conventions)) disambiguates with the name of the function
+clearly as well. For a similar reason labels are to be put on their own line:
+
+``` assembly
+bad: lda #1
+good:
+ lda #1
+
+;; Yes, even data.
+bad: .byte $02
+good:
+ .byte $02
+```
+
+Introduce a new indentation level inside of `.proc`, `.macro`, `.repeat`, `.if`
+and similar control statements which expect a block of code inside. Thus:
+
+``` assembly
+.proc bad
+lda #1
+.endproc
+
+.proc good
+ lda #1
+.endproc
+```
+
+One could argue that after a label we could introduce another indentation level.
+That is certainly tolerable, but I have the following disagreements with this
+approach:
+
+1. It artificially makes it look like a higher programming language (e.g. there
+ is no lexical scoping).
+2. 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.
+
+## 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](./.editorconfig)
+to help you on this). If your editor doesn't support some of this options, just
+replace it.
+
+# Allocation conventions
+
+I am not going to reinvent the wheel here: just stick to the comments on the
+[NESDev wiki](https://www.nesdev.org/wiki/CPU_memory_map), or the [sample RAM
+map](https://www.nesdev.org/wiki/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](#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:
+
+``` assembly
+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. On that note
+I'm working on an "address sanitizer" in
+[mssola/tools.nes](https://github.com/mssola/tools.nes) which could take care of
+listing which slots are available or which slots have been already been taken.
+
+# Naming conventions
+
+Use `snake_case` everywhere.
+
+``` assembly
+badThing:
+ .byte $01
+
+good_thing:
+ .byte $01
+```
+
+## 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 is in accordance to the style used virtually
+everwhere, and it 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.
+
+``` assembly
+;; bad
+loop:
+ ldx #0
+ lda @data, x
+@data:
+ .byte $00
+
+;; good
+@loop:
+ ldx #0
+ lda data, x
+data:
+ .byte $00
+```
+
+## Use memory-aware prefixes for variables
+
+Just to reiterate over what was said on [Allocation
+conventions](#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. Hence, use the following prefixes:
+
+- `zp_` for variables on the zeropage.
+- `wr_` for variables on "Working RAM".
+- `m_` for the rest.
+
+Consider the following code:
+
+``` assembly
+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 aliases to memory-mapped I/O
+
+In a similar spirit as many code styles for C, use `UPPER_CASE` for macros, as
+that makes it more apparent what they are.
+
+``` assembly
+.macro Bad
+ lda #1
+.endmacro
+
+.macro GOOD
+ lda #1
+.endmacro
+```
+
+In a similar way, there are some assignments which are simply aliases to
+memory-mapped I/O. These "variables" are to be defined also in upper case as
+they are quite special.
+
+``` assembly
+lda PPU::m_control ; BAD
+lda PPU::CONTROL ; GOOD
+```
+
+# 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 this function
+3. Do not be afraid to lay out on the name of the function how you can call it.
+ This is better explained in the following sections.
+
+## Registers are to be saved by the caller
+
+To make things easier when implementing functions, as a caller don't expect
+values on registers 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 considered like this:
+
+1. It's up to the caller to save values from these addresses if they are to be
+ preserved across calls. The called function might change these values during its
+ execution.
+2. The called function will save to `zp_arg0` the returned value, or to
+ `zp_arg1` if the returned value is 16-bit (little endian).
+3. From 1. and 2.; you can infer that if you are passing things through
+ `zp_argX`, everything is done in memory: you cannot mix registers and memory
+ arguments.
+
+As for the parameters being accepted, one might follow the same convention as
+it's done in places like the Linux kernel, in which equivalent C signatures are
+given as documentation for an assembly function. Moreover, it could also be a
+good idea to embed this information into the function's name. This can come at
+the cost of a darn ugly name, but at least there won't be any misunderstandings
+when using this function. So all in all, you could end up with something like:
+
+``` assembly
+;; Get a thing given the passed argument. It expects the value on memory and it
+;; will return a byte. You can assume this function to have the equivalent C
+;; signature:
+;;
+;; uint8_t get_thing_from_arg(uint8_t value)
+;;
+;; NOTE: `a` register is modified.
+.proc get_thing_from_arg
+ lda zp_arg0
+ ;; Do something with it
+
+ sta zp_arg0
+ rts
+.endproc
+```
+
+## Register-only strategies
+
+Sometimes just passing a value on the register is enough. Consider the following
+patterns.
+
+### Using the `a` register as input and output
+
+``` assembly
+lda #m_value
+jsr compute_next_value
+clc
+adc #m_base
+```
+
+Here `compute_next_value` expects only one argument, to be set in the `a`
+register. The result is also left in `a`. This is useful on pure functions that
+will not overwrite on other registers or on many memory addresses. That being
+said, the function should always state if it's touching other registers or other
+memory addresses. Moreover, in assembly the function signature is tightly
+coupled to how it will be called. Hence, consider how code elsewhere is calling
+this function as pulling data into the `a` register just to call a function can
+defeat the purpose of this optimization.
+
+### Using the `x` or `y` registers for code that has to "select"
+
+``` assembly
+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. Similarly, use the `x` or the `y`
+registers 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.
+
+## Memory arguments first, optimize with registers later
+
+In a similar fashion than the principle of "don't optimize early": prefer the
+calling convention through memory addresses first if you are not sure how a
+given function is going to be used in the end. You can later optimize to the
+register-only strategies if you are sure that you can make this optimization.
+This allows for the flexibility of memory addresses at first without the
+possible gotchas from register-only calls. Moreover, if you anticipate that a
+given function will be a core library one, it will probably be a good idea to
+stick with memory arguments, as they fit a more generic approach.
+
+# 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 really no
+downsides to it.
+
+``` assembly
+bad:
+ rts
+
+.proc good
+ rts
+.endproc
+```
+
+Hence, 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:
+
+``` assembly
+ lda #whatever
+ beq :+
+ ldx #$FF
+:
+ inx
+```
+
+Compilers like [cc65](https://github.com/cc65/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.
+
+# 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::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.
+
+# License
+
+This work is licensed under <a
+href="https://creativecommons.org/licenses/by/4.0/?ref=chooser-v1"
+target="_blank" rel="license noopener noreferrer">Creative Commons Attribution
+4.0 International</a>.