aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <msabate@suse.com>2024-04-16 22:46:31 +0200
committerMiquel Sabaté Solà <mikisabate@gmail.com>2025-02-04 21:12:11 +0100
commit4cfc79f145b7c620a2688f4a755d5e53c7ef0e9d (patch)
treed281bae20cfda9481188f08509aa1f6cac7f50ce
parent3c6a583190b827c0e88e535d486a4f191ee255c0 (diff)
downloadcode.nes-4cfc79f145b7c620a2688f4a755d5e53c7ef0e9d.tar.gz
code.nes-4cfc79f145b7c620a2688f4a755d5e53c7ef0e9d.zip
Vastly improve the documentation
This includes more clear explanations, images, etc. Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
-rw-r--r--README.md50
-rw-r--r--basics/README.md107
-rw-r--r--basics/flicker.s8
-rw-r--r--basics/sprite.s100
-rw-r--r--docs/flicker.gifbin0 -> 658484 bytes
-rw-r--r--docs/input.pngbin0 -> 33370 bytes
-rw-r--r--docs/space.gifbin0 -> 304309 bytes
-rw-r--r--docs/sprite.pngbin0 -> 22863 bytes
-rw-r--r--docs/unrom.pngbin0 -> 37125 bytes
-rw-r--r--space/README.md2
10 files changed, 184 insertions, 83 deletions
diff --git a/README.md b/README.md
index df49e70..d239e28 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,16 @@
-<p align="center">
- <a href="https://github.com/mssola/NES/actions?query=workflow%3ACI" title="CI status for the main branch"><img src="https://github.com/mssola/NES/workflows/CI/badge.svg" alt="Build Status for main branch" /></a>
-</p>
+Examples for the Famicom/NES. These examples are not full blown games nor
+standalone projects. Rather, they should be seen as learning material and
+experiments. If you want to check out standalone projects that I have written,
+check out [this list](#other-projects).
----
+## Build
-This repository consists of some examples of programs for the NES. You can build
-them by just calling `make` and they will then be available at the `out`
-directory. Before doing that, though, you will need a compiler for the 6052
-platform. A good option is [cc65](https://github.com/cc65/cc65), which is
-available on all major platforms. Otherwise, if you want to use another
-compiler, you can pass the `CC65` and `CCOPTS` variables to the Makefile.
+You can build everything by just calling `make` and binaries will then be
+available in the `out` directory. Before doing that, though, you will need a
+compiler for the 6052 platform. A good option is
+[cc65](https://github.com/cc65/cc65), which is available on all major platforms.
+Otherwise, if you want to use another compiler, you can pass the `CC65` and
+`CCOPTS` variables to the Makefile.
After that, it's recommended that you run the ROMs with an emulator with
debugging support or at least some form of memory visualization. This is because
@@ -19,33 +20,28 @@ NES memory. A safe bet is to go with either
[Mesen](https://github.com/SourMesen/Mesen2/), which provide tools like RAM
watchers or a full debugger.
-Notice also that I will use some terms without actually introducing them. That
-is, I expect you to go over the [NES Dev
-wiki](https://www.nesdev.org/wiki/Nesdev_Wiki) for glossary or for full
-documentation on the stuff being shown here. For example, if I am writing an
-example code using the UNROM or the MMC1 chips, I assume that you will go over
-the [NES Dev wiki](https://www.nesdev.org/wiki/Nesdev_Wiki) for details on what
-these chips actually are or how they were used. That is, if you find that on a
-bunch of comments I write stuff like "memory mapper", "MMC3 chip", "OAM" or
-stuff like that, just go to the [NES Dev
-wiki](https://www.nesdev.org/wiki/Nesdev_Wiki) to get a better picture.
+## Examples
The examples are distributed like this:
- `basics`: simple examples which cover basic stuff for NES development. These
examples are self-contained and supposed to be read by absolute newcomers. The
- description for each example is covered by an initial comment on each file.
+ description for each example is covered by an initial comment on each file,
+ and you can also find more info in the [basics/README.md](./basics/README.md)
+ file.
- `space`: example in which you can move a spaceship with subpixel movement and
- shoot bullets. Consider this an evolution from the `basics/sprite.s` example.
- That is, we are no longer just showing a sprite, but we make it move and
- perform an action like shooting bullets.
+ shoot bullets. Consider this an evolution from the `basics/sprite.s` and
+ `basics/input.s` examples. That is, we are no longer just showing a sprite,
+ but we make it move and perform an action like shooting bullets.
- `scroll`: different scrolling tactics. Read the
[scroll/README.md](./scroll/README.md) file for more info.
- `fx`: miscellanous effects that can be achieved with this humble machine.
-Other than that, I have also written complete games. Take a look at
-[jetpac.nes](https://github.com/mssola/jetpac.nes), which is simple enough so a
-newcomer can read it, but complex enough so it's an actual game.
+## Other projects
+
+- Full games: [jetpac.nes](https://github.com/mssola/jetpac.nes).
+- Libraries: [list.nes](https://github.com/mssola/list.nes).
+- Misc: [Advent of Code 2023](https://github.com/mssola/aoc2023.nes).
## License
diff --git a/basics/README.md b/basics/README.md
index 84d43a6..3ffb285 100644
--- a/basics/README.md
+++ b/basics/README.md
@@ -1,4 +1,4 @@
-## Basics
+## Showing a sprite and reading from a controller
Here we have simple programs which try to explain a particular topic of NES
programming. They are single files which already contain all you need in order
@@ -6,22 +6,97 @@ to have a working program. This means that there is quite a lot of boilerplate
you might not be aware. For this reason I encourage you to start with the
`sprite.s` file, which has a step by step explanation on how the NES is
initialized and what do all these magic "HEADER" and other jargon actually mean.
-Thus, for absolute beginners go to:
+Thus, read the file thoroughly, and when you build the example you will see the
+following:
-- `sprite.s`: detailed explanation on how to initialize the NES in order to have
- some background and a sprite shown on screen. Follow along the code in order
- to get a detailed explanation on each section.
-- `input.s`: how to read the input from one controller.
+![sprite.png](../docs/sprite.png)
-After this, you should be good to go for full examples like the one on the
-`space` directory, in which we take the example on `sprite.s` and make it move
-and shoot bullets depending on the given input.
+Now that you know how to show a sprite on screen, let's read a controller. This
+is demonstrated with the `input.s` file, but note that a safer and more
+comprehensive version of reading from a controller is shown in
+[shared/joypad.s](../shared/joypad.s). Read the `input.s` file, but note that
+nothing useful will be displayed on screen. Out of simplicity, you are required
+to just look at a RAM value. If you do so, just watch on the `$42` memory
+address, which contains how many times the right button has been pressed. This
+is an example with FCEUX showing a run which has pressed the right button three
+times:
-Whenever you are done with that, you can then move into other topics like:
+![input.png](../docs/input.png)
-- `flicker.s`: flickers sprites which are aligned so they don't disappear due to
- NES horizontal sprites limit.
-- `persist.s`: using the MMC1 chip in order to persist data.
-- `unrom.s`: bank switching using the UNROM chip.
-- `chr-ram.s`: how to show background and sprites via CHR-RAM instead of
- CHR-ROM. This is a combination of `sprite.s` and `unrom.s`.
+We could put these two examples together, and that's the role of the
+[space/](../space/) directory.
+
+## Flickering sprites
+
+The NES had a limitation which developers found out pretty quickly: there can
+only be 8 sprites on a single scanline. If this is not considered and more than
+eight sprites are on a given scanline, those "extra" sprites are going to be
+skipped from rendering. This is of course bad, and because of this most games
+implemented some logic to avoid it: either by placing sprites in clever ways, or
+using background elements as if they were sprites, or, more famously, by
+applying a flicker effect.
+
+A dumb flicker effect on the NES is pretty easy to achieve, it's a matter of
+mindlessly re-ordering sprites as found on the
+[OAM](https://www.nesdev.org/wiki/PPU_OAM). The PPU will respect this order and
+thus each sprite will be shown most of the times except for a few frames. To the
+human eye this looks like flickering images, and it's a technique which is used
+a lot on NES games. A dumb and conservative approach has been implemented in
+`flicker.s`, which looks like this:
+
+![flicker.gif](../docs/flicker.gif)
+
+A more clever way to implement this flickering effect stems from being more
+aware about the priorities of your game:
+
+1. Which sprites can never be "flickered"? Such as in this example, it might be
+ a good idea to leave the main character out of this flickering technique for
+ the player's convenience.
+2. Is there a way to lay out the OAM memory in a way in which we always get the
+ proper results without having to re-arrange objects on the OAM? Imagine a
+ very simple game such as [jetpac.nes](https://github.com/mssola/jetpac.nes),
+ in which we know in advance the slots for enemies, bonuses, etc.
+
+## Showing a sprite through CHR-RAM instead of CHR-ROM
+
+Rendering a sprite onto the screen is easy as shown on the `sprite.s` example,
+but loading sprites from ROM space might be in some cases not in our interest.
+This is a huge topic, covered on the [NESdev
+wiki](https://www.nesdev.org/wiki/CHR_ROM_vs._CHR_RAM). That is, some games, for
+a handful of very specific reasons, might want to load sprites first from ROM
+into RAM, and then only rely on RAM for manipulating and showing sprites on
+screen.
+
+The [UNROM](https://www.nesdev.org/wiki/UxROM) chip is possibly one of the first
+to ever implement this technique. Moreover, this is also the first chip to ever
+implement "bank switching". This is a way to map way more memory than the
+original hardware allowed, in which cartridges brought a multiplexer which
+selected one memory chip or another depending on the "bank" being selected. So,
+to begin with this, I have added the `unrom.s` example, which shows a very basic
+way to setup the UNROM chip, and how bank switching can be performed. The end
+result for this example has to be shown from the RAM viewer. For example, on
+FCEUX we can see:
+
+![unrom.png](../docs/unrom.png)
+
+These are the expected values as described on `unrom.s`. After that, we already
+have the foundations for `chr-ram.s`, which will use one of the banks for the
+assets, and then upon initialization move it to RAM. The result is the same as
+with `sprite.s`, but the goal of this example is to appreciate the technique and
+understand when it's useful.
+
+## Persisting memory
+
+Another area explored here is a way to persist data across runs. This was
+probably to be achieved via the Famicom Disk System, but once that was scrapped
+outside of Japan, a replacement had to be delivered for games built for the Disk
+System like 'The Legend of Zelda'. This solution came first in the form of the
+MMC1, the first memory mapper to support a memory region where persistence was
+guaranteed via a battery included into the cartridge itself.
+
+Again, there's not much to show other than RAM data, but at least FCEUX allows
+us to retrieve back the data that was explicitely persisted. That is, after you
+run this example, simply check on `$HOME/.fceux/sav/` (or at least that's on
+Linux). Hence, just run `hexdump -C $HOME/.fceux/sav/persist.sav`, and that will
+print the bytes stored starting from `$6000`. The first two bytes is data
+explicitely changed by `persist.s`.
diff --git a/basics/flicker.s b/basics/flicker.s
index 640817a..34fc312 100644
--- a/basics/flicker.s
+++ b/basics/flicker.s
@@ -34,7 +34,7 @@
sta $2001 ; PPUMASK
@main_game_loop:
- ;; NOTE: the logic is pretty simply: read the pad, move the player
+ ;; NOTE: the logic is pretty simple: read the pad, move the player
;; accordingly, and apply the flickering effect.
jsr joypad_read
jsr Diskun::update
@@ -121,9 +121,9 @@
adc #4
sta $40
- ;; We know that that last byte from the last sprite is held at $24F. Thus,
- ;; if the sprite pointer is already passed this point, we can break the
- ;; loop. Otherwise just carry on.
+ ;; We know that the last byte from the last sprite is held at $24F. Thus, if
+ ;; the sprite pointer is already passed this point, we can break the loop.
+ ;; Otherwise just carry on.
cmp #$50
bne @loop
diff --git a/basics/sprite.s b/basics/sprite.s
index 4e9c573..34f9dba 100644
--- a/basics/sprite.s
+++ b/basics/sprite.s
@@ -396,6 +396,12 @@ palettes:
;; init_nametable loads the relevant data to the nametable that is then going to
;; be used in order to build up the background.
+;;
+;; NOTE: this function is called after the PPU has been initialized, but NMIs
+;; are still disabled. This is important because you **cannot** write into PPU
+;; data outside of VBlank space (during `nmi` code). If you do so it will result
+;; into rendering glitches. This is better explained at the `scroll` examples,
+;; where VRAM buffering techniques are applied and explained.
.proc init_nametable
;; The general idea here is that the background defaults to the
;; "transparent" color, which is the first color on the palette. Then for
@@ -416,21 +422,38 @@ palettes:
bit $2002 ; PPUSTATUS
;; Load the first item for the background! Let's make sense of the values.
+ ;;
;; We first need to set the address that the PPU will use (that is, which
- ;; couple of bytes we need to pass to PPUADDR). This can be tricky, but
- ;; luckily some tools makes things easier for us. For example, the NEXXT
- ;; tool allows users to draw a NES screen and then it gives the offset
- ;; address that you can use for each drawn element. This way I got that the
- ;; offset for the first element was $00C8, which added to the base address
- ;; of $2000 (start of the first nametable), gives us the address $20C8.
- ;; Therefore, if I want this background element to be rendered in the
- ;; position I envisioned on this tool, I need to write $20C8 into PPUADDR.
- ;; NOTE: these are a total of two bytes to be loaded, for operations that
- ;; only support one byte at a time. Hence, we have to load byte by byte (in
- ;; little-endian format) and store them. As for the PPUDATA address, I need
- ;; to pass #$02 because that's the index inside of the CHR file of the star
- ;; in the second pattern table (check the PPUCTRL setting at the end of the
- ;; `main` function on why it's the second pattern table).
+ ;; couple of bytes we need to pass to PPUADDR). That is: which tile index is
+ ;; to be used from the nametable (read more about PPU Nametables here:
+ ;; https://www.nesdev.org/wiki/PPU_nametables). Think of this as the screen
+ ;; being set as a grid of 8x8 pixels (tile), and each of these tiles is
+ ;; indexed in PPU memory: 960 bytes of actual data, and 64 bytes for the
+ ;; attribute table (see more on attribute tables below). You can draw this
+ ;; by hand by using resources like Morhpcat's developer spreadsheet
+ ;; (https://ko-fi.com/s/ad5d7601e0), based on the ones being used at
+ ;; Nintendo. This is good but there are also handy tools that make things
+ ;; easier for us. For example the NEXXT tool allows users to draw a full NES
+ ;; screen and then it gives the offset address that you can use for each
+ ;; drawn element. I did this and I got that the offset for the first element
+ ;; was $0C8, which added to the base address of $2000 (start of the first
+ ;; nametable), gave me the address $20C8. If you want to make more sense of
+ ;; it by just reading the number, notice that each row has exactly 32 tiles.
+ ;; Thus, you can divide 0x0C8 by 32 and you will get 6.25: that is, on the
+ ;; vertical axis it's the row '6'. Then subtract 0xC8 by 192 (6 rows of 32
+ ;; tiles) to get '8' as the value on the horizontal axis. As you can see,
+ ;; doing things manually can be tedious, that's why people either use a dev
+ ;; spreadsheet or a computer assisted tool.
+ ;;
+ ;; Anyways, if I want this background element to be rendered in the position
+ ;; I envisioned on this tool, I need to write $20C8 into PPUADDR, which adds
+ ;; for a total of two bytes to be loaded for operations that only support
+ ;; one byte at a time. Hence, we have to load byte by byte (in little-endian
+ ;; format) and store them. As for the PPUDATA address, I need to pass #$02
+ ;; because that's the index inside of the CHR file of the star in the second
+ ;; pattern table (check the PPUCTRL setting at the end of the `main`
+ ;; function on why it's the second pattern table). You can check that by
+ ;; opening the `.chr` file for this example on YY-CHR.
lda #$20
sta $2006 ; PPUADDR
lda #$C8
@@ -453,26 +476,25 @@ palettes:
;; Now everything we wanted to write for the background is there, but we
;; might want to toggle the colors for some of the elements. You can do this
;; with palettes, which for background tiles is done via attribute tables.
- ;; Each attribute table lives right after each nametable, and it consists of
- ;; 1KB of memory in which you can establish the palettes to be used for the
- ;; tiles appearing on the background screen as defined on that specific
- ;; nametable. Information on this is also given to us by tools like NEXXT.
- ;; Shortly, for one of the elements I want to change their palette I was
- ;; told that the "attribute offset" was $03CE. Again, added to the base
- ;; address, it means that we need to pass $23CE to the PPUADDR. The value
- ;; for PPUDATA is a bit more tricky: on the attribute table each 8x8 tile is
- ;; broken down into 4 2-sized squares. Each of these 4 squares of the tile
- ;; has a palette assigned to it ($00 by default). Thanks to this setup, a
- ;; single byte can encode four palettes by addressing each square:
- ;; %44332211. The tool also tells us in which of these smaller squares our
- ;; element resides. With this info, and since we have two bits available for
- ;; each of these smaller squares and, therefore, can encode up to 4
- ;; palettes, it means that we just have to assign either on the range of
- ;; 00-11 to the pair of bits representing the smaller square that NEXXT is
- ;; telling us that our background element resides in. In this case it's the
- ;; first square, so we just need to assign $01 to the bits reserved to this
- ;; square if I want to change the palette to 1. This is what we pass as a
- ;; value.
+ ;; Each attribute table lives right after each nametable, in which you can
+ ;; establish the palettes to be used for the tiles appearing on the
+ ;; background screen as defined on that specific nametable. Information on
+ ;; this is also given to us by tools like NEXXT. Shortly, for one of the
+ ;; elements I want to change their palette I was told that the "attribute
+ ;; offset" was $03CE. Again, added to the base address, it means that we
+ ;; need to pass $23CE to the PPUADDR. The value for PPUDATA is a bit more
+ ;; tricky: on the attribute table each 8x8 tile is broken down into 4
+ ;; 2-sized squares. Each of these 4 squares of the tile has a palette
+ ;; assigned to it ($00 by default). Thanks to this setup, a single byte can
+ ;; encode four palettes by addressing each square: %44332211. The tool also
+ ;; tells us in which of these smaller squares our element resides. With this
+ ;; info, and since we have two bits available for each of these smaller
+ ;; squares and, therefore, can encode up to 4 palettes, it means that we
+ ;; just have to assign either on the range of 00-11 to the pair of bits
+ ;; representing the smaller square that NEXXT is telling us that our
+ ;; background element resides in. In this case it's the first square, so we
+ ;; just need to assign $01 to the bits reserved to this square if I want to
+ ;; change the palette to 1. This is what we pass as a value.
WRITE_PPU_DATA $23CE, %00000001
rts
@@ -529,6 +551,11 @@ initial_sprite_data:
;; rendering frame during the Vertical Blanking Interval (VBLANK). This time
;; span is pretty tight (around 2273 CPU cycles), so you better update
;; everything to be rendered before that if you don't want graphical glitches.
+;;
+;; NOTE: NMI code can be more complex as we want to do more complex stuff, but
+;; we ought to keep things as simple and fast as possible. You can take a look
+;; at the examples from `scroll` for more complex NMI code that have to handle
+;; stuff like VRAM buffering or setting other PPU registers.
;;;
nmi:
;; As mentioned on the `main` subroutine, rendering will be skipped until
@@ -595,8 +622,9 @@ irq:
;; copy the bitmap that you have generated through a program such as YY-CHR into
;; the CHR ROM. Note that not all games used the CHR-ROM for storing their
;; assets and instead used the PRG-ROM for that as well (e.g. The Legend of
-;; Zelda). There are multiple technical reasons to do this, but this falls out
-;; of the scope of this file and my expertise, to be honest.
+;; Zelda). Take a look at the `basics/chr-ram.s` example for more. There are
+;; multiple technical reasons to do this, but this falls out of the scope of
+;; this example.
;;;
.segment "CHARS"
.incbin "../assets/basic.chr"
diff --git a/docs/flicker.gif b/docs/flicker.gif
new file mode 100644
index 0000000..e55cd42
--- /dev/null
+++ b/docs/flicker.gif
Binary files differ
diff --git a/docs/input.png b/docs/input.png
new file mode 100644
index 0000000..b99ad8a
--- /dev/null
+++ b/docs/input.png
Binary files differ
diff --git a/docs/space.gif b/docs/space.gif
new file mode 100644
index 0000000..3bfb5de
--- /dev/null
+++ b/docs/space.gif
Binary files differ
diff --git a/docs/sprite.png b/docs/sprite.png
new file mode 100644
index 0000000..7413075
--- /dev/null
+++ b/docs/sprite.png
Binary files differ
diff --git a/docs/unrom.png b/docs/unrom.png
new file mode 100644
index 0000000..f0fb3a4
--- /dev/null
+++ b/docs/unrom.png
Binary files differ
diff --git a/space/README.md b/space/README.md
index 8a0b451..cf59034 100644
--- a/space/README.md
+++ b/space/README.md
@@ -8,3 +8,5 @@ The movement is done by taking into consideration subpixels. For this, I have
taken most of the code/idea from [NES
Hacker](https://github.com/NesHacker/PlatformerMovement). Thus, this part is
mostly attributed to him.
+
+![space demo](../docs/space.gif)