aboutsummaryrefslogtreecommitdiff
path: root/README.md
blob: 80d9274a11db4c3fed3853070eeaece956862874 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
# 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. 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 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 these 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 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.

``` assembly
.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
or memory addresses which are not directly tied to the CPU (i.e. memory-mapped
I/O), use upper case in the same way you would in C.

``` assembly
lda PPU::m_control ; bad!
lda PPU::CONTROL   ; good!

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 this 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.

## 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, and also 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 both 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. 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.

## 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>.