diff options
| -rw-r--r-- | .editorconfig | 13 | ||||
| -rw-r--r-- | README.md | 517 |
2 files changed, 530 insertions, 0 deletions
diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c420a82 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +indent_size = 4 +insert_final_newline = true +end_of_line = lf + +[*.{s,S}] +indent_style = space + +[Makefile] +indent_style = tab 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>. |
