├── res ├── sprite.png ├── background.png └── screen-capture.png ├── demo.cfg ├── README.md ├── LICENSE ├── tools ├── demo_fceux_symbols.py └── chr_tool.py ├── src └── oam.s └── demo.s /res/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike42/nes-ball-demo/main/res/sprite.png -------------------------------------------------------------------------------- /res/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike42/nes-ball-demo/main/res/background.png -------------------------------------------------------------------------------- /res/screen-capture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike42/nes-ball-demo/main/res/screen-capture.png -------------------------------------------------------------------------------- /demo.cfg: -------------------------------------------------------------------------------- 1 | MEMORY { 2 | ZP: start = $00, size = $0100, type = rw, file = ""; 3 | OAM: start = $0200, size = $0200, type = rw, file = ""; 4 | RAM: start = $0300, size = $0400, type = rw, file = ""; 5 | HDR: start = $0000, size = $0010, type = ro, file = %O, fill = yes, fillval = $00; 6 | PRG: start = $8000, size = $8000, type = ro, file = %O, fill = yes, fillval = $00; 7 | CHR: start = $0000, size = $2000, type = ro, file = %O, fill = yes, fillval = $00; 8 | } 9 | 10 | SEGMENTS { 11 | ZEROPAGE: load = ZP, type = zp; 12 | OAM: load = OAM, type = bss, align = $100; 13 | BSS: load = RAM, type = bss; 14 | HEADER: load = HDR, type = ro; 15 | CODE: load = PRG, type = ro, start = $8000; 16 | RODATA: load = PRG, type = ro; 17 | VECTORS: load = PRG, type = ro, start = $FFFA; 18 | TILES: load = CHR, type = ro; 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NES ball demo 2 | 3 | This is a port of the classic Amiga bouncing ball demo to the Nintendo Entertainment System (NES). 4 | 5 | ![Start screen](res/screen-capture.png) 6 | 7 | ## Try it 8 | 9 | The most recent version is available in the [releases](https://github.com/mike42/nes-ball-demo/releases) section. It is shipped as an iNES ROM (`.nes`), which is suitable for use in a NES emulator or flash cartridge. 10 | 11 | ## Build it 12 | 13 | The steps to build this project are listed in `build.sh`. You will need `python3` and the `cc65` toolchain installed. 14 | 15 | ## License 16 | 17 | The code may be used, distributed and modified under the terms of the MIT license, see [LICENSE](https://github.com/mike42/nes-ball-demo/blob/master/LICENSE) for details. 18 | 19 | The code is based on a NES example project by Brad Smith, which can be found at [bbbradsmith/NES-ca65-example](https://github.com/bbbradsmith/NES-ca65-example). 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Michael Billington 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tools/demo_fceux_symbols.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | demo_fceux_symbols: Convert debug symbols to FCEUX-format. 4 | 5 | Based on https://github.com/bbbradsmith/NES-ca65-example 6 | """ 7 | import sys 8 | assert sys.version_info[0] >= 3, "Python 3 required." 9 | 10 | from collections import OrderedDict 11 | 12 | def label_to_nl(label_file, nl_file, range_min, range_max): 13 | labels = [] 14 | try: 15 | of = open(label_file, "rt") 16 | labels = of.readlines() 17 | except IOError: 18 | print("skipped: "+label_file) 19 | return 20 | labs = {} 21 | sout = "" 22 | for line in labels: 23 | words = line.split() 24 | if (words[0] == "al"): 25 | adr = int(words[1], 16) 26 | sym = words[2] 27 | sym = sym.lstrip('.') 28 | if (sym[0] == '_' and sym[1] == '_'): 29 | continue # skip compiler internals 30 | if (adr >= range_min and adr <= range_max): 31 | if (adr in labs): 32 | # multiple symbol 33 | text = labs[adr] 34 | textsplit = text.split() 35 | if (sym not in textsplit): 36 | text = text + " " + sym 37 | labs[adr] = text 38 | else: 39 | labs[adr] = sym 40 | for (adr, sym) in labs.items(): 41 | sout += ("$%04X#%s#\n" % (adr, sym)) 42 | open(nl_file, "wt").write(sout) 43 | print("debug symbols: " + nl_file) 44 | 45 | if __name__ == "__main__": 46 | label_to_nl("build/demo.labels.txt", "build/demo.nes.ram.nl", 0x0000, 0x7FF) 47 | label_to_nl("build/demo.labels.txt", "build/demo.nes.0.nl", 0x8000, 0xBFFF) 48 | label_to_nl("build/demo.labels.txt", "build/demo.nes.1.nl", 0xC000, 0xFFFF) 49 | -------------------------------------------------------------------------------- /tools/chr_tool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | chr_tool: Convert between PNG and CHR formats. 4 | """ 5 | import argparse 6 | import logging 7 | import os 8 | 9 | from PIL import Image 10 | from PIL.PngImagePlugin import PngImageFile 11 | 12 | 13 | def indexes_from_image(image: PngImageFile, sprite_idx: int) -> list: 14 | assert(0 <= sprite_idx <= 255) 15 | sprite_x = (sprite_idx % 16) * 8 16 | sprite_y = (sprite_idx // 16) * 8 17 | assert (0 <= sprite_x <= (128 - 8)) 18 | assert (0 <= sprite_y <= (128 - 8)) 19 | ret = [0 for _ in range(0, 64)] 20 | for y in range(0, 8): 21 | for x in range(0, 8): 22 | ret[y * 8 + x] = image.getpixel((x + sprite_x, y + sprite_y)) 23 | assert(len(ret) == 64) 24 | return ret 25 | 26 | 27 | def pixel_values_to_chr(values: list) -> bytes: 28 | assert(len(values) == 64) 29 | # First color bit 30 | ret_list = [0 for _ in range(0, 16)] 31 | for byte_id in range(0, 8): 32 | plane0_byte_val = 0 33 | plane1_byte_val = 0 34 | for pixel_id in range(0, 8): 35 | plane0_byte_val |= (values[byte_id * 8 + pixel_id] & 0x01) << (7 - pixel_id) 36 | plane1_byte_val |= ((values[byte_id * 8 + pixel_id] >> 1) & 0x01) << (7 - pixel_id) 37 | ret_list[byte_id] = plane0_byte_val 38 | ret_list[byte_id + 8] = plane1_byte_val 39 | return bytes(ret_list) 40 | 41 | 42 | def png_to_chr(png_file: str, chr_file: str): 43 | logging.info("PNG to CHR: {} {}".format(png_file, chr_file)) 44 | im = Image.open(png_file) 45 | # Do some checks 46 | if not isinstance(im, PngImageFile): 47 | logging.error("Expected PNG image file") 48 | return 1 49 | if im.size != (128, 128): 50 | # Note: Could change this code to handle any multiple of 8 pixels, but 128x128 is fairly conventional 51 | logging.error("Expected 128 x 128 image, got {}".format(im.size)) 52 | return 1 53 | pixel_range = im.getextrema() 54 | if pixel_range != (0, 3): 55 | # TODO lax this up a max and a min 56 | logging.error("Expected indexed data in the range (0, 3), got {}".format(pixel_range)) 57 | return 1 58 | # Load in each sprite 59 | sprite_pixel_values = [indexes_from_image(im, idx) for idx in range(0, 256)] 60 | # Convert to CHR-formatted sprites 61 | sprite_chr_data = [pixel_values_to_chr(x) for x in sprite_pixel_values] 62 | # Stitch together 63 | chr_file_content = b"".join(sprite_chr_data) 64 | assert(len(chr_file_content) == 4096) 65 | open(chr_file, "wb").write(chr_file_content) 66 | return 0 67 | 68 | 69 | def chr_to_pixel_values(chr_data: bytes): 70 | assert(len(chr_data) == 16) 71 | int_data = list(chr_data) 72 | pixel_values = [0 for _ in range(0, 64)] 73 | for pixel_id in range(0, 64): 74 | byte_id_1 = pixel_id // 8 75 | byte_id_2 = byte_id_1 + 8 76 | byte_1 = int_data[byte_id_1] 77 | byte_2 = int_data[byte_id_2] 78 | bit_id = 7 - (pixel_id % 8) 79 | bit_1 = (byte_1 >> bit_id) & 0x01 80 | bit_2 = (byte_2 >> bit_id) & 0x01 81 | idx = (bit_1 << 1) | bit_2 82 | pixel_values[pixel_id] = idx 83 | return pixel_values 84 | 85 | 86 | def add_indexes_to_image(image: Image, sprite_pixel_values: list, sprite_idx): 87 | assert(len(sprite_pixel_values) == 64) 88 | assert(0 <= sprite_idx <= 255) 89 | sprite_x = (sprite_idx % 16) * 8 90 | sprite_y = (sprite_idx // 16) * 8 91 | assert (0 <= sprite_x <= (128 - 8)) 92 | assert (0 <= sprite_y <= (128 - 8)) 93 | for y in range(0, 8): 94 | for x in range(0, 8): 95 | image.putpixel((x + sprite_x, y + sprite_y), sprite_pixel_values[y * 8 + x]) 96 | 97 | def chr_to_png(chr_file: str, png_file: str): 98 | data_bytes = open(chr_file, "rb").read() 99 | if len(data_bytes) != 4096: 100 | logging.error("Expected exactly 4096 bytes in {}, got {}".format(chr_file, len(data_bytes))) 101 | # Calculate per-sprite pixel values 102 | sprite_chr_data = [data_bytes[sprite_idx * 16:(sprite_idx + 1) * 16] for sprite_idx in range(0, 256)] 103 | sprite_pixel_values = [chr_to_pixel_values(x) for x in sprite_chr_data] 104 | # Set up new image and palette 105 | im = Image.new(mode="P", size=(128, 128), color=0) 106 | im_palette = [0 for _ in range(0, 768)] 107 | display_cols = [ 108 | (0x00, 0x00, 0x00), 109 | (0x7c, 0x7c, 0x7c), 110 | (0xbc, 0xbc, 0xbc), 111 | (0xfc, 0xfc, 0xfc) 112 | ] 113 | for x in range(0, 4): 114 | for y in range(0, 3): 115 | im_palette[x * 3 + y] = display_cols[x][y] 116 | im.putpalette(im_palette) 117 | # Paste in the sprites 118 | for sprite_idx in range(0, 256): 119 | add_indexes_to_image(im, sprite_pixel_values[sprite_idx], sprite_idx) 120 | im.save(png_file) 121 | 122 | 123 | def main(): 124 | parser = argparse.ArgumentParser(description='Tool to read and write NES CHR graphics data. PNG files must be a ' 125 | '2bpp pixel map at 128x128, while CHR files must be 4096 bytes each') 126 | parser.add_argument('--verbose', '-v', action='count', default=0) 127 | parser.add_argument('input', type=str, help='Input filename (required). If this ends with ".PNG" extension, ' 128 | 'then it will be read and converted to CHR. If it does not, ' 129 | 'then it will be read as a CHR file, and converted to PNG.', 130 | metavar='INPUT') 131 | parser.add_argument('--output', type=str, help='Output filename', metavar='OUTPUT') 132 | args = parser.parse_args() 133 | 134 | # Set log level 135 | if args.verbose > 1: 136 | log_level = logging.DEBUG 137 | elif args.verbose == 1: 138 | log_level = logging.INFO 139 | else: 140 | log_level = logging.WARNING 141 | logging.basicConfig(level=log_level) 142 | 143 | # Actually run 144 | if args.input.endswith('.png') or args.input.endswith('.PNG'): 145 | output_filename = os.path.splitext(args.input)[0]+'.chr' if args.output is None else args.output 146 | return png_to_chr(args.input, output_filename) 147 | else: 148 | output_filename = os.path.splitext(args.input)[0]+'.png' if args.output is None else args.output 149 | return chr_to_png(args.input, output_filename) 150 | 151 | 152 | if __name__ == "__main__": 153 | ret = main() 154 | exit(ret) 155 | -------------------------------------------------------------------------------- /src/oam.s: -------------------------------------------------------------------------------- 1 | ; 2 | ; Macros and routines for handling object attribute memory (OAM) 3 | ; 4 | 5 | ; Store A register to 8 OAM locations (row of sprite Y locations) 6 | .macro oam_row oam_base, sprite_base, offset 7 | sta oam_base+((0+sprite_base)*4)+offset 8 | sta oam_base+((1+sprite_base)*4)+offset 9 | sta oam_base+((2+sprite_base)*4)+offset 10 | sta oam_base+((3+sprite_base)*4)+offset 11 | sta oam_base+((4+sprite_base)*4)+offset 12 | sta oam_base+((5+sprite_base)*4)+offset 13 | sta oam_base+((6+sprite_base)*4)+offset 14 | sta oam_base+((7+sprite_base)*4)+offset 15 | .endmacro 16 | 17 | ; Store A register to 8 OAM locations and increment each time (row of tiles) 18 | .macro oam_row_inc oam_base, sprite_base, offset 19 | clc 20 | sta oam_base+((0+sprite_base)*4)+offset 21 | adc #1 22 | sta oam_base+((1+sprite_base)*4)+offset 23 | adc #1 24 | sta oam_base+((2+sprite_base)*4)+offset 25 | adc #1 26 | sta oam_base+((3+sprite_base)*4)+offset 27 | adc #1 28 | sta oam_base+((4+sprite_base)*4)+offset 29 | adc #1 30 | sta oam_base+((5+sprite_base)*4)+offset 31 | adc #1 32 | sta oam_base+((6+sprite_base)*4)+offset 33 | adc #1 34 | sta oam_base+((7+sprite_base)*4)+offset 35 | .endmacro 36 | 37 | ; Store A register to 8 OAM locations (column of sprites) 38 | .macro oam_col oam_base, sprite_base, offset 39 | sta oam_base+((0+sprite_base)*4)+offset 40 | sta oam_base+((8+sprite_base)*4)+offset 41 | sta oam_base+((16+sprite_base)*4)+offset 42 | sta oam_base+((24+sprite_base)*4)+offset 43 | sta oam_base+((32+sprite_base)*4)+offset 44 | sta oam_base+((40+sprite_base)*4)+offset 45 | sta oam_base+((48+sprite_base)*4)+offset 46 | sta oam_base+((56+sprite_base)*4)+offset 47 | .endmacro 48 | 49 | .segment "OAM" 50 | oam_1: .res 256 ; OAM for even frames 51 | oam_2: .res 256 ; OAM for odd frames 52 | 53 | .segment "ZEROPAGE" 54 | active_oam: .res 1 55 | 56 | .segment "CODE" 57 | sprite_y_oam_1: ; Set Y position of 64 sprites in OAM 1 58 | clc 59 | lda ball_y ; Row 0 60 | oam_row oam_1, 0, 0 61 | adc #8 ; Row 1 62 | oam_row oam_1, 8, 0 63 | adc #8 ; Row 2 64 | oam_row oam_1, 16, 0 65 | adc #8 ; Row 3 66 | oam_row oam_1, 24, 0 67 | adc #8 ; Row 4 68 | oam_row oam_1, 32, 0 69 | adc #8 ; Row 5 70 | oam_row oam_1, 40, 0 71 | adc #8 ; Row 6 72 | oam_row oam_1, 48, 0 73 | adc #8 ; Row 7 74 | oam_row oam_1, 56, 0 75 | rts 76 | 77 | sprite_y_oam_2: ; Set Y position of 64 sprites in OAM 2 78 | clc 79 | lda ball_y ; Row 0 80 | oam_row oam_2, 0, 0 81 | adc #8 ; Row 1 82 | oam_row oam_2, 8, 0 83 | adc #8 ; Row 2 84 | oam_row oam_2, 16, 0 85 | adc #8 ; Row 3 86 | oam_row oam_2, 24, 0 87 | adc #8 ; Row 4 88 | oam_row oam_2, 32, 0 89 | adc #8 ; Row 5 90 | oam_row oam_2, 40, 0 91 | adc #8 ; Row 6 92 | oam_row oam_2, 48, 0 93 | adc #8 ; Row 7 94 | oam_row oam_2, 56, 0 95 | rts 96 | 97 | sprite_tile_oam_1: ; Set tile of 64 sprites in OAM 1 98 | lda #0 ; Row 0 99 | oam_row_inc oam_1, 0, 1 100 | lda #16 ; Row 1 101 | oam_row_inc oam_1, 8, 1 102 | lda #32 ; Row 2 103 | oam_row_inc oam_1, 16, 1 104 | lda #48 ; Row 3 105 | oam_row_inc oam_1, 24, 1 106 | lda #64 ; Row 4 107 | oam_row_inc oam_1, 32, 1 108 | lda #80 ; Row 5 109 | oam_row_inc oam_1, 40, 1 110 | lda #96 ; Row 6 111 | oam_row_inc oam_1, 48, 1 112 | lda #112 ; Row 7 113 | oam_row_inc oam_1, 56, 1 114 | rts 115 | 116 | sprite_tile_oam_2: ; Set tile of 64 sprites in OAM 2 117 | lda #8 ; Row 0 118 | oam_row_inc oam_2, 0, 1 119 | lda #24 ; Row 1 120 | oam_row_inc oam_2, 8, 1 121 | lda #40 ; Row 2 122 | oam_row_inc oam_2, 16, 1 123 | lda #56 ; Row 3 124 | oam_row_inc oam_2, 24, 1 125 | lda #72 ; Row 4 126 | oam_row_inc oam_2, 32, 1 127 | lda #88 ; Row 5 128 | oam_row_inc oam_2, 40, 1 129 | lda #104 ; Row 6 130 | oam_row_inc oam_2, 48, 1 131 | lda #120 ; Row 7 132 | oam_row_inc oam_2, 56, 1 133 | rts 134 | 135 | sprite_x_oam_1: 136 | clc 137 | lda ball_x ; Row 0 138 | oam_col oam_1, 0, 3 139 | adc #8 ; Row 1 140 | oam_col oam_1, 1, 3 141 | adc #8 ; Row 2 142 | oam_col oam_1, 2, 3 143 | adc #8 ; Row 3 144 | oam_col oam_1, 3, 3 145 | adc #8 ; Row 4 146 | oam_col oam_1, 4, 3 147 | adc #8 ; Row 5 148 | oam_col oam_1, 5, 3 149 | adc #8 ; Row 6 150 | oam_col oam_1, 6, 3 151 | adc #8 ; Row 7 152 | oam_col oam_1, 7, 3 153 | rts 154 | 155 | sprite_x_oam_2: 156 | clc 157 | lda ball_x ; Row 0 158 | oam_col oam_2, 0, 3 159 | adc #8 ; Row 1 160 | oam_col oam_2, 1, 3 161 | adc #8 ; Row 2 162 | oam_col oam_2, 2, 3 163 | adc #8 ; Row 3 164 | oam_col oam_2, 3, 3 165 | adc #8 ; Row 4 166 | oam_col oam_2, 4, 3 167 | adc #8 ; Row 5 168 | oam_col oam_2, 5, 3 169 | adc #8 ; Row 6 170 | oam_col oam_2, 6, 3 171 | adc #8 ; Row 7 172 | oam_col oam_2, 7, 3 173 | rts 174 | 175 | sprite_attr_oam_1: 176 | ; attributes * 64 177 | lda #%00000000 ; no flip 178 | oam_row oam_1, 0, 2 179 | oam_row oam_1, 8, 2 180 | oam_row oam_1, 16, 2 181 | oam_row oam_1, 24, 2 182 | oam_row oam_1, 32, 2 183 | oam_row oam_1, 40, 2 184 | oam_row oam_1, 48, 2 185 | oam_row oam_1, 60, 2 186 | rts 187 | 188 | sprite_attr_oam_2: 189 | ; attributes * 64 190 | lda #%00000000 ; no flip 191 | oam_row oam_2, 0, 2 192 | oam_row oam_2, 8, 2 193 | oam_row oam_2, 16, 2 194 | oam_row oam_2, 24, 2 195 | oam_row oam_2, 32, 2 196 | oam_row oam_2, 40, 2 197 | oam_row oam_2, 48, 2 198 | oam_row oam_2, 60, 2 199 | rts 200 | 201 | setup_oam: 202 | jsr sprite_tile_oam_1 203 | jsr sprite_attr_oam_1 204 | jsr sprite_tile_oam_2 205 | jsr sprite_attr_oam_2 206 | lda #0 207 | sta anim_frame 208 | jsr setup_frame 209 | rts 210 | 211 | setup_frame: 212 | lda anim_frame 213 | and #%00000010 214 | cmp #%00000010 215 | beq @odd_frame 216 | lda #>oam_2 217 | sta active_oam 218 | jmp @end_frame_branch 219 | @odd_frame: 220 | lda #>oam_1 221 | sta active_oam 222 | @end_frame_branch: 223 | lda anim_frame 224 | and #%00000100 225 | cmp #%00000100 226 | beq @pal1_frame 227 | ; Second palette 228 | lda #$20 229 | ldy #17 230 | sta palette, Y 231 | lda #$16 232 | ldy #19 233 | sta palette, Y 234 | jmp @end_pal_branch 235 | @pal1_frame: 236 | ; Second palette 237 | lda #$16 238 | ldy #17 239 | sta palette, Y 240 | lda #$20 241 | ldy #19 242 | sta palette, Y 243 | @end_pal_branch: 244 | rts 245 | 246 | update_ball: 247 | jsr setup_frame 248 | jsr sprite_x_oam_1 ; Set sprite locations 249 | jsr sprite_y_oam_1 250 | jsr sprite_x_oam_2 ; Set sprite locations 251 | jsr sprite_y_oam_2 252 | rts 253 | -------------------------------------------------------------------------------- /demo.s: -------------------------------------------------------------------------------- 1 | ; demo.s - bouncing ball demo for the NES. 2 | ; 3 | ; iNES header 4 | ; 5 | 6 | .segment "HEADER" 7 | 8 | INES_MAPPER = 0 ; 0 = NROM 9 | INES_MIRROR = 1 ; 0 = horizontal mirroring, 1 = vertical mirroring 10 | INES_SRAM = 0 ; 1 = battery backed SRAM at $6000-7FFF 11 | 12 | .byte 'N', 'E', 'S', $1A ; ID 13 | .byte $02 ; 16k PRG chunk count 14 | .byte $01 ; 8k CHR chunk count 15 | .byte INES_MIRROR | (INES_SRAM << 1) | ((INES_MAPPER & $f) << 4) 16 | .byte (INES_MAPPER & %11110000) 17 | .byte $0, $0, $0, $0, $0, $0, $0, $0 ; padding 18 | 19 | ; 20 | ; CHR ROM 21 | ; 22 | 23 | .segment "TILES" 24 | .incbin "build/background.chr" 25 | .incbin "build/sprite.chr" 26 | 27 | ; 28 | ; vectors placed at top 6 bytes of memory area 29 | ; 30 | 31 | .segment "VECTORS" 32 | .word nmi 33 | .word reset 34 | .word irq 35 | 36 | .include "src/oam.s" 37 | 38 | ; 39 | ; reset routine 40 | ; 41 | .segment "CODE" 42 | reset: 43 | sei ; mask interrupts 44 | lda #0 45 | sta $2000 ; disable NMI 46 | sta $2001 ; disable rendering 47 | ; 48 | ; sta $4015 ; disable APU sound 49 | ; sta $4010 ; disable DMC IRQ 50 | ; lda #$40 51 | ; sta $4017 ; disable APU IRQ 52 | cld ; disable decimal mode 53 | ldx #$FF 54 | txs ; initialize stack 55 | ; wait for first vblank 56 | bit $2002 57 | : 58 | bit $2002 59 | bpl :- 60 | ; clear all RAM to 0 61 | lda #0 62 | ldx #0 63 | : 64 | sta $0000, X 65 | sta $0100, X 66 | sta $0200, X 67 | sta $0300, X 68 | sta $0400, X 69 | sta $0500, X 70 | sta $0600, X 71 | sta $0700, X 72 | inx 73 | bne :- 74 | ; place all sprites offscreen at Y=255 75 | lda #255 76 | ldx #0 77 | : 78 | sta oam_1, X 79 | inx 80 | inx 81 | inx 82 | inx 83 | bne :- 84 | ; wait for second vblank 85 | : 86 | bit $2002 87 | bpl :- 88 | ; NES is initialized, ready to begin! 89 | ; enable the NMI for graphical updates, and jump to our main program 90 | lda #%10001000 91 | sta $2000 92 | jmp main 93 | 94 | ; Based on https://wiki.nesdev.org/w/index.php/APU_basics 95 | apu_init: ; Init $4000-4013 96 | ldy #$13 97 | @loop: lda @apu_startup_values, Y 98 | sta $4000, Y 99 | dey 100 | bpl @loop 101 | ; We have to skip over $4014 (OAMDMA) 102 | lda #$0f 103 | sta $4015 104 | lda #$40 105 | sta $4017 106 | rts 107 | @apu_startup_values: 108 | .byte $30,$08,$00,$00 109 | .byte $30,$08,$00,$00 110 | .byte $80,$00,$00,$00 111 | .byte $30,$00,$00,$00 112 | .byte $00,$00,$00,$00 113 | 114 | ; 115 | ; nmi routine 116 | ; 117 | 118 | .segment "ZEROPAGE" 119 | nmi_lock: .res 1 ; prevents NMI re-entry 120 | nmi_count: .res 1 ; is incremented every NMI 121 | nmi_ready: .res 1 ; set to 1 to push a PPU frame update, 2 to turn rendering off next NMI 122 | nmt_update_len: .res 1 ; number of bytes in nmt_update buffer 123 | scroll_x: .res 1 ; x scroll position 124 | scroll_y: .res 1 ; y scroll position 125 | scroll_nmt: .res 1 ; nametable select (0-3 = $2000,$2400,$2800,$2C00) 126 | temp: .res 1 ; temporary variable 127 | ball_x: .res 1 128 | ball_y: .res 1 129 | y_frame: .res 1 ; frame for Y-position 130 | anim_frame: .res 1 ; ball animation frame 131 | ball_dir: .res 1 132 | mute_count: .res 1 133 | 134 | .segment "BSS" 135 | nmt_update: .res 256 ; nametable update entry buffer for PPU update 136 | palette: .res 32 ; palette buffer for PPU update 137 | 138 | .segment "CODE" 139 | nmi: 140 | ; save registers 141 | pha 142 | txa 143 | pha 144 | tya 145 | pha 146 | ; prevent NMI re-entry 147 | lda nmi_lock 148 | beq :+ 149 | jmp @nmi_end 150 | : 151 | lda #1 152 | sta nmi_lock 153 | ; increment frame counter 154 | inc nmi_count 155 | ; 156 | lda nmi_ready 157 | bne :+ ; nmi_ready == 0 not ready to update PPU 158 | jmp @ppu_update_end 159 | : 160 | cmp #2 ; nmi_ready == 2 turns rendering off 161 | bne :+ 162 | lda #%00000000 163 | sta $2001 164 | ldx #0 165 | stx nmi_ready 166 | jmp @ppu_update_end 167 | : 168 | ; sprite OAM DMA 169 | ldx #0 170 | stx $2003 171 | lda active_oam 172 | sta $4014 173 | ; palettes 174 | lda #%10001000 175 | sta $2000 ; set horizontal nametable increment 176 | lda $2002 177 | lda #$3F 178 | sta PPUADDR 179 | stx PPUADDR ; set PPU address to $3F00 180 | ldx #0 181 | : 182 | lda palette, X 183 | sta PPUDATA 184 | inx 185 | cpx #32 186 | bcc :- 187 | ; nametable update 188 | ldx #0 189 | cpx nmt_update_len 190 | bcs @scroll 191 | @nmt_update_loop: 192 | lda nmt_update, X 193 | sta PPUADDR 194 | inx 195 | lda nmt_update, X 196 | sta PPUADDR 197 | inx 198 | lda nmt_update, X 199 | sta PPUDATA 200 | inx 201 | cpx nmt_update_len 202 | bcc @nmt_update_loop 203 | lda #0 204 | sta nmt_update_len 205 | @scroll: 206 | lda scroll_nmt 207 | and #%00000011 ; keep only lowest 2 bits to prevent error 208 | ora #%10001000 209 | sta $2000 210 | lda scroll_x 211 | sta $2005 212 | lda scroll_y 213 | sta $2005 214 | ; enable rendering 215 | lda #%00011110 216 | sta $2001 217 | ; flag PPU update complete 218 | ldx #0 219 | stx nmi_ready 220 | @ppu_update_end: 221 | ; if this engine had music/sound, this would be a good place to play it 222 | ; unlock re-entry flag 223 | lda #0 224 | sta nmi_lock 225 | @nmi_end: 226 | ; restore registers and return 227 | pla 228 | tay 229 | pla 230 | tax 231 | pla 232 | rti 233 | 234 | ; 235 | ; irq 236 | ; 237 | 238 | .segment "CODE" 239 | irq: 240 | rti 241 | 242 | ; ppu_update: waits until next NMI, turns rendering on (if not already), uploads OAM, palette, and nametable update to PPU 243 | ppu_update: 244 | lda #1 245 | sta nmi_ready 246 | : 247 | lda nmi_ready 248 | bne :- 249 | rts 250 | 251 | .segment "RODATA" 252 | start_palette: ; background last, foreground second 253 | .byte $0F,$04,$26,$10 ; bg0 purple/grey 254 | .byte $0F,$09,$19,$29 ; bg1 not used 255 | .byte $0F,$01,$11,$21 ; bg2 not used 256 | .byte $0F,$00,$10,$30 ; bg3 not used 257 | .byte $0F,$20,$28,$16 ; sp0 updated dynamically 258 | .byte $0F,$14,$24,$34 ; sp1 not used 259 | .byte $0F,$1B,$2B,$3B ; sp2 not used 260 | .byte $0F,$12,$22,$32 ; sp3 not used 261 | 262 | 263 | .segment "CODE" 264 | main: 265 | ; setup 266 | ldx #0 267 | : 268 | lda start_palette, X 269 | sta palette, X 270 | inx 271 | cpx #32 272 | bcc :- 273 | jsr setup_background 274 | ; center the ball 275 | lda #150 276 | sta ball_x 277 | lda #80 278 | sta ball_y 279 | lda #0 280 | sta anim_frame 281 | sta ball_dir 282 | sta mute_count 283 | jsr setup_oam 284 | jsr update_ball 285 | jsr ppu_update 286 | jsr apu_init 287 | ; main loop 288 | @draw: 289 | ; draw everything and finish the frame 290 | jsr ball_physics 291 | jsr update_ball 292 | jsr ppu_update 293 | ; keep doing this forever! 294 | jmp @draw 295 | 296 | PPUADDR = $2006 297 | PPUDATA = $2007 298 | 299 | setup_background: 300 | ; clear first nametable 301 | lda $2002 ; reset latch 302 | lda #$20 303 | sta PPUADDR 304 | lda #$00 305 | sta PPUADDR 306 | ; empty nametable 307 | lda #$34 308 | ldy #30 ; 30 rows 309 | : 310 | ldx #32 ; 32 columns 311 | : 312 | sta PPUDATA 313 | dex 314 | bne :- 315 | dey 316 | bne :-- 317 | ; set all attributes to 0 318 | lda #0 319 | ldx #64 ; 64 bytes 320 | : 321 | sta PPUDATA 322 | dex 323 | bne :- 324 | 325 | ; second nametable empty (not used) 326 | lda #$24 327 | sta PPUADDR 328 | lda #$00 329 | sta PPUADDR 330 | ; empty nametable 331 | lda #0 332 | ldy #30 ; 30 rows 333 | : 334 | ldx #32 ; 32 columns 335 | : 336 | sta PPUDATA 337 | dex 338 | bne :- 339 | dey 340 | bne :-- 341 | 342 | ; draw a grid to first nametable 343 | lda $2002 ; reset latch 344 | lda #$20 345 | sta PPUADDR 346 | lda #$60 347 | sta PPUADDR 348 | ldy #11 349 | @grid_row: ; each grid row is 2 roes of tiles 350 | lda #$34 351 | sta PPUDATA 352 | sta PPUDATA 353 | ldx #14 354 | : 355 | lda #$30 356 | sta PPUDATA 357 | lda #$31 358 | sta PPUDATA 359 | dex 360 | bne :- 361 | lda #$32 362 | sta PPUDATA 363 | lda #$34 364 | sta PPUDATA 365 | sta PPUDATA 366 | sta PPUDATA 367 | ldx #14 368 | : 369 | lda #$32 370 | sta PPUDATA 371 | lda #$34 372 | sta PPUDATA 373 | dex 374 | bne :- 375 | lda #$32 376 | sta PPUDATA 377 | lda #$34 378 | sta PPUDATA 379 | dey 380 | bne @grid_row 381 | ; Paste in some defined lines for the end of the screen 382 | ldx #16 383 | lda #$40 384 | clc 385 | : 386 | sta PPUDATA 387 | adc #1 388 | dex 389 | bne :- 390 | ldx #16 391 | lda #$60 392 | clc 393 | : 394 | sta PPUDATA 395 | adc #1 396 | dex 397 | bne :- 398 | ldx #16 399 | lda #$50 400 | clc 401 | : 402 | sta PPUDATA 403 | adc #1 404 | dex 405 | bne :- 406 | ldx #16 407 | lda #$70 408 | clc 409 | : 410 | sta PPUDATA 411 | adc #1 412 | dex 413 | bne :- 414 | 415 | ldx #32 416 | : 417 | lda #$33 418 | sta PPUDATA 419 | dex 420 | bne :- 421 | rts 422 | 423 | ball_physics: 424 | jsr ball_physics_x 425 | jsr ball_physics_y 426 | jsr ball_physics_spin 427 | jsr ball_bounce_mute 428 | rts 429 | 430 | ball_bounce_mute: ; mute sound eventually 431 | dec mute_count 432 | bne @end 433 | jsr apu_init 434 | @end: 435 | rts 436 | 437 | ; lookup table for y-coordinates over two bounces 438 | ball_loc_y: .byte $96, $93, $90, $8d, $8a, $87, $84, $81, $7e, $7b, $78, $75, $72, $6f, $6c, $69, $66, $63, $60, $5e, $5b, $58, $55, $53, $50, $4e, $4b, $49, $46, $44, $42, $3f, $3d, $3b, $39, $37, $35, $33, $31, $2f, $2d, $2c, $2a, $29, $27, $26, $24, $23, $22, $21, $20, $1f, $1e, $1d, $1c, $1b, $1b, $1a, $1a, $19, $19, $19, $19, $19, $19, $19, $19, $19, $19, $1a, $1a, $1b, $1b, $1c, $1c, $1d, $1e, $1f, $20, $21, $22, $24, $25, $26, $28, $29, $2b, $2d, $2e, $30, $32, $34, $36, $38, $3a, $3c, $3e, $40, $43, $45, $48, $4a, $4c, $4f, $52, $54, $57, $5a, $5c, $5f, $62, $65, $68, $6a, $6d, $70, $73, $76, $79, $7c, $7f, $82, $85, $89, $8c, $8f, $92, $95, $95, $92, $8f, $8c, $89, $85, $82, $7f, $7c, $79, $76, $73, $70, $6d, $6a, $68, $65, $62, $5f, $5c, $5a, $57, $54, $52, $4f, $4c, $4a, $48, $45, $43, $40, $3e, $3c, $3a, $38, $36, $34, $32, $30, $2e, $2d, $2b, $29, $28, $26, $25, $24, $22, $21, $20, $1f, $1e, $1d, $1c, $1c, $1b, $1b, $1a, $1a, $19, $19, $19, $19, $19, $19, $19, $19, $19, $19, $1a, $1a, $1b, $1b, $1c, $1d, $1e, $1f, $20, $21, $22, $23, $24, $26, $27, $29, $2a, $2c, $2d, $2f, $31, $33, $35, $37, $39, $3b, $3d, $3f, $42, $44, $46, $49, $4b, $4e, $50, $53, $55, $58, $5b, $5e, $60, $63, $66, $69, $6c, $6f, $72, $75, $78, $7b, $7e, $81, $84, $87, $8a, $8d, $90, $93, $96 439 | 440 | X_MIN = 8 441 | X_MAX = 184 442 | BALL_RIGHT = 0 443 | BALL_LEFT = 1 444 | 445 | ball_physics_x: 446 | lda ball_dir 447 | cmp #BALL_RIGHT ; branch on ball direction 448 | bne @ball_move_left 449 | inc ball_x ; move right 450 | lda ball_x 451 | cmp #X_MAX ; check against right wall 452 | bne @ball_move_end 453 | lda #BALL_LEFT ; change direction 454 | sta ball_dir 455 | jsr bounce_noise 456 | jmp @ball_move_end 457 | @ball_move_left: 458 | dec ball_x ; move left 459 | lda ball_x 460 | cmp #X_MIN ; check against left wall 461 | bne @ball_move_end 462 | lda #BALL_RIGHT ; change direction 463 | sta ball_dir 464 | jsr bounce_noise 465 | @ball_move_end: 466 | rts 467 | 468 | ball_physics_y: ; set Y from lookup table 469 | inc y_frame 470 | ldy y_frame 471 | lda ball_loc_y, Y 472 | sta ball_y 473 | cmp #$95 474 | bcc @end 475 | jsr bounce_noise 476 | @end: 477 | rts 478 | 479 | ball_physics_spin: ; set animation frame 480 | lda ball_x 481 | ; slow mode 482 | ror 483 | and #%01111111 484 | sta anim_frame 485 | rts 486 | 487 | bounce_noise: 488 | jsr apu_init 489 | lda #%00000101 ; some raw noise 490 | sta $400E 491 | lda #%00110001 492 | sta $400C 493 | lda #5 494 | sta mute_count 495 | rts 496 | --------------------------------------------------------------------------------