├── docs ├── logo.png ├── vol4labels │ ├── a53v4_FC_Labels46.png │ └── a53v4_NES_Labels46.png ├── pb53 compression.txt └── file_system.md ├── obj └── nes │ └── index.txt ├── audio ├── selnow.wav └── selnow-original.wav ├── tilesets ├── vwf7.png ├── select_tiles.png ├── title_screen.png └── screenshots │ └── default.png ├── collections ├── demo │ ├── tab.png │ ├── roms │ │ ├── cnrom.nes │ │ ├── pretendo.nes │ │ ├── scaling.nes │ │ ├── jmp-to-coredump.nes │ │ └── jmp-to-coredump.asm │ ├── screenshots │ │ ├── cnrom.png │ │ ├── pretendo.png │ │ └── scaling.png │ └── a53.cfg └── dtetest │ └── dtetest.png ├── src ├── bnrom.s ├── mouse.s ├── wait_loops.s ├── nes.inc ├── bcd.s ├── pentlyconfig.inc ├── pently.h ├── interbank_fetch.s ├── paldetect.s ├── undte.s ├── zapkernels.s ├── ppuclear.s ├── quadpcm.s ├── unpb53.s ├── global.inc ├── musicseq.pently ├── a53mapper.s ├── pads.s ├── identify.s ├── checksums.s ├── pently.inc ├── title.s ├── vwf_draw.s ├── pentlyseq.inc ├── pentlysound.s └── donut.s ├── tools ├── zip12to0.sh ├── dtefe.py ├── vwfbuild.py ├── mktables.py ├── chnutils.py ├── a53charset.py ├── firstfit.py ├── nesasm2ca65.py ├── autosubmulti.py ├── crc16xmodem.py ├── soxwave.py ├── ines.py ├── pentlybss.py ├── prgunused.py ├── quadanalyze.py ├── innie.py ├── pilbmp2nes.py └── ineschr.py ├── .gitignore ├── lastbank.x ├── makefile └── CHANGES.txt /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/action53/HEAD/docs/logo.png -------------------------------------------------------------------------------- /obj/nes/index.txt: -------------------------------------------------------------------------------- 1 | Files produced by build tools go here, but caulk goes where? 2 | -------------------------------------------------------------------------------- /audio/selnow.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/action53/HEAD/audio/selnow.wav -------------------------------------------------------------------------------- /tilesets/vwf7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/action53/HEAD/tilesets/vwf7.png -------------------------------------------------------------------------------- /collections/demo/tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/action53/HEAD/collections/demo/tab.png -------------------------------------------------------------------------------- /audio/selnow-original.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/action53/HEAD/audio/selnow-original.wav -------------------------------------------------------------------------------- /tilesets/select_tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/action53/HEAD/tilesets/select_tiles.png -------------------------------------------------------------------------------- /tilesets/title_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/action53/HEAD/tilesets/title_screen.png -------------------------------------------------------------------------------- /collections/demo/roms/cnrom.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/action53/HEAD/collections/demo/roms/cnrom.nes -------------------------------------------------------------------------------- /collections/dtetest/dtetest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/action53/HEAD/collections/dtetest/dtetest.png -------------------------------------------------------------------------------- /tilesets/screenshots/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/action53/HEAD/tilesets/screenshots/default.png -------------------------------------------------------------------------------- /collections/demo/roms/pretendo.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/action53/HEAD/collections/demo/roms/pretendo.nes -------------------------------------------------------------------------------- /collections/demo/roms/scaling.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/action53/HEAD/collections/demo/roms/scaling.nes -------------------------------------------------------------------------------- /docs/vol4labels/a53v4_FC_Labels46.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/action53/HEAD/docs/vol4labels/a53v4_FC_Labels46.png -------------------------------------------------------------------------------- /collections/demo/screenshots/cnrom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/action53/HEAD/collections/demo/screenshots/cnrom.png -------------------------------------------------------------------------------- /docs/vol4labels/a53v4_NES_Labels46.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/action53/HEAD/docs/vol4labels/a53v4_NES_Labels46.png -------------------------------------------------------------------------------- /collections/demo/roms/jmp-to-coredump.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/action53/HEAD/collections/demo/roms/jmp-to-coredump.nes -------------------------------------------------------------------------------- /collections/demo/screenshots/pretendo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/action53/HEAD/collections/demo/screenshots/pretendo.png -------------------------------------------------------------------------------- /collections/demo/screenshots/scaling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinobatch/action53/HEAD/collections/demo/screenshots/scaling.png -------------------------------------------------------------------------------- /src/bnrom.s: -------------------------------------------------------------------------------- 1 | .include "global.inc" 2 | .segment "LOWCODE" 3 | .proc start_game 4 | ldy #TITLE_PRG_BANK 5 | lda (start_bankptr),y 6 | sta (start_bankptr),y 7 | jmp (start_entrypoint) 8 | .endproc 9 | 10 | .segment "CODE" 11 | .proc init_mapper 12 | rts 13 | .endproc 14 | -------------------------------------------------------------------------------- /tools/zip12to0.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Convert Info-ZIP Zip's "nothing to do" status (12) to "success" (0) 3 | # Based on an answer by Alexander L. Belikoff 4 | # http://stackoverflow.com/a/19258421/2738262 5 | 6 | zip "$@" 7 | status=$? 8 | if [[ status -eq 12 ]]; then 9 | exit 0 10 | fi 11 | exit $status 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw[a-p] 2 | *.o 3 | *~ 4 | /*.nes 5 | *.prg 6 | *.chr 7 | *.qdp 8 | *.pb53 9 | *.donut 10 | /obj/nes/*.s 11 | /obj/nes/*.inc 12 | /zip.in 13 | *.nes.deb 14 | /map.txt 15 | /nsfmap.txt 16 | __pycache__/ 17 | *.pyc 18 | /tools/donut 19 | /tools/donut.exe 20 | /tools/dte 21 | /tools/dte.exe 22 | 23 | # this is NES homebrew, not DSiWare 24 | *.DS_Store 25 | -------------------------------------------------------------------------------- /src/mouse.s: -------------------------------------------------------------------------------- 1 | .export read_mouse, mouse_change_sensitivity 2 | .exportzp cur_mbuttons, new_mbuttons 3 | .segment "ZEROPAGE" 4 | cur_mbuttons: .res 2 5 | new_mbuttons: .res 2 6 | 7 | .segment "CODE" 8 | ;; 9 | ; @param X player number 10 | ; @return 1: mouse signature, buttons, and speed; 2: Y move; 3: X move 11 | .proc read_mouse 12 | lda #1 13 | sta 1 14 | sta 2 15 | sta 3 16 | : 17 | lda $4016,x 18 | lsr a 19 | rol 1 20 | bcc :- 21 | lda cur_mbuttons,x 22 | eor #$FF 23 | and 1 24 | sta new_mbuttons,x 25 | lda 1 26 | sta cur_mbuttons,x 27 | : 28 | lda $4016,x 29 | lsr a 30 | rol 2 31 | bcc :- 32 | : 33 | lda $4016,x 34 | lsr a 35 | rol 3 36 | bcc :- 37 | rts 38 | .endproc 39 | 40 | .proc mouse_change_sensitivity 41 | lda #1 42 | sta $4016 43 | lda $4016,x 44 | lda #0 45 | sta $4016 46 | rts 47 | .endproc 48 | -------------------------------------------------------------------------------- /src/wait_loops.s: -------------------------------------------------------------------------------- 1 | 2 | .export wait36k, wait1284y 3 | 4 | .segment "CODE" 5 | 6 | ; Based on allpads-nes/src/openbus.s and allpads-nes/src/identify.s, 7 | ; this detects a a Super NES Mouse in port 1 or a Zapper in port 2. 8 | ; It doesn't attempt to detect an Arkanoid controller or Power Pad 9 | ; because the menu does not support them. 10 | .proc wait36k 11 | ldx #28 12 | ldy #0 13 | waitloop: 14 | dey 15 | bne waitloop 16 | dex 17 | bne waitloop 18 | rts 19 | .endproc 20 | .assert >wait36k = >*, error, "wait36k crosses page boundary" 21 | 22 | ;; 23 | ; Waits for 1284*y + 5*x cycles + 5 cycles, minus 1284 if x is 24 | ; nonzero, and then reads bit 7 and 6 of the PPU status port. 25 | ; @param X fine period adjustment 26 | ; @param Y coarse period adjustment 27 | ; @return N=NMI status; V=sprite 0 status; X=Y=0; A unchanged 28 | .proc wait1284y 29 | dex 30 | bne wait1284y 31 | dey 32 | bne wait1284y 33 | bit $2002 34 | rts 35 | .endproc 36 | .assert >wait1284y = >*, error, "wait1284y crosses page boundary" 37 | -------------------------------------------------------------------------------- /src/nes.inc: -------------------------------------------------------------------------------- 1 | ; 2 | ; NES I/O definitions 3 | ; Copyright 2010 Damian Yerrick 4 | ; 5 | ; Copying and distribution of this file, with or without 6 | ; modification, are permitted in any medium without royalty provided 7 | ; the copyright notice and this notice are preserved in all source 8 | ; code copies. This file is offered as-is, without any warranty. 9 | ; 10 | 11 | PPUCTRL = $2000 12 | NT_2000 = $00 13 | NT_2400 = $01 14 | NT_2800 = $02 15 | NT_2C00 = $03 16 | VRAM_DOWN = $04 17 | OBJ_0000 = $00 18 | OBJ_1000 = $08 19 | OBJ_8X16 = $20 20 | BG_0000 = $00 21 | BG_1000 = $10 22 | VBLANK_NMI = $80 23 | 24 | PPUMASK = $2001 25 | LIGHTGRAY = $01 26 | BG_OFF = $00 27 | BG_CLIP = $08 28 | BG_ON = $0A 29 | OBJ_OFF = $00 30 | OBJ_CLIP = $10 31 | OBJ_ON = $14 32 | TINT_R = $20 33 | TINT_G = $40 34 | TINT_B = $80 35 | 36 | PPUSTATUS = $2002 37 | OAMADDR = $2003 38 | ; Don't worry about $2004; let OAM_DMA do the work for you. 39 | PPUSCROLL = $2005 40 | PPUADDR = $2006 41 | PPUDATA = $2007 42 | 43 | OAM_DMA = $4014 44 | SNDCHN = $4015 45 | P1 = $4016 46 | P2 = $4017 47 | 48 | KEY_A = %10000000 49 | KEY_B = %01000000 50 | KEY_SELECT = %00100000 51 | KEY_START = %00010000 52 | KEY_UP = %00001000 53 | KEY_DOWN = %00000100 54 | KEY_LEFT = %00000010 55 | KEY_RIGHT = %00000001 56 | -------------------------------------------------------------------------------- /collections/demo/roms/jmp-to-coredump.asm: -------------------------------------------------------------------------------- 1 | ; written to compile with asm6 2 | 3 | ;---------------------------------------------------------------- 4 | ; NES 2.0 header 5 | ;---------------------------------------------------------------- 6 | INES_MAPPER = 34 ; 0 = NROM, 218 = Single Chip 7 | INES_PRG_COUNT = 1 ; number of 16KB PRG-ROM pages 8 | INES_CHR_COUNT = 0 ; number of 8KB CHR-ROM pages 9 | INES_MIRRORING = %0001 ; %0000 = horizontal, %0001 = vertical, 10 | ; %1000 = four-screen 11 | 12 | .db "NES", $1a ; magic signature 13 | .db INES_PRG_COUNT 14 | .db INES_CHR_COUNT 15 | .db ((INES_MAPPER << 4) & $f0) | INES_MIRRORING 16 | .db (INES_MAPPER & $f0) 17 | .dsb 8, $00 ; NES 1.0, no other features 18 | 19 | .fillvalue $ff 20 | .org $c000 21 | 22 | .org $ffd6 23 | 24 | RST: 25 | ldx #$100-(-ramcode_begin+ramcode_end) 26 | copy_loop: 27 | lda ramcode_end-$100,x 28 | sta 0, x 29 | inx 30 | bne copy_loop 31 | ldy #$80 32 | sty $5000 33 | lda #$02 34 | ldy #$81 35 | ldx #$ff 36 | jmp $100-(-ramcode_begin+ramcode_end) 37 | ramcode_begin: 38 | sta $8000 39 | sty $5000 40 | stx $8000 41 | brk 42 | NMI: 43 | IRQ: 44 | rti 45 | .db $02 ; halt CPU hard in case we get this far. 46 | ramcode_end: 47 | 48 | .org $fffa 49 | 50 | vectors: 51 | .dw NMI 52 | .dw RST 53 | .dw IRQ 54 | -------------------------------------------------------------------------------- /lastbank.x: -------------------------------------------------------------------------------- 1 | # 2 | # Linker script for Action 53 3 | # Copyright 2010-2012 Damian Yerrick 4 | # 5 | # Copying and distribution of this file, with or without 6 | # modification, are permitted in any medium without royalty 7 | # provided the copyright notice and this notice are preserved. 8 | # This file is offered as-is, without any warranty. 9 | # 10 | MEMORY { 11 | # Does not add an iNES header. That will be added by the menu builder 12 | # (tools/a53build.py). 13 | ZP: start = $10, size = $f0, type = rw; 14 | # use first $10 zeropage locations as locals 15 | RAM: start = $0200, size = $0600, type = rw; 16 | ROM63: start = $8000, size = $8000, type = ro, file = %O, fill=yes, fillval=$FF; 17 | } 18 | 19 | SEGMENTS { 20 | ZEROPAGE: load = ZP, type = zp; 21 | KEYBLOCK: load = ROM63, type = ro, start = $8000, optional=yes; 22 | VOICEDATA: load = ROM63, type = ro, start = $a000; 23 | PAGERODATA: load = ROM63, type = ro, start = $c000; 24 | CODE: load = ROM63, type = ro; 25 | RODATA: load = ROM63, type = ro; 26 | OAM: load = RAM, type = bss, define = yes, align = $100; 27 | LOWCODE: load = ROM63, run = RAM, type = rw, define = yes, align = $100; 28 | BSS: load = RAM, type = bss, define = yes, align = $100; 29 | FFF0: load = ROM63, type = ro, start = $FFF0; 30 | } 31 | 32 | FILES { 33 | %O: format = bin; 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/bcd.s: -------------------------------------------------------------------------------- 1 | ; 2 | ; Binary to decimal conversion for 8-bit numbers 3 | ; Copyright 2010 Damian Yerrick 4 | ; 5 | ; Copying and distribution of this file, with or without 6 | ; modification, are permitted in any medium without royalty provided 7 | ; the copyright notice and this notice are preserved in all source 8 | ; code copies. This file is offered as-is, without any warranty. 9 | ; 10 | .export bcd8bit 11 | 12 | .macro bcd8bit_iter value 13 | .local skip 14 | cmp value 15 | bcc skip 16 | sbc value 17 | skip: 18 | rol highDigits 19 | .endmacro 20 | 21 | ;; 22 | ; Converts a decimal number to two or three BCD digits 23 | ; in no more than 84 cycles. 24 | ; @param a the number to change 25 | ; @return a: low digit; 0: upper digits as nibbles 26 | ; No other memory or register is touched. 27 | .proc bcd8bit 28 | highDigits = $00 29 | ; First clear out two bits of highDigits. (The conversion will 30 | ; fill in the other six.) 31 | asl highDigits 32 | asl highDigits 33 | 34 | ; Each iteration takes 11 if subtraction occurs or 10 if not. 35 | ; But if 80 is subtracted, 40 and 20 aren't, and if 200 is 36 | ; subtracted, 80 is not, and at least one of 40 and 20 is not. 37 | ; So this part takes up to 6*11-2 cycles. 38 | bcd8bit_iter #200 39 | bcd8bit_iter #100 40 | bcd8bit_iter #80 41 | bcd8bit_iter #40 42 | bcd8bit_iter #20 43 | bcd8bit_iter #10 44 | rts 45 | .endproc 46 | -------------------------------------------------------------------------------- /src/pentlyconfig.inc: -------------------------------------------------------------------------------- 1 | ; Configuration settings for Pently 2 | 3 | ; Master switch for build supporting only sound effects 4 | PENTLY_USE_MUSIC = 1 5 | 6 | ; Music engine features that not all projects will need 7 | ; Disable to save ROM and RAM space 8 | PENTLY_USE_VIBRATO = 0 9 | PENTLY_USE_PORTAMENTO = 1 10 | PENTLY_USE_303_PORTAMENTO = 0 11 | 12 | PENTLY_USE_ARPEGGIO = 0 13 | PENTLY_USE_ATTACK_PHASE = 1 14 | PENTLY_USE_ATTACK_TRACK = 1 15 | 16 | PENTLY_USE_CHANNEL_VOLUME = 0 17 | PENTLY_USE_VARMIX = 0 18 | 19 | ; Features that affect policy more than ROM space, such as 20 | ; sound effect interruption 21 | PENTLY_USE_SQUARE_POOLING = 1 22 | PENTLY_USE_MUSIC_IF_LOUDER = 1 23 | PENTLY_USE_PAL_ADJUST = 1 24 | PENTLY_USE_TRIANGLE_DUTY_FIX = 1 25 | 26 | ; Utilities used when syncing animation to the audio. 27 | ; Disable to save ROM and RAM space 28 | PENTLY_USE_BPMMATH = 0 29 | PENTLY_USE_ROW_CALLBACK = 0 30 | PENTLY_USE_VIS = 0 31 | PENTLY_USE_REHEARSAL = 0 32 | 33 | ; Should sound effects be selectable in the NSF? 34 | PENTLY_USE_NSF_SOUND_FX = 0 35 | 36 | ; 0-127; higher means quieter tri/noise 37 | PENTLY_INITIAL_4011 = 64 38 | 39 | ; Five bytes of scratch space on zero page that need not be preserved 40 | ; across calls. This needs to be either an = or an .importzp 41 | 42 | ;pently_zptemp = $0000 43 | .importzp pently_zptemp 44 | 45 | .define PENTLY_CODE "CODE" 46 | .define PENTLY_RODATA "RODATA" 47 | -------------------------------------------------------------------------------- /src/pently.h: -------------------------------------------------------------------------------- 1 | /* 2 | C bindings for Pently 3 | 4 | Copyright 2018 Damian Yerrick 5 | 6 | This software is provided 'as-is', without any express or implied 7 | warranty. In no event will the authors be held liable for any damages 8 | arising from the use of this software. 9 | 10 | Permission is granted to anyone to use this software for any purpose, 11 | including commercial applications, and to alter it and redistribute it 12 | freely, subject to the following restrictions: 13 | 14 | 1. The origin of this software must not be misrepresented; you must not 15 | claim that you wrote the original software. If you use this software 16 | in a product, an acknowledgment in the product documentation would be 17 | appreciated but is not required. 18 | 2. Altered source versions must be plainly marked as such, and must not be 19 | misrepresented as being the original software. 20 | 3. This notice may not be removed or altered from any source distribution. 21 | 22 | */ 23 | #ifndef PENTLY_H 24 | #define PENTLY_H 25 | 26 | void __fastcall__ pently_init(void); 27 | void __fastcall__ pently_start_sound(unsigned char effect); 28 | void __fastcall__ pently_start_music(unsigned char song); 29 | void __fastcall__ pently_update(void); 30 | void __fastcall__ pently_stop_music(void); 31 | void __fastcall__ pently_resume_music(void); 32 | void __fastcall__ pently_skip_to_row(unsigned short row); 33 | 34 | /* pently_play_note() not yet bound because multiple arguments */ 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /tools/dtefe.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Python frontend for JRoatch's C language DTE compressor 4 | license: zlib 5 | """ 6 | import sys, os, subprocess 7 | 8 | def dte_compress(lines, compctrl=False, mincodeunit=128): 9 | dte_path = os.path.join(os.path.dirname(__file__), "dte") 10 | delimiter = b'\0' 11 | if len(lines) > 1: 12 | unusedvalues = set(range(1 if compctrl else 32)) 13 | for line in lines: 14 | unusedvalues.difference_update(line) 15 | delimiter = min(unusedvalues) 16 | delimiter = bytes([delimiter]) 17 | excluderange = "0x00-0x00" if compctrl else "0x00-0x1F" 18 | digramrange = "0x%02x-0xFF" % mincodeunit 19 | compress_cmd_line = [ 20 | dte_path, "-c", "-e", excluderange, "-r", digramrange 21 | ] 22 | inputdata = delimiter.join(lines) 23 | spresult = subprocess.run( 24 | compress_cmd_line, check=True, 25 | input=inputdata, stdout=subprocess.PIPE 26 | ) 27 | table_len = (256 - mincodeunit) * 2 28 | repls = [spresult.stdout[i:i + 2] for i in range(0, table_len, 2)] 29 | clines = spresult.stdout[table_len:].split(delimiter) 30 | return clines, repls, None 31 | 32 | def main(argv=None): 33 | argv = argv or sys.argv 34 | with open(argv[1], "rb") as infp: 35 | lines = [x.rstrip(b"\r\n") for x in infp] 36 | clines, repls = dte_compress(lines)[:2] 37 | print(clines) 38 | print(repls) 39 | 40 | if __name__=='__main__': 41 | if 'idlelib' in sys.modules: 42 | main(["dtefe.py", "../README.md"]) 43 | else: 44 | main() 45 | -------------------------------------------------------------------------------- /src/interbank_fetch.s: -------------------------------------------------------------------------------- 1 | ; 2 | ; Fetch bytes from another bank 3 | ; Copyright 2012 Damian Yerrick 4 | ; 5 | ; Copying and distribution of this file, with or without 6 | ; modification, are permitted in any medium without royalty provided 7 | ; the copyright notice and this notice are preserved in all source 8 | ; code copies. This file is offered as-is, without any warranty. 9 | ; 10 | ; Separated out from unpb53.s 11 | 12 | .export interbank_fetch, interbank_fetch_buf 13 | 14 | .segment "BSS" 15 | ; The largest thing we'll fetch out of another bank at once is 16 | ; the compressed data for 10 tiles, and that's no bigger than 180. 17 | interbank_fetch_buf: .res 192 18 | 19 | .segment "LOWCODE" 20 | ;; 21 | ; Copies data from another bank to the interbank fetch buffer 22 | ; and returns to bank $FF. 23 | ; 0-1: address of the data 24 | ; 2-3: address in ROM of the bank number (NOT the bank number itself; 25 | ; we need this address to avoid a data bus conflict) 26 | ; 4: number of bytes 27 | .proc interbank_fetch 28 | ldy #0 29 | lda (2),y ; must write the bank number to a location 30 | sta (2),y ; that already has the bank number 31 | copyloop: 32 | lda ($00),y 33 | sta interbank_fetch_buf,y 34 | iny 35 | cpy 4 36 | bne copyloop 37 | 38 | ; That was the easy part. The hard part is getting back home: 39 | ; we need to find $FF somewhere in the bank. Fortunately, we 40 | ; ordinarily see $FF very early in the reset patch. 41 | lda #$FF 42 | cmp $FFFD 43 | bne ff_not_in_reset 44 | sta $FFFD 45 | rts 46 | ff_not_in_reset: 47 | ldy $FFFC 48 | sty 2 49 | ldy $FFFD 50 | sty 3 51 | ldy #0 52 | lda #$FF 53 | homeloop: 54 | cmp (2),y 55 | beq found_ff 56 | iny 57 | bne homeloop 58 | found_ff: 59 | sta (2),y 60 | rts 61 | .endproc 62 | 63 | .if 0 64 | ; example of use: 65 | lda #$00 66 | sta 0 67 | lda #$80 68 | sta 1 69 | sta 4 70 | lda #<(addr_with_bank+1) 71 | sta 2 72 | lda #>(addr_with_bank+1) 73 | addr_with_bank: 74 | sta 3 75 | jsr interbank_fetch 76 | .endif 77 | -------------------------------------------------------------------------------- /src/paldetect.s: -------------------------------------------------------------------------------- 1 | ; 2 | ; TV system detection code, mark 2 3 | ; Copyright 2017 Damian Yerrick 4 | ; (Insert zlib license here) 5 | ; 6 | 7 | ; 8 | ; Uses the length from one frame to the next to detect NTSC, PAL 9 | ; NES, or Dendy in 41 bytes. This is slightly bigger than the old 10 | ; NMI-dependent detection used since Pently 3 (2011, Thwaite), 11 | ; but it also does all the vblank waiting needed for PPU warm-up, 12 | ; saving an estimated 13 init code bytes. 13 | ; 14 | .p02 15 | .export getTVSystem 16 | .import wait1284y 17 | 18 | .segment "CODE" 19 | 20 | ;; 21 | ; Waits for the PPU to stabilize and returns which TV system 22 | ; is in use. Assumes NMI is disabled. 23 | ; @return A is 0 for NTSC or RGB, 1 for PAL NES, or 2 for Dendy and 24 | ; similar PAL famiclones designed for compatibility with NTSC games 25 | .proc getTVSystem 26 | ; Pressing Reset during vertical blanking (vblank) on a toploader 27 | ; leaves NMI unacknowledged, causing vwait1 loop to be skipped. 28 | ; So acknowledge NMI. 29 | bit $2002 30 | 31 | ; Wait for the start of vblank at the bottom of a frame. 32 | ; This may occasionally miss a frame due to a race in the PPU; 33 | ; this is harmless. 34 | vwait1: 35 | bit $2002 36 | bpl vwait1 37 | 38 | ; The PPU is stable at the end of a vblank. Determining the TV 39 | ; system takes slightly longer than that: into the post-render 40 | ; or vblank below the second frame. 41 | ; 42 | ; NTSC: 29780 cycles, 23.19 loops. Will end in vblank 43 | ; PAL NES: 33247 cycles, 25.89 loops. Will end in vblank 44 | ; Dendy: 35464 cycles, 27.62 loops. Will end in post-render 45 | ldx #0 46 | ldy #24 47 | jsr wait1284y 48 | bpl not_ntsc 49 | ; Another vblank happened within 24 loops. 50 | ; It must be NTSC. 51 | tya 52 | rts 53 | not_ntsc: 54 | 55 | lda #1 56 | ldy #3 57 | jsr wait1284y 58 | 59 | ; If another vblank happened by 27 loops, we're on a PAL NES. 60 | ; Otherwise, we're on a Dendy (PAL famiclone). 61 | bmi not_dendy 62 | asl a 63 | not_dendy: 64 | rts 65 | .endproc 66 | -------------------------------------------------------------------------------- /src/undte.s: -------------------------------------------------------------------------------- 1 | .include "global.inc" 2 | 3 | ; BPE (Byte Pair Encoding) or DTE (Digram Tree Encoding) 4 | ; Code units less then DTE_MIN_CODEUNIT map to literal characters. 5 | ; Code units greater than or equal to DTE_MIN_CODEUNIT (which must 6 | ; be at least 128 and must match the value in a53build.py) map to 7 | ; pairs of code units. The second is added to a stack, and the 8 | ; first is interpreted as above. 9 | 10 | DTE_MIN_CODEUNIT = 128 11 | 12 | .segment "CODE" 13 | .proc undte_line 14 | srcaddr = $00 15 | sty srcaddr 16 | sta srcaddr+1 17 | .endproc 18 | .proc undte_line0 19 | srcaddr = $00 20 | ysave = $02 21 | repltable = $03 22 | 23 | ; Copy the compressed data to the END of dte_output_buf. 24 | ; First calculate compressed data length 25 | lda DTE_REPLACEMENTS + 0 26 | sta repltable + 0 27 | lda DTE_REPLACEMENTS + 1 28 | sta repltable + 1 29 | ldy #0 30 | strlenloop: 31 | lda (srcaddr),y 32 | iny 33 | cpy #DTE_OUTPUT_LEN 34 | bcs have_strlen 35 | cmp #FIRST_PRINTABLE_CU 36 | bcs strlenloop 37 | have_strlen: 38 | tya 39 | pha ; Save compressed byte count 40 | 41 | ; Now copy backward 42 | ldx #DTE_OUTPUT_LEN 43 | poolypoc: 44 | dey 45 | dex 46 | lda (srcaddr),y 47 | sta dte_output_buf,x 48 | cpy #0 49 | bne poolypoc 50 | 51 | ; at this point, Y = 0, pointing to the decompressed data, 52 | ; and X points to the remaining compressed data 53 | decomploop: 54 | lda dte_output_buf,x 55 | decomp_code: 56 | cmp #DTE_MIN_CODEUNIT 57 | bcs handle_bytepair 58 | sta dte_output_buf,y 59 | iny 60 | inx 61 | cpx #DTE_OUTPUT_LEN 62 | bcc decomploop 63 | 64 | ; A: compressed bytes read; Y: decompressed bytes written 65 | pla 66 | rts 67 | 68 | handle_bytepair: 69 | ; For a bytepair, stack the second byte on the compressed data 70 | ; and reprocess the first byte 71 | sty ysave 72 | ; sec ; always set by bcs 73 | rol a ; A = (bytecode - 128) * 2 + 1 74 | tay 75 | lda (repltable),y 76 | sta dte_output_buf,x 77 | dex 78 | dey ; Y = (bytecode - 128) * 2 79 | lda (repltable),y 80 | ldy ysave 81 | jmp decomp_code 82 | .endproc 83 | -------------------------------------------------------------------------------- /docs/pb53 compression.txt: -------------------------------------------------------------------------------- 1 | The 2011 continuation of the Who's Cuter project demonstrated a CHR codec called PB8, which operated on 8-byte packets (hence the name). Each packet started with an 8-bit flags byte telling whether each byte in the packet was new (0) or a repeat of the previous byte (1). However, if the first byte of a packet was a repeat, it would use $00 as the byte's value; this behavior is called "top zero". 2 | 3 | The PB8 decoder in Who's Cuter decompresses to RAM at roughly 30 cycles per output byte, which is much faster than the roughly 400 cycles per byte of 2BT (pixel RLE). A semi-unrolled copy to VRAM takes 9 cycles per byte. If we were to use PB8 and leave the screen off, we wouldn't even need a loading bar because it'd be 10 times faster than the loader in mgcmenu, or less than 15 frames. 4 | 5 | But for the sake of space efficiency, I've discovered various aspects of the CHR ROM of the games in Action 53 that could be used to save a bunch of space. 6 | 7 | LAN Master and Lawn Mower share a *lot* of tiles between $0000-$0FFF and $1000-$1FFF because they switch between the two to animate tiles in the game area. Slappin' Bitches just sets $1000-$1FFF equal to $0000-$0FFF. 8 | 9 | But PB8 doesn't support copying entire tiles from the first half of VRAM. Nor do I especially want to copy. So instead, I'll decode $0000-$0FFF and $1000-$1FFF in parallel. 10 | 11 | Control byte for plane 0 12 | $00-$7F: Each set bit repeats the byte above it 13 | $80: Equivalent to $7F $00 (tile uses colors 0 and 2) 14 | $81: Equivalent to $7F $FF (tile uses colors 1 and 3) 15 | $82: Copy tile from 16 bytes back 16 | $83: Copy tile from 4096 bytes back 17 | $84: Equivalent to $7F $00 $7F $00 (entire tile color 0) 18 | $85: Equivalent to $7F $FF $7F $00 (entire tile color 1) 19 | $86: Equivalent to $7F $00 $7F $FF (entire tile color 2) 20 | $87: Equivalent to $7F $FF $7F $FF (entire tile color 3) 21 | 22 | Control byte for plane 1 23 | $00-$7F: Each set bit repeats the byte above it 24 | $80: Equivalent to $7F $00 (tile uses colors 0 and 1) 25 | $81: Equivalent to $7F $FF (tile uses colors 2 and 3) 26 | $82: Copy plane from 8 bytes back (tile uses colors 0 and 3) 27 | $83: XOR plane from 8 bytes back with $FF (tile uses colors 1 and 2) 28 | -------------------------------------------------------------------------------- /tools/vwfbuild.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import with_statement 3 | from PIL import Image 4 | import array 5 | 6 | def ca65_bytearray(s): 7 | s = [' .byte ' + ','.join("%3d" % ch for ch in s[i:i + 16]) 8 | for i in range(0, len(s), 16)] 9 | return '\n'.join(s) 10 | 11 | def vwfcvt(filename, tileHt=8): 12 | im = Image.open(filename) 13 | pixels = im.load() 14 | (w, h) = im.size 15 | (xparentColor, sepColor) = im.getextrema() 16 | widths = bytearray() 17 | tiledata = bytearray() 18 | for yt in range(0, h, tileHt): 19 | for xt in range(0, w, 8): 20 | # step 1: find the glyph width 21 | tilew = 8 22 | for x in range(8): 23 | if pixels[x + xt, yt] == sepColor: 24 | tilew = x 25 | break 26 | if tilew == 0: 27 | continue 28 | # step 2: encode the pixels 29 | widths.append(tilew) 30 | for y in range(tileHt): 31 | rowdata = 0 32 | for x in range(8): 33 | pxhere = pixels[x + xt, y + yt] 34 | pxhere = 0 if pxhere in (xparentColor, sepColor) else 1 35 | rowdata = (rowdata << 1) | pxhere 36 | tiledata.append(rowdata) 37 | return (widths, tiledata) 38 | 39 | def main(argv=None): 40 | import sys 41 | if argv is None: 42 | argv = sys.argv 43 | if len(argv) > 1 and argv[1] == '--help': 44 | print("usage: %s font.png font.s" % argv[0]) 45 | return 46 | if len(argv) != 3: 47 | print("wrong number of options; try %s --help" % argv[0], file=sys.stderr) 48 | sys.exit(1) 49 | 50 | (widths, tiledata) = vwfcvt(argv[1]) 51 | out = ["; Generated by vwfbuild", 52 | ".export vwfChrWidths, vwfChrData", 53 | '.segment "PAGERODATA"', 54 | '.align 256', 55 | 'vwfChrData:', 56 | ca65_bytearray(tiledata), 57 | "vwfChrWidths:", 58 | ca65_bytearray(widths), 59 | ''] 60 | with open(argv[2], 'w') as outfp: 61 | outfp.write('\n'.join(out)) 62 | 63 | if __name__ == '__main__': 64 | ## main(['vwfbuild', '../tilesets/vwf7.png', '../obj/vwf7.s']) 65 | main() 66 | -------------------------------------------------------------------------------- /collections/demo/a53.cfg: -------------------------------------------------------------------------------- 1 | [title] 2 | titlescreen=tab.png 3 | titlepalette=0f0010200f1616160f1616160f161616 4 | menuprg=../../a53menu.prg 5 | 6 | text=2018 Damian Yerrick and 7 | at=52,200 8 | color=2,0 9 | 10 | text=Contributors 11 | at=160,200 12 | color=2,0 13 | 14 | text=Your gift message goes here. 15 | at=64,184 16 | color=2,0 17 | 18 | [games] 19 | 20 | page=Tech demo 21 | 22 | title=Pretendo 23 | author=Damian Yerrick 24 | year=2013 25 | rom=roms/pretendo.nes 26 | screenshot=screenshots/pretendo.png 27 | prgunused0=C470-FFF9 28 | description: 29 | Displays a random logo in 30 | a parody of the Game Boy 31 | boot animation. 32 | 33 | From forums.nesdev.com/ 34 | viewtopic.php?t=9796 35 | . 36 | 37 | title=Sprite scaling demo 38 | author=Damian Yerrick 39 | year=2011 40 | players=1-2 41 | rom=roms/scaling.nes 42 | screenshot=screenshots/scaling.png 43 | 44 | prgunused0=FFF0-FFF9 45 | description: 46 | Scales sprite cels to 47 | CHR RAM in real time. 48 | 49 | ←→ Move 50 | ↑↓ Change size 51 | 52 | From forums.nesdev.com/ 53 | viewtopic.php?t=12055 54 | . 55 | 56 | title=CNROM demo 57 | author=Damian Yerrick 58 | year=2018 59 | players=1 60 | rom=roms/cnrom.nes 61 | screenshot=screenshots/cnrom.png 62 | 63 | prgunused0=FFF0-FFF9 64 | description: 65 | This just jumps to the 66 | reset vector. Use it 67 | to gauge decompression 68 | time for CNROM games. 69 | . 70 | 71 | title=Description Text demo 72 | author: 73 | Multiple lines of 74 | text in the author 75 | . 76 | year=2018 77 | rom=roms/jmp-to-coredump.nes 78 | screenshot=../../tilesets/screenshots/default.png 79 | prgunused=C000-FFD5 80 | description: 81 | ASCII range: 82 | !"#$%&'()*+,-./ 83 | 0123456789:;<=>? 84 | @ABCDEFGHIJKLMNO 85 | PQRSTUVWXYZ[\]^_ 86 | `abcdefghijklmno 87 | pqrstuvwxyz{|}~ 88 | 89 | Unicode range: 90 | ↑↓←→✜ⒷⒶ©Łżę🐦␣éñö 91 | 92 | '✜' is a control pad 93 | '␣' is a single pixel space 94 | '🐦' is a bird for @twitter 95 | . 96 | 97 | title=240p Test Suite 98 | author=Damian Yerrick 99 | year=2020 100 | rom=../../../240p-test-mini/nes/240pee.nes 101 | exitmethod=none 102 | screenshot=../../tilesets/screenshots/default.png 103 | description: 104 | Port of Artemio Urbina's 105 | 240p Test Suite to NES, 106 | including MDFourier 107 | . 108 | -------------------------------------------------------------------------------- /tools/mktables.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Lookup table generator for Concentration Room 4 | # Copyright 2010 Damian Yerrick 5 | # 6 | # Copying and distribution of this file, with or without 7 | # modification, are permitted in any medium without royalty 8 | # provided the copyright notice and this notice are preserved. 9 | # This file is offered as-is, without any warranty. 10 | # 11 | import sys 12 | 13 | ntscOctaveBase = 39375000.0/(22 * 16 * 55) 14 | palOctaveBase = 266017125.0/(10 * 16 * 16 * 55) 15 | maxNote = 80 16 | 17 | def makePeriodTable(filename, pal=False): 18 | semitone = 2.0**(1./12) 19 | octaveBase = palOctaveBase if pal else ntscOctaveBase 20 | relFreqs = [(1 << (i // 12)) * semitone**(i % 12) 21 | for i in range(maxNote)] 22 | periods = [int(round(octaveBase / freq)) - 1 for freq in relFreqs] 23 | systemName = "PAL" if pal else "NTSC" 24 | with open(filename, 'wt') as outfp: 25 | outfp.write("""; %s period table generated by mktables.py 26 | .export periodTableLo, periodTableHi 27 | .segment "RODATA" 28 | periodTableLo:\n""" 29 | % systemName) 30 | for i in range(0, maxNote, 12): 31 | outfp.write(' .byt ' 32 | + ','.join('$%02x' % (i % 256) 33 | for i in periods[i:i + 12]) 34 | + '\n') 35 | outfp.write('periodTableHi:\n') 36 | for i in range(0, maxNote, 12): 37 | outfp.write(' .byt ' 38 | + ','.join('$%02x' % (i >> 8) 39 | for i in periods[i:i + 12]) 40 | + '\n') 41 | 42 | def makePALPeriodTable(filename): 43 | return makePeriodTable(filename, pal=True) 44 | 45 | tableNames = { 46 | 'period': makePeriodTable, 47 | 'palperiod': makePALPeriodTable 48 | } 49 | 50 | def main(argv): 51 | if len(argv) >= 2 and argv[1] in ('/?', '-?', '-h', '--help'): 52 | print("usage: %s TABLENAME FILENAME" % argv[0]) 53 | print("known tables:", ' '.join(sorted(tableNames))) 54 | elif len(argv) < 3: 55 | print("mktables: too few arguments; try %s --help" % argv[0]) 56 | elif argv[1] in tableNames: 57 | tableNames[argv[1]](argv[2]) 58 | else: 59 | print("mktables: no such table %s; try %s --help" % (argv[1], argv[0])) 60 | 61 | if __name__=='__main__': 62 | main(sys.argv) 63 | 64 | -------------------------------------------------------------------------------- /src/zapkernels.s: -------------------------------------------------------------------------------- 1 | ; 2 | ; Zapper reading kernels (NTSC) 3 | ; Copyright 2011 Damian Yerrick 4 | ; 5 | ; Copying and distribution of this file, with or without 6 | ; modification, are permitted in any medium without royalty provided 7 | ; the copyright notice and this notice are preserved in all source 8 | ; code copies. This file is offered as-is, without any warranty. 9 | ; 10 | ; 2012-02-04: Removed xyon and yon2p kernels, used by Zap Ruder but 11 | ; not by the a53 menu 12 | ; 13 | .include "nes.inc" 14 | 15 | ; $4017.D4: Trigger switch (1: pressed) 16 | ; $4017.D3: Light detector (0: bright) 17 | ; 18 | ; One kernel is included in this version: 19 | ; NTSC single player (X, Y) kernel 20 | 21 | .export zapkernel_yonoff_ntsc 22 | 23 | ;; 24 | ; @param Y number of scanlines to watch 25 | ; @return 0: number of lines off, 1: number of lines on 26 | .proc zapkernel_yonoff_ntsc 27 | off_lines = 0 28 | on_lines = 1 29 | subcycle = 2 30 | DEBUG_THIS = 0 31 | lda #0 32 | sta off_lines 33 | sta on_lines 34 | sta subcycle 35 | 36 | ; Wait for photosensor to turn ON 37 | lineloop_on: 38 | ; 8 39 | lda #$08 40 | and $4017 41 | beq hit_on 42 | 43 | ; 72 44 | jsr waste_12 45 | jsr waste_12 46 | jsr waste_12 47 | jsr waste_12 48 | jsr waste_12 49 | jsr waste_12 50 | 51 | ; 11 52 | lda off_lines 53 | and #LIGHTGRAY 54 | ora #BG_ON|OBJ_ON 55 | .if DEBUG_THIS 56 | sta PPUMASK 57 | .else 58 | bit $0100 59 | .endif 60 | 61 | ; 12.67 62 | clc 63 | lda subcycle 64 | adc #$AA 65 | sta subcycle 66 | bcs :+ 67 | : 68 | 69 | ; 10 70 | inc off_lines 71 | dey 72 | bne lineloop_on 73 | jmp bail 74 | 75 | ; Wait for photosensor to turn ON 76 | lineloop_off: 77 | ; 8 78 | lda #$08 79 | and $4017 80 | bne hit_off 81 | 82 | hit_on: 83 | ; 72 84 | jsr waste_12 85 | jsr waste_12 86 | jsr waste_12 87 | jsr waste_12 88 | jsr waste_12 89 | jsr waste_12 90 | 91 | ; 11 92 | lda off_lines 93 | and #LIGHTGRAY 94 | ora #BG_ON|OBJ_ON 95 | .if DEBUG_THIS 96 | sta PPUMASK 97 | .else 98 | bit $0100 99 | .endif 100 | 101 | ; 12.67 102 | clc 103 | lda subcycle 104 | adc #$AA 105 | sta subcycle 106 | bcs :+ 107 | : 108 | 109 | ; 10 110 | inc on_lines 111 | dey 112 | bne lineloop_off 113 | 114 | hit_off: 115 | bail: 116 | waste_12: 117 | rts 118 | .endproc 119 | 120 | .assert >zapkernel_yonoff_ntsc = >*, error, "zapkernel.s crosses page boundary" 121 | -------------------------------------------------------------------------------- /src/ppuclear.s: -------------------------------------------------------------------------------- 1 | ; 2 | ; NES PPU common functions 3 | ; Copyright 2011 Damian Yerrick 4 | ; 5 | ; Copying and distribution of this file, with or without 6 | ; modification, are permitted in any medium without royalty provided 7 | ; the copyright notice and this notice are preserved in all source 8 | ; code copies. This file is offered as-is, without any warranty. 9 | ; 10 | .include "nes.inc" 11 | .include "global.inc" 12 | .export ppu_clear_nt, ppu_clear_oam, ppu_screen_on, ppu_wait_vblank 13 | .export ppu_screen_on_scroll_0 14 | 15 | ;; 16 | ; Clears a nametable to a given tile number and attribute value. 17 | ; (Turn off rendering in PPUMASK and set the VRAM address increment 18 | ; to 1 in PPUCTRL first.) 19 | ; @param A tile number 20 | ; @param X base address of nametable ($20, $24, $28, or $2C) 21 | ; @param Y attribute value ($00, $55, $AA, or $FF) 22 | .proc ppu_clear_nt 23 | 24 | ; Set base PPU address to XX00 25 | stx PPUADDR 26 | ldx #$00 27 | stx PPUADDR 28 | 29 | ; Clear the 960 spaces of the main part of the nametable, 30 | ; using a 4 times unrolled loop 31 | ldx #960/4 32 | loop1: 33 | .repeat 4 34 | sta PPUDATA 35 | .endrepeat 36 | dex 37 | bne loop1 38 | 39 | ; Clear the 64 entries of the attribute table 40 | ldx #64 41 | loop2: 42 | sty PPUDATA 43 | dex 44 | bne loop2 45 | rts 46 | .endproc 47 | 48 | ;; 49 | ; Moves all sprites starting at address X (e.g, $04, $08, ..., $FC) 50 | ; below the visible area. 51 | ; X is 0 at the end. 52 | .proc ppu_clear_oam 53 | 54 | ; First round the address down to a multiple of 4 so that it won't 55 | ; freeze should the address get corrupted. 56 | txa 57 | and #%11111100 58 | tax 59 | lda #$FF ; Any Y value from $EF through $FF will work 60 | loop: 61 | sta OAM,x 62 | inx 63 | inx 64 | inx 65 | inx 66 | bne loop 67 | rts 68 | .endproc 69 | 70 | .proc ppu_screen_on_scroll_0 71 | ldx #0 72 | ldy #0 73 | ;,; jmp ppu_screen_on 74 | .endproc 75 | 76 | ;; 77 | ; Sets the scroll position and turns PPU rendering on. 78 | ; @param A value for PPUCTRL ($2000) including scroll position 79 | ; MSBs; see nes.inc 80 | ; @param X horizontal scroll position (0-255) 81 | ; @param Y vertical scroll position (0-239) 82 | ; @param C if true, sprites will be visible 83 | .proc ppu_screen_on 84 | stx PPUSCROLL 85 | sty PPUSCROLL 86 | sta PPUCTRL 87 | lda #BG_ON 88 | bcc :+ 89 | lda #BG_ON|OBJ_ON 90 | : 91 | sta PPUMASK 92 | rts 93 | .endproc 94 | 95 | .proc ppu_wait_vblank 96 | lda nmis 97 | : 98 | cmp nmis 99 | beq :- 100 | rts 101 | .endproc 102 | -------------------------------------------------------------------------------- /src/quadpcm.s: -------------------------------------------------------------------------------- 1 | .include "nes.inc" 2 | .importzp ciBufEnd, ciSrc 3 | ciBlocksLeft = ciBufEnd 4 | .export quadpcm_test, quadpcm_playPages 5 | 6 | .segment "CODE" 7 | .proc quadpcm_test 8 | ldy #selnow_qdp 10 | ldx #>(selnow_qdp_end - selnow_qdp) 11 | ;,;jmp quadpcm_playPages 12 | .endproc 13 | 14 | .proc quadpcm_playPages 15 | y_start = 2 16 | thisSample = 3 17 | lastSample = 4 18 | deEss = 5 19 | sty y_start 20 | sta ciSrc+1 21 | inx 22 | stx ciBlocksLeft 23 | lda #$00 24 | sta ciSrc+0 25 | lda #64 26 | sta thisSample 27 | sta lastSample 28 | 29 | next_page: 30 | dec ciBlocksLeft 31 | beq return 32 | jsr read_ciSrc 33 | sta deEss 34 | 35 | wait_20c: 36 | nop 37 | nop 38 | ldx #3 39 | wait_20c_loop: 40 | dex 41 | bne wait_20c_loop 42 | 43 | play_byte: 44 | lda (ciSrc),y ; read but don't increment 45 | and #$0F 46 | jsr decode_samples 47 | 48 | wait_35c: 49 | nop 50 | nop 51 | ldx #6 52 | wait_35c_loop: 53 | dex 54 | bne wait_35c_loop 55 | 56 | jsr read_ciSrc 57 | ; Fetch upper nibble 58 | lsr a 59 | lsr a 60 | lsr a 61 | lsr a 62 | jsr decode_samples 63 | 64 | cpy y_start 65 | beq next_page 66 | 67 | wait_55c: 68 | nop 69 | nop 70 | ldx #10 71 | wait_55c_loop: 72 | dex 73 | bne wait_55c_loop 74 | 75 | beq play_byte ;,; jmp play_byte 76 | 77 | read_ciSrc: 78 | lda (ciSrc),y 79 | iny 80 | ; if Z = 0, 2+2+4 cycles 81 | ; if Z = 1, 3+5 cycles 82 | beq inc_ciSrc_hi 83 | nop 84 | .byte $2c ; bit opcode to skip next instruction 85 | inc_ciSrc_hi: 86 | inc ciSrc+1 87 | return: 88 | rts 89 | 90 | decode_samples: 91 | tax 92 | lda quadpcm_deltas,x 93 | clc 94 | adc lastSample 95 | and #$7F 96 | sta thisSample 97 | interpolate: 98 | clc 99 | adc lastSample 100 | lsr a 101 | eor deEss 102 | output_samples: 103 | sta $4011 ; 112 cycles between output samples ~= 15980 hz 104 | ; end sample period 105 | wait_102c: 106 | ldx #19 107 | wait_102c_loop: 108 | dex 109 | bne wait_102c_loop 110 | lda (ciSrc,x) 111 | 112 | lda thisSample 113 | sta lastSample 114 | sta $4011 ; 112 cycles between output samples ~= 15980 hz 115 | rts 116 | .endproc 117 | .assert >quadpcm_playPages = >*, error, "quadpcm_playPages crosses page boundary" 118 | 119 | .segment "CODE" 120 | quadpcm_deltas: 121 | .byt 0,1,4,9,16,25,36,49 122 | .byt 64,<-49,<-36,<-25,<-16,<-9,<-4,<-1 123 | .assert >quadpcm_deltas = >*, error, "quadpcm_deltas crosses page boundary" 124 | 125 | .segment "VOICEDATA" 126 | selnow_qdp: 127 | .incbin "obj/nes/selnow.qdp" 128 | selnow_qdp_end: 129 | -------------------------------------------------------------------------------- /tools/chnutils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Nametable compression for cut scenes 4 | # Copyright 2011-2015 Damian Yerrick 5 | # 6 | # Copying and distribution of this file, with or without 7 | # modification, are permitted in any medium without royalty 8 | # provided the copyright notice and this notice are preserved. 9 | # This file is offered as-is, without any warranty. 10 | # 11 | from __future__ import with_statement 12 | 13 | def histo(it): 14 | """Count occurrences of each distinct element in an iterable.""" 15 | out = {} 16 | for el in it: 17 | out.setdefault(el, 0) 18 | out[el] += 1 19 | return out 20 | 21 | def dedupe_chr(chrdata): 22 | seen_chrdata = {} 23 | nt = [] 24 | for tile in chrdata: 25 | seen_chrdata.setdefault(tile, len(seen_chrdata)) 26 | nt.append(seen_chrdata[tile]) 27 | seen_chrdata = sorted(seen_chrdata.items(), key=lambda x: x[1]) 28 | seen_chrdata = [row[0] for row in seen_chrdata] 29 | return (seen_chrdata, nt) 30 | 31 | def compress_nt(ntdata): 32 | from sys import stderr as red 33 | from bitbuilder import BitBuilder, log2 34 | 35 | runcounts = {} # used for determining backref 36 | base = 0 37 | runs = [] 38 | greatest = -1 39 | while base < len(ntdata): 40 | 41 | # measure the run of new tiles (greatest+i+1) 42 | # starting at t 43 | i = 0 44 | imax = min(128, len(ntdata) - base) 45 | while (i < imax 46 | and (greatest + i + 1) % 256 == ntdata[base + i]): 47 | i += 1 48 | if i > 0: 49 | greatest += i 50 | base += i 51 | runs.append((-1, i)) 52 | continue 53 | 54 | # measure the +0 run starting at t 55 | i = 1 56 | imax = min(128, len(ntdata) - base) 57 | while (i < imax 58 | and ntdata[base] == ntdata[base + i]): 59 | i += 1 60 | 61 | # we use the same number of bits for a backreference 62 | # that are in greatest 63 | runs.append((ntdata[base], i, log2(greatest) + 1)) 64 | runcounts.setdefault(ntdata[base], 0) 65 | runcounts[ntdata[base]] += 1 66 | base += i 67 | runcounts = sorted(runcounts.items(), key=lambda x: -x[1]) 68 | most_common_backref = runcounts[0][0] if len(runcounts) else 0 69 | 70 | out = BitBuilder() 71 | out.append(most_common_backref, 8) 72 | for row in runs: 73 | idx, runlength = row[:2] 74 | out.appendGamma(runlength - 1) 75 | if idx < 0: 76 | out.append(2, 2) 77 | elif idx == most_common_backref: 78 | out.append(3, 2) 79 | else: 80 | nbits = row[2] 81 | if idx >= 1 << nbits: 82 | print("index FAIL! %d can't fit in %d bits" % (idx, nbits), 83 | file=red) 84 | out.append(idx, nbits + 1) 85 | return str(out) 86 | -------------------------------------------------------------------------------- /tools/a53charset.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import codecs 3 | 4 | # Make a special character codepage mapping starting from ASCII printable 5 | # characters and adding special characters in non-whitespace ASCII control 6 | # characters. So that 128 of the 256 available characters can be used 7 | # for DTE code units. 8 | name = 'action53' 9 | decoding_table = [ 10 | # Some ASCII control characters 11 | # 0x00 and 0x0a are for termination and newline respectively 12 | *range(16), 13 | # unassigned, e with acute, n with tilde, o with diaeresis 14 | 0xFFFE, 0x00E9, 0x00F1, 0x00F6, 15 | # copyright, L with stroke, z with dot, e with tail 16 | 0x00A9, 0x0141, 0x017C, 0x0119, 17 | # A button, B button, d-pad, bird 18 | 0x24B6, 0x24B7, 0x271C, 0x1F426, 19 | # up arrow, down arrow, left arrow, right arrow 20 | 0x2191, 0x2193, 0x2190, 0x2192, 21 | # Ascii printable characters 22 | *range(32,127), 23 | # 1 px space, in the place of ASCII DEL 24 | 0x2423, 25 | # reserved for DTE 26 | *[0xFFFE]*128, 27 | ] 28 | 29 | ### encoding map from decoding table 30 | 31 | #encoding_table = codecs.charmap_build(''.join(chr(x) for x in decoding_table)) 32 | encoding_table = dict((c,i) for (i,c) in enumerate(decoding_table)) 33 | 34 | # Codecs API boilerplate ############################################ 35 | 36 | ### Codec APIs 37 | 38 | class Codec(codecs.Codec): 39 | 40 | def encode(self,input,errors='strict'): 41 | return codecs.charmap_encode(input,errors,encoding_table) 42 | 43 | def decode(self,input,errors='strict'): 44 | return codecs.charmap_decode(input,errors,decoding_table) 45 | 46 | class IncrementalEncoder(codecs.IncrementalEncoder): 47 | def encode(self, input, final=False): 48 | return codecs.charmap_encode(input,self.errors,encoding_table)[0] 49 | 50 | class IncrementalDecoder(codecs.IncrementalDecoder): 51 | def decode(self, input, final=False): 52 | return codecs.charmap_decode(input,self.errors,decoding_table)[0] 53 | 54 | class StreamWriter(Codec,codecs.StreamWriter): 55 | pass 56 | 57 | class StreamReader(Codec,codecs.StreamReader): 58 | pass 59 | 60 | ### encodings module API 61 | 62 | def getregentry(): 63 | return codecs.CodecInfo( 64 | name=name, 65 | encode=Codec().encode, 66 | decode=Codec().decode, 67 | incrementalencoder=IncrementalEncoder, 68 | incrementaldecoder=IncrementalDecoder, 69 | streamreader=StreamReader, 70 | streamwriter=StreamWriter, 71 | ) 72 | 73 | def register(): 74 | ci = getregentry() 75 | def lookup(encoding): 76 | if encoding == name: 77 | return ci 78 | codecs.register(lookup) 79 | 80 | # End boilerplate ################################################### 81 | 82 | ### Testing 83 | 84 | def main(): 85 | register() 86 | s = "HELŁO" 87 | b = s.encode(name) 88 | print(s) 89 | print(b.hex()) 90 | 91 | if __name__=='__main__': 92 | main() 93 | -------------------------------------------------------------------------------- /src/unpb53.s: -------------------------------------------------------------------------------- 1 | ; 2 | ; PB53 unpacker for 6502 systems 3 | ; Copyright 2012 Damian Yerrick 4 | ; 5 | ; Copying and distribution of this file, with or without 6 | ; modification, are permitted in any medium without royalty provided 7 | ; the copyright notice and this notice are preserved in all source 8 | ; code copies. This file is offered as-is, without any warranty. 9 | ; 10 | .export unpb53_some, PB53_outbuf 11 | .export unpb53_block_ay, unpb53_block 12 | .importzp ciSrc, ciBufStart, ciBufEnd 13 | 14 | PB53_outbuf = $0100 15 | 16 | ; the decompressor is less than 176 bytes, useful for loading into 17 | ; RAM with a trampoline 18 | .segment "CODE" 19 | .proc unpb53_some 20 | ctrlbyte = 0 21 | bytesLeft = 1 22 | ldx ciBufStart 23 | loop: 24 | ldy #0 25 | lda (ciSrc),y 26 | inc ciSrc 27 | bne :+ 28 | inc ciSrc+1 29 | : 30 | cmp #$82 31 | bcc twoPlanes 32 | beq copyLastTile 33 | cmp #$84 34 | bcs solidColor 35 | 36 | ; at this point we're copying from the first stream to this one 37 | ; assuming that we're decoding two streams in parallel and the 38 | ; first stream's decompression buffer is PB53_outbuf[0:ciBufStart] 39 | txa 40 | sec 41 | sbc ciBufStart 42 | tay 43 | copyTile_ytox: 44 | lda #16 45 | sta bytesLeft 46 | prevStreamLoop: 47 | lda PB53_outbuf,y 48 | sta PB53_outbuf,x 49 | inx 50 | iny 51 | dec bytesLeft 52 | bne prevStreamLoop 53 | tileDone: 54 | cpx ciBufEnd 55 | bcc loop 56 | rts 57 | 58 | copyLastTile: 59 | txa 60 | cmp ciBufStart 61 | bne notAtStart 62 | lda ciBufEnd 63 | notAtStart: 64 | sec 65 | sbc #16 66 | tay 67 | jmp copyTile_ytox 68 | 69 | solidColor: 70 | pha 71 | jsr solidPlane 72 | pla 73 | lsr a 74 | jsr solidPlane 75 | jmp tileDone 76 | 77 | twoPlanes: 78 | jsr onePlane 79 | ldy #0 80 | lda (ciSrc),y 81 | inc ciSrc 82 | bne :+ 83 | inc ciSrc+1 84 | : 85 | cmp #$82 86 | bcs copyPlane0to1 87 | jsr onePlane 88 | jmp tileDone 89 | 90 | copyPlane0to1: 91 | ldy #8 92 | and #$01 93 | beq noInvertPlane0 94 | lda #$FF 95 | noInvertPlane0: 96 | sta ctrlbyte 97 | copyPlaneLoop: 98 | lda a:PB53_outbuf-8,x 99 | eor ctrlbyte 100 | sta PB53_outbuf,x 101 | inx 102 | dey 103 | bne copyPlaneLoop 104 | jmp tileDone 105 | 106 | onePlane: 107 | ora #$00 108 | bpl pb8Plane 109 | solidPlane: 110 | ldy #8 111 | and #$01 112 | beq solidLoop 113 | lda #$FF 114 | solidLoop: 115 | sta PB53_outbuf,x 116 | inx 117 | dey 118 | bne solidLoop 119 | rts 120 | 121 | pb8Plane: 122 | sec 123 | rol a 124 | sta ctrlbyte 125 | lda #$00 126 | pb8loop: 127 | 128 | ; at this point: 129 | ; A: previous byte in this plane 130 | ; C = 0: copy byte from bitstream 131 | ; C = 1: repeat previous byte 132 | bcs noNewByte 133 | lda (ciSrc),y 134 | iny 135 | noNewByte: 136 | sta PB53_outbuf,x 137 | inx 138 | asl ctrlbyte 139 | bne pb8loop 140 | clc 141 | tya 142 | adc ciSrc 143 | sta ciSrc 144 | bcc :+ 145 | inc ciSrc+1 146 | : 147 | rts 148 | .endproc 149 | 150 | .global draw_progress 151 | ;; 152 | ; decompress X*16 bytes starting at AAYY to PPUDATA 153 | .proc unpb53_block_ay 154 | sty ciSrc 155 | sta ciSrc+1 156 | .endproc 157 | .proc unpb53_block 158 | stx draw_progress 159 | lda #16 160 | sta ciBufEnd 161 | lda #0 162 | sta ciBufStart 163 | loop: 164 | jsr unpb53_some 165 | ldx #0 166 | copyloop: 167 | lda PB53_outbuf,x 168 | sta $2007 169 | inx 170 | cpx #16 171 | bcc copyloop 172 | dec draw_progress 173 | bne loop 174 | rts 175 | .endproc 176 | -------------------------------------------------------------------------------- /src/global.inc: -------------------------------------------------------------------------------- 1 | ; Globals for Action 53 2 | ; Copyright 2012-2018 Damian Yerrick 3 | ; 4 | ; Copying and distribution of this file, with or without 5 | ; modification, are permitted in any medium without royalty provided 6 | ; the copyright notice and this notice are preserved in all source 7 | ; code copies. This file is offered as-is, without any warranty. 8 | 9 | ; main.s fields 10 | .globalzp cur_keys, cur_trigger, new_keys, nmis, tvSystem 11 | .global OAM 12 | USE_TITLE_MSGS = 0 13 | ; main.s methods 14 | .global read_mouse_with_backward_buttons, read_zapper_trigger 15 | .global pently_update_lag 16 | 17 | ; title.s methods 18 | .global title_screen, no_games_error 19 | 20 | ; .s ZP variables 21 | start_bankptr = $00 ; Pointer to the activity's outer bank number 22 | start_entrypoint = $FE ; Activity's entry point: JMP ($00FE) 23 | start_mappercfg = $02 ; Mapper configuration byte for this activity 24 | ; .s methods 25 | .global init_mapper, start_game 26 | 27 | ; cartmenu.s fields 28 | .globalzp draw_progress 29 | .globalzp tab_tilelens ; reused as locals for draw_title_strings 30 | ; cartmenu.s methods 31 | .global cart_menu, get_titledir_a 32 | 33 | ; paldetect.s methods 34 | .global getTVSystem 35 | 36 | ; pads.s methods 37 | .global read_pads 38 | 39 | ; mouse.s fields 40 | .globalzp cur_mbuttons, new_mbuttons 41 | ; mouse.s methods 42 | .global read_mouse, mouse_change_sensitivity 43 | 44 | ; ppuclear.s fields 45 | .globalzp oam_used 46 | ; ppuclear.s methods 47 | .global ppu_clear_nt, ppu_clear_oam, ppu_screen_on, ppu_wait_vblank 48 | .global ppu_screen_on_scroll_0 49 | 50 | ; paldetect.s methods 51 | .global getTVSystem 52 | 53 | ; bcd.s methods 54 | .global bcd8bit 55 | 56 | ; unpb8.s fields 57 | .globalzp ciSrc, ciBufStart, ciBufEnd 58 | .global PB53_outbuf, interbank_fetch_buf 59 | ; unpb8.s methods 60 | .global unpb53_some, unpb53_block_ay, unpb53_block, interbank_fetch 61 | 62 | ; donut.s methods 63 | .global donut_decompress_block, donut_block_ayx, donut_block_x 64 | ; donut.s fields 65 | .global donut_block_buffer 66 | .globalzp donut_stream_ptr, donut_block_count 67 | 68 | ; undte.s constants 69 | DTE_OUTPUT_LEN = 64 70 | ; undte.s fields 71 | dte_output_buf = interbank_fetch_buf + 128 72 | ; undte.s methods 73 | .global undte_line, undte_line0 74 | 75 | ; vwf_draw.s methods 76 | .global vwfStrWidth, vwfGlyphWidth, vwfPuts 77 | .global clearLineImg, copyLineImg, invertTiles 78 | .globalzp lineImgBufLen, FIRST_PRINTABLE_CU 79 | 80 | ; zapkernels.s methods 81 | .global zapkernel_yonoff_ntsc 82 | 83 | ; identify.s constants 84 | DETECT_2P_ZAPPER = $08 85 | DETECT_1P_MOUSE = $01 86 | ; identify.s fields 87 | .globalzp detected_pads 88 | ; identify.s methods 89 | .global identify_controllers 90 | 91 | ; quadpcm.s methods 92 | .global quadpcm_test 93 | 94 | ; Directories 95 | DIRECTORY_HEADER = $8000 96 | NEG_NUMBER_OF_BANKS = $8005 97 | CHRDIR_START = $8008 98 | SCREENSLIST = $800a 99 | TITLELIST = $800c 100 | PAGELIST = $800e 101 | NAMESLIST = $8010 102 | DESCSLIST = $8012 103 | DESCSBANK = $8014 104 | TITLESCREEN = $8016 105 | TITLESTRINGS = $8018 106 | DTE_REPLACEMENTS = $801a 107 | BANK_CHECKSUMS = $801c 108 | _UNUSED_ROM_DIR = $801e 109 | 110 | TITLE_PRG_BANK = $00 111 | TITLE_CHR_BANK = $01 112 | TITLE_SCREENSHOT = $02 113 | TITLE_YEAR = $03 114 | TITLE_PLAYERS_TYPE= $04 115 | TITLE_NUMBER_OF_CHR = $05 116 | TITLE_NAME_OFFSET = $08 117 | TITLE_DESC_OFFSET = $0A 118 | TITLE_ENTRY_POINT = $0C 119 | TITLE_MAPPER_CFG = $0E 120 | 121 | TITLE_BASE_YEAR = 1970 122 | -------------------------------------------------------------------------------- /src/musicseq.pently: -------------------------------------------------------------------------------- 1 | # 2 | # Pently audio engine 3 | # Sample songs 4 | # 5 | # Copyright 2001-2018 Damian Yerrick 6 | # 7 | # This software is provided 'as-is', without any express or implied 8 | # warranty. In no event will the authors be held liable for any damages 9 | # arising from the use of this software. 10 | # 11 | # Permission is granted to anyone to use this software for any purpose, 12 | # including commercial applications, and to alter it and redistribute it 13 | # freely, subject to the following restrictions: 14 | # 15 | # 1. The origin of this software must not be misrepresented; you must not 16 | # claim that you wrote the original software. If you use this software 17 | # in a product, an acknowledgment in the product documentation would be 18 | # appreciated but is not required. 19 | # 2. Altered source versions must be plainly marked as such, and must not be 20 | # misrepresented as being the original software. 21 | # 3. This notice may not be removed or altered from any source distribution. 22 | # 23 | 24 | durations stick 25 | notenames english 26 | 27 | sfx move_cursor on pulse 28 | timbre 1 29 | volume 15 4 30 | pitch a'':2 31 | 32 | sfx pageupdown on noise 33 | rate 2 34 | volume 7 9 9 8 6 35 | pitch 4 5 4 3 1 36 | 37 | sfx view_description on pulse 38 | timbre 1 39 | volume 15 4 15 4 15 4 4 2 4 2 4 2 2 1 2 1 2 1 40 | pitch | a':2 d'':2 g'':2 41 | 42 | sfx start_activity on pulse 43 | timbre 1:6 2:9 44 | volume 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 45 | pitch | c'' d#'' f'' c'' d'' f'' 46 | 47 | sfx kick on noise 48 | volume 12 10 8 6 5 4 3 2 1 1 49 | pitch 10 0 50 | 51 | sfx snare on noise 52 | volume 12 10 8 6 5 4 3 2 1 1 53 | pitch 4 10 54 | 55 | sfx hihat on noise 56 | volume 4 2 2 1 57 | pitch 12 58 | timbre | 0 1 59 | 60 | sfx openhat on noise 61 | volume 6 5 4 4 3:3 2:3 1:5 62 | pitch 12 63 | timbre | 0 1 64 | 65 | drum kick kick 66 | drum snare snare 67 | drum clhat hihat 68 | drum ohat openhat 69 | 70 | instrument bass 71 | 72 | instrument framepop 73 | volume 14 0 74 | 75 | # Attack injection 76 | # by Damian Yerrick, 2015 77 | # If anything, inspired by "The Big One 78 | # (Theme from The People's Court)" 79 | # Also similar to "Subterranean Creep" from Haunted: Halloween '85 80 | # for NES, but Greg Caldwell of Retrotainment Games said "It's 81 | # different enough" in a Skype conversation on June 19, 2016 82 | song attacktest 83 | time 12/4 84 | scale 8 85 | tempo 85.9 86 | # This is technically in New Jack Swing time, where the second 87 | # division is compound instead of the first, but there's no 88 | # classical time sig for NJS. So instead, we choose a time 89 | # signature that forces the correct beat length (dotted half). 90 | 91 | at 1 92 | pattern drums 93 | kick4. clhat snare clhat4 kick8 94 | clhat4. kick snare clhat 95 | kick4. clhat snare clhat4 snare8 96 | clhat4. kick snare ohat 97 | play drums 98 | pattern bass on triangle with bass 99 | eb2 r8 f4 r1 r2. c4 r8 100 | eb2 r8 f4 r4 EP16 ab1 r2 EP00 c4 r8 101 | eb2 r8 f4 r1 r2. c4 r8 102 | # 2018-03-20: A bug that required staying on a note for at least 103 | # 2 frames (2g) before it's picked up as the portamento base 104 | # is now fixed. 105 | eb2 r8 f4 r4 eb1g EP16 gb1 EP00 r2 c4 r8 106 | play bass 107 | attack on triangle 108 | pattern atk on attack with framepop 109 | relative 110 | f'''4 ab8 c4 ab8 c4 eb8 c4 eb8 111 | g4 eb8 c4 eb8 c4 ab8 c4 ab8 112 | play atk 113 | 114 | at 4 115 | play atk down 2 116 | at 4:4 117 | pattern drumfill 118 | snare4 ohat8 snare snare snare 119 | play drumfill 120 | at 5 121 | fine 122 | # da capo 123 | -------------------------------------------------------------------------------- /tools/firstfit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | def slices_union(seq): 4 | """Sort 2-tuples and combine them as if right-open intervals.""" 5 | out = [] 6 | for (start, end) in sorted(seq): 7 | if len(out) < 1 or out[-1][1] < start: 8 | out.append((start, end)) 9 | else: 10 | out[-1] = (out[-1][0], end) 11 | return out 12 | 13 | # slices_union([(5, 8), (12, 15), (3, 6), (10, 12)]) 14 | # should return (3, 8), (10, 15) 15 | 16 | def slices_find(slices, start_end): 17 | """Return the index of the slice that contains a given slice, or -1.""" 18 | from bisect import bisect_right 19 | slice_r = bisect_right(slices, start_end) 20 | start, end = start_end 21 | if slice_r > 0: 22 | (l, r) = slices[slice_r - 1] 23 | if l <= start and end <= r: 24 | return slice_r - 1 25 | if slice_r < len(slices): 26 | (l, r) = slices[slice_r] 27 | if l <= start and end <= r: 28 | return slice_r 29 | return -1 30 | 31 | def slices_remove(slices, start_end): 32 | """Remove a slice from a list of slices.""" 33 | idx = slices_find(slices, start_end) 34 | start, end = start_end 35 | if idx < 0: 36 | raise KeyError("%s not found" % repr(start_end)) 37 | 38 | if slices[idx] == (start, end): # deleting an entire slice 39 | del slices[idx] 40 | elif slices[idx][0] == start: # cutting the start of a slice 41 | slices[idx] = (end, slices[idx][1]) 42 | elif slices[idx][1] == end: # cutting the end of a slice 43 | slices[idx] = (slices[idx][0], start) 44 | else: # cutting the middle out of a slice 45 | slices[idx:idx + 1] = [(slices[idx][0], start), (end, slices[idx][1])] 46 | 47 | def ffd_find(prgbanks, datalen, bank_factory=None): 48 | """Find the first unused range that will accept a given piece of data. 49 | 50 | prgbanks -- a list of (bytearray, slice list) tuples 51 | datalen -- the length of a byte string to insert in an unused area 52 | bank_factory -- a function returning (bytearray, slice list), called 53 | when data doesn't fit, or None to instead throw ValueError 54 | 55 | We use the First Fit Decreasing algorithm, which has been proven no 56 | more than 22% inefficient (Yue 1991). Because we don't plan to 57 | insert more than about 100 things into a ROM at once, we can deal 58 | with O(n^2) time complexity and don't need the fancy data structures 59 | that O(n log n) requires. Yet. 60 | 61 | Return a (bank, address) tuple denoting where it would be inserted. 62 | """ 63 | 64 | for (bank, (prgdata, unused_ranges)) in enumerate(prgbanks): 65 | for (start, end) in unused_ranges: 66 | if start + datalen <= end: 67 | return (bank, start) 68 | 69 | # At this point we need to add another PRG bank. Create a PRG 70 | # bank that has the reset patch built into it. 71 | if not bank_factory: 72 | raise ValueError("could not add bank") 73 | 74 | prgbanks.append(bank_factory()) 75 | last_bank_ranges = prgbanks[-1][1] 76 | if datalen > last_bank_ranges[0][1] - last_bank_ranges[0][0]: 77 | raise ValueError("string too long") 78 | return (len(prgbanks) - 1, 0x8000) 79 | 80 | def ffd_add(prgbanks, data, bank_factory=None): 81 | """Insert a string into a bank using FFD. 82 | 83 | data -- the byte string to insert 84 | 85 | Other arguments and return same as those for ffd_find. 86 | """ 87 | from array import array 88 | 89 | (bank, address) = ffd_find(prgbanks, len(data), bank_factory) 90 | offset = address - 0x8000 91 | (romdata, unused_ranges) = prgbanks[bank] 92 | romdata[offset:offset + len(data)] = array('B', data) 93 | slices_remove(unused_ranges, (address, address + len(data))) 94 | return (bank, address) 95 | 96 | -------------------------------------------------------------------------------- /tools/nesasm2ca65.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | 4 | NESASM to ca65 syntax translator 5 | 6 | Copyright 2012, 2020 Damian Yerrick 7 | [insert zlib license here] 8 | 9 | This tool covers just enough of NESASM's syntax to translate 10 | LAN Master from NESASM to ca65 for inclusion in a submulti. 11 | 12 | """ 13 | 14 | import sys 15 | import re 16 | 17 | # Ordinarily we call the vectors segment "VECTORS", but we change it 18 | # if we want the submulti shell to override a game's vectors. 19 | # In LAN Master + Munchie Attack, we're discarding bank 4 because 20 | # we're mapper hacking LAN Master to CHR RAM. 21 | vectors_segment = "BANK4" 22 | globals_line = '.global NMI_CALL, reset' 23 | 24 | # these are applied only to the first word after the label if any 25 | opcode_translation = { 26 | 'equ': '=', 27 | '.db': '.byte', 28 | '.list': '.list on', 29 | '.nolist': '.list off', 30 | '.endm': '.endmacro', 31 | '.dw': '.addr', 32 | '.ds': '.res', 33 | '.org': ';.org', 34 | '.zp': '.zeropage', 35 | '.fail': '.assert 0, error, ".fail"', 36 | '.inesprg': ';.inesprg', 37 | '.ineschr': ';.ineschr', 38 | '.inesmir': ';.inesmir', 39 | '.inesmap': ';.inesmap', 40 | } 41 | 42 | words_translation = { 43 | # reference: https://cc65.github.io/doc/ca65.html 44 | # reference: https://raw.githubusercontent.com/camsaul/nesasm/master/usage.txt 45 | 'low': '.lobyte', 46 | 'high': '.hibyte', 47 | 'bank': '<.bank', 48 | '[': '(', # NESASM's syntax for (d),Y and (d,X) 49 | ']': ')', # addressing modes is nonstandard 50 | } 51 | 52 | # Obscurities such as .func, .macro arguments, .incchr, .defchr, etc. 53 | # are not translated. 54 | 55 | words_nonwordsRE = re.compile(r"[$%.0-9a-zA-Z_]+|[^$%.0-9a-zA-Z_]") 56 | 57 | def translate_word(word): 58 | ## print(";translating %s" % word) 59 | word = words_translation.get(word.lower(), word) 60 | if word.startswith('.'): 61 | word = '@' + word[1:] 62 | return word 63 | 64 | def translate_line(line): 65 | line_comment = line.split(';', 1) 66 | comment = line_comment[1] if len(line_comment) > 1 else '' 67 | line = line_comment[0].rstrip() 68 | if line == '': 69 | return '' 70 | 71 | # Apparently a label MUST begin in the first column, 72 | # and an instruction MUST NOT. 73 | splitParts = line.split() 74 | if not line[0].isspace(): 75 | label = splitParts[0] 76 | if label.startswith('.'): 77 | label = '@' + label[1:] 78 | splitParts = splitParts[1:] 79 | else: 80 | label = '' 81 | if len(splitParts) > 0: 82 | opcode = splitParts[0] 83 | splitParts = splitParts[1:] 84 | else: 85 | opcode = '' 86 | 87 | # translate .db into .byt, etc. 88 | opcode = opcode_translation.get(opcode.lower(), opcode) 89 | 90 | # ca65 puts a colon after each label that's not part of an equate 91 | if label != '' and opcode != '=': 92 | label = label + ':' 93 | 94 | # ca65 uses link scripts 95 | if opcode == '.bank' and len(splitParts) == 1: 96 | opcode = '.segment' 97 | splitParts = ['"BANK%s"' % splitParts[0]] 98 | elif (opcode == ';.org' and len(splitParts) == 1 99 | and splitParts[0].lower() == '$fffa'): 100 | opcode = '.segment' 101 | splitParts = ['"%s"' % vectors_segment] 102 | 103 | splitParts = [''.join(translate_word(word) 104 | for word in words_nonwordsRE.findall(part)) 105 | for part in splitParts] 106 | 107 | return "%s %s %s" % (label, opcode, ' '.join(splitParts)) 108 | 109 | def main(): 110 | printed_globals_line = False 111 | # sys.stdin of IDLE in Python 2.6 wasn't iterable. 112 | # Fortunately, this was fixed by Python 3.6. 113 | for line in sys.stdin: 114 | line = translate_line(line) 115 | if line == '' and not printed_globals_line: 116 | line = globals_line 117 | printed_globals_line = True 118 | sys.stdout.write(line + "\n") 119 | 120 | # The globals line should have replaced an existing blank line. 121 | # (This is done to preserve source line numbers.) If there was 122 | # no blank line, drop it here. 123 | if globals_line and not printed_globals_line: 124 | sys.stdout.write(globals_line + "\n") 125 | 126 | if __name__=='__main__': 127 | main() 128 | -------------------------------------------------------------------------------- /tools/autosubmulti.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import ines, os, sys 3 | from binascii import a2b_hex 4 | 5 | 6 | # [0]: Output file 7 | # [1]: ROM file for first half of bank 8 | # [2]: Address of 16-byte reset patch in first half (in $8000-$BFE0) 9 | # [3]: ROM file for second half of bank 10 | submultis = [ 11 | ( 12 | '../submulti/SM_JupiterScope2_SuperTiltBro.nes', 13 | '../revised3/jupiter2.nes', 14 | 0xBFE0, 15 | '../revised3/Super_Tilt_Bro_v4_(E).nes' 16 | ), 17 | ( 18 | '../submulti/SM_BrickBreaker_RPSLS.nes', 19 | '../revised3/brix_2018-02-28.nes', 20 | 0xBFB8, 21 | '../revised3/Rock9b.nes' 22 | ), 23 | ] 24 | 25 | def get_vectors(prg): 26 | prg = prg[-6:] 27 | return [prg[i + 1] * 256 + prg[i] for i in (0, 2, 4)] 28 | 29 | def ls_roms(folder='.'): 30 | for filename in sorted(os.listdir(folder), key=str.lower): 31 | try: 32 | rom = ines.load_ines(os.path.join(folder, filename)) 33 | except Exception as e: 34 | continue 35 | if len(rom['prg']) != 16384: 36 | continue 37 | nmi, reset, irq = get_vectors(rom['prg']) 38 | trominfo = ("%s:\n %d kbit PRG, %d kbit CHR, %s mirroring,\n" 39 | " nmi:$%04x reset:$%04x irq:$%04x" 40 | % (filename, 41 | len(rom['prg']) // 128, len(rom.get('chr', '')) // 128, 42 | rom['mirrtype'], 43 | nmi, reset, irq)) 44 | print(trominfo) 45 | 46 | 47 | # This adds a patch to $8000-$BFFF: 48 | # LDA #$01 STA $2000 ; kill NMI 49 | # STA $5000 STA $8000 ; put second 16K bank in $C000-$FFFF 50 | # LDA #$81 STA $5000 ; next $8000 write controls outer bank 51 | # JMP ($FFFC) 52 | # 53 | # After this, the standard reset patch in $C000-$FFFF takes over: 54 | # LDX #$FF STX $8000 ; set outer bank to last 55 | # JMP ($FFFC) ; jump to start of menu 56 | resetpatchtemplate = a2b_hex("A9018D00208D00508D0080A9818D00506CFCFF") 57 | 58 | def make_submulti(outfilename, rom80_filename, resetpatchaddr, romC0_filename): 59 | 60 | rom80 = ines.load_ines(rom80_filename) 61 | rom80_reset = get_vectors(rom80['prg'])[1] 62 | rom80_mirrtype = rom80['mirrtype'] 63 | prgrom = bytearray(rom80['prg']) 64 | chrrom = [rom80.get('chr', '')] 65 | del rom80 66 | 67 | romC0 = ines.load_ines(romC0_filename) 68 | romC0_mirrtype = romC0['mirrtype'] 69 | prgrom.extend(romC0['prg']) 70 | chrrom.append(romC0.get('chr', '')) 71 | del romC0 72 | 73 | rom80_mapmode = 0x8A if rom80_mirrtype == 'ABAB' else 0x8B 74 | romC0_mapmode = 0x8E if romC0_mirrtype == 'ABAB' else 0x8F 75 | print("[%s]\n" 76 | "rom80 (%s):\n entrypoint=%04X\n mapmode=%02X\n" 77 | "romC0 (%s):\n mapmode=%02X\n" 78 | % (outfilename, 79 | rom80_filename, rom80_reset, rom80_mapmode, 80 | romC0_filename, romC0_mapmode)) 81 | 82 | # In order to reset correctly, the game in the lower bank needs 83 | # to switch in the upper bank to run its reset code. 84 | # Print the lower bank's old reset vector (formerly in $BFFC-$BFFD) 85 | # The following code brings the reset vector into the upper 16K, 86 | # prepares to set the outer bank, and jumps to the submulti's 87 | # primary reset vector. 88 | # lda #$01 sta $2000 sta $5000 sta $8000 lda #$81 sta $5000 jmp ($FFFC) 89 | assert 0x8000 <= resetpatchaddr <= 0xBFFA - len(resetpatchtemplate) 90 | resetpatchdata = bytearray(resetpatchtemplate) 91 | resetpatchoffset = resetpatchaddr - 0x8000 92 | prgrom[0x3FFC] = resetpatchaddr & 0xFF 93 | prgrom[0x3FFD] = (resetpatchaddr >> 8) & 0xFF 94 | prgrom[resetpatchoffset:resetpatchoffset + len(resetpatchtemplate)] = resetpatchdata 95 | 96 | inesheader = bytearray(b"NES\x1A") 97 | inesheader.extend([(len(prgrom) // 16384), 98 | (sum(len(x) for x in chrrom) // 8192), 99 | 0xC0, 0x10]) 100 | inesheader.extend([0] * (16 - len(inesheader))) 101 | with open(outfilename, 'wb') as outfp: 102 | outfp.write(inesheader) 103 | outfp.write(prgrom) 104 | outfp.writelines(chrrom) 105 | 106 | def main(argv=None): 107 | argv = argv or sys.argv 108 | 109 | ## ls_roms("../revised3") 110 | ## ls_roms("../roms4") 111 | ## ls_roms("../revised4") 112 | for outfilename, rom80name, rom80patch, romC0name in submultis: 113 | make_submulti(outfilename, rom80name, rom80patch, romC0name) 114 | 115 | if __name__=='__main__': 116 | main() 117 | -------------------------------------------------------------------------------- /tools/crc16xmodem.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Pure python library for calculating CRC16""" 3 | 4 | ############################################################################## 5 | # 6 | # Copyright (C) Gennady Trafimenkov, 2011 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU Lesser General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Lesser General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Lesser General Public License 19 | # along with this program. If not, see . 20 | # 21 | ############################################################################## 22 | 23 | 24 | # table for calculating CRC 25 | # this particular table was generated using pycrc v0.7.6, http://www.tty1.net/pycrc/ 26 | # using the configuration: 27 | # * Width = 16 28 | # * Poly = 0x1021 29 | # * XorIn = 0x0000 30 | # * ReflectIn = False 31 | # * XorOut = 0x0000 32 | # * ReflectOut = False 33 | # * Algorithm = table-driven 34 | # by following command: 35 | # python pycrc.py --model xmodem --algorithm table-driven --generate c 36 | CRC16_XMODEM_TABLE = [ 37 | 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 38 | 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 39 | 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 40 | 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 41 | 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, 42 | 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 43 | 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, 44 | 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 45 | 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 46 | 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 47 | 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 48 | 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 49 | 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 50 | 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 51 | 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 52 | 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, 53 | 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 54 | 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 55 | 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 56 | 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 57 | 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 58 | 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 59 | 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, 60 | 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 61 | 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 62 | 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, 63 | 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 64 | 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 65 | 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 66 | 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 67 | 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 68 | 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, 69 | ] 70 | 71 | 72 | def _crc16(data, crc, table): 73 | """Calculate CRC16 using the given table. 74 | `data` - data for calculating CRC, must be bytes 75 | `crc` - initial value 76 | `table` - table for caclulating CRC (list of 256 integers) 77 | Return calculated value of CRC 78 | """ 79 | for byte in data: 80 | crc = ((crc<<8)&0xff00) ^ table[((crc>>8)&0xff)^byte] 81 | return crc & 0xffff 82 | 83 | 84 | def crc16xmodem(data, crc=0): 85 | """Calculate CRC-CCITT (XModem) variant of CRC16. 86 | `data` - data for calculating CRC, must be bytes 87 | `crc` - initial value 88 | Return calculated value of CRC 89 | """ 90 | return _crc16(data, crc, CRC16_XMODEM_TABLE) 91 | -------------------------------------------------------------------------------- /src/a53mapper.s: -------------------------------------------------------------------------------- 1 | ; Self-clearing Action 53 trampoline 2 | ; By jroatch, 2018-09-09 3 | 4 | .include "nes.inc" 5 | .include "global.inc" 6 | 7 | .segment "CODE" 8 | ;; 9 | ; Clears all but the top few stack bytes and starts the chosen 10 | ; activity. NMI and rendering must be turned off. 11 | ; @param start_entrypoint activity's entry point 12 | ; @param start_mappercfg activity's starting mapper configuration 13 | .proc start_game 14 | ; A53 Mapper register write sequence: 15 | ; 16 | ; $$00 = 0x00 17 | ; Meaning; $5000 = 0x00, $8000 = 0x00 18 | ; Which could expand to the cpu code; 19 | ; lda #$00 sta $5000 sta $8000 20 | ; $$01 = start_mappercfg & 0x0C if fixed-lo else start_mappercfg & 0x0F 21 | ; $$80 = start_mappercfg 22 | ; $$81 = (start_bankptr),TITLE_PRG_BANK 23 | ; $5000 = 0x00 if start_mappercfg & 0x80 24 | ; 0x01 if start_mappercfg & 0x30 25 | ; else 0x81 26 | sei ; Disable interrupts 27 | ldx #$00 28 | stx PPUCTRL 29 | stx PPUMASK 30 | 31 | ; Copy trampoline code to the end of the stack page with 'pha' 32 | ; so that the stack pointer ends up on its parameter stack 33 | dex ;,; ldx #$ff 34 | txs 35 | lda start_entrypoint+1 36 | pha 37 | lda start_entrypoint+0 38 | pha 39 | ldx #trampoline_code_size 40 | copy_trampoline_loop: 41 | lda trampoline_code_begin-1, x 42 | pha 43 | dex 44 | bne copy_trampoline_loop 45 | 46 | ; Compute parameters, in reverse order of register writing, 47 | ; and push them to the stack. 48 | 49 | ; Last thing first, set the final register select for game execution. 50 | ; Rules for setting final register in $5000 for game execution: 51 | ; If CNROM, and CHR loader supports CNROM, use $00 (CHR bank). 52 | ; If game is 32K, use $81 (outer PRG bank) so that simple 53 | ; reset code works. 54 | ; Otherwise, use $01 (inner PRG bank). 55 | ldy #0 56 | lda start_mappercfg 57 | bpl have_exe_mode_in_y 58 | ; Game size 64K+: AOROM/BNROM/UNROM; use reg $01 (PRG inner bank) 59 | iny 60 | and #$30 61 | bne have_exe_mode_in_y 62 | ; Game size 32K: NROM (use outer bank) 63 | ldy #$81 64 | have_exe_mode_in_y: 65 | tya 66 | pha 67 | 68 | ; Outer bank register 69 | ldy #TITLE_PRG_BANK 70 | lda (start_bankptr),y 71 | pha 72 | 73 | ; Now the outer bank and reset vector are correct, and the mapper 74 | ; configuration has been saved in a variable. Now interpret the 75 | ; mapper configuration in reg $80 format: 76 | ; 76543210 77 | ; | |||||| 78 | ; | ||||++- Nametable mirroring (0=AAAA, 2=ABAB, 3=AABB) 79 | ; | ||++--- PRG bank mode (0=32k, 2=fixed $8000, 3=fixed $C000) 80 | ; | ++----- Game size (0=32k, 1=64k, 2=128k, 3=256k) 81 | ; +-------- If set, game isn't CNROM 82 | lda start_mappercfg 83 | pha 84 | 85 | ; Starting inner PRG bank should be $0F except for mapper 180 86 | ; where it should be $00. Bits 3-2 of mapper mode control this. 87 | ;,;lda start_mappercfg 88 | and #$0C 89 | eor #$08 ; 0: mapper 180; nonzero: mapper 0, 2, 3, 7, 34 90 | bne :+ 91 | lda #$0F 92 | : 93 | pha 94 | 95 | ; Before commiting to mapper writes, Clear RAM and Nametables 96 | ; while avoiding the trampoline area in stack page. 97 | ; TODO: Code golf this to take less ROM bytes. 98 | lda #$20 99 | ldx #$00 100 | sta PPUADDR 101 | stx PPUADDR 102 | txa 103 | clear_memory_loop: 104 | sta $00,x 105 | ; leave stack page for the ram code to clear. 106 | sta $0200,x 107 | sta $0300,x 108 | sta $0400,x 109 | sta $0500,x 110 | sta $0600,x 111 | sta $0700,x 112 | ldy #16 113 | clear_part_nt_loop: 114 | sta PPUDATA 115 | dey 116 | bne clear_part_nt_loop 117 | inx 118 | bne clear_memory_loop 119 | 120 | ; Start with CHR bank 0 121 | ; ldx #$00 ; set by previous inx/bne pair 122 | stx $5000 123 | stx $8000 124 | 125 | ; Prepare trampoline to set starting inner PRG bank (reg $01) 126 | ; and pop game mode and outer bank values (regs $80 and $81) 127 | inx 128 | stx $5000 129 | pla 130 | ldx #$80 131 | jmp trampoline_enter 132 | 133 | ; Fortunately this part is all position-independent code 134 | ; so the link script isn't polluted more. 135 | trampoline_code_begin: 136 | sta $8000 137 | loop: 138 | stx $5000 139 | pla 140 | sta $8000 141 | inx 142 | cpx #$82 143 | bcc loop 144 | pla 145 | sta $5000 146 | ldx #$ff-((trampoline_code_end + 2) - clr_sp_loop) 147 | txs 148 | lda #$00 149 | inx 150 | clr_sp_loop: 151 | dex 152 | pha 153 | bne clr_sp_loop 154 | ; This should also leave the stack pointer at the bottom. 155 | .byte $4C ; JMP opcode immediately before start_entrypoint 156 | trampoline_code_end: 157 | 158 | trampoline_code_size = trampoline_code_end - trampoline_code_begin 159 | trampoline_enter = $0200 - ((trampoline_code_end+2) - trampoline_code_begin) 160 | .endproc 161 | -------------------------------------------------------------------------------- /src/pads.s: -------------------------------------------------------------------------------- 1 | ; 2 | ; NES controller and Zapper trigger reading code 3 | ; Copyright 2009-2012 Damian Yerrick 4 | ; 5 | ; Copying and distribution of this file, with or without 6 | ; modification, are permitted in any medium without royalty provided 7 | ; the copyright notice and this notice are preserved in all source 8 | ; code copies. This file is offered as-is, without any warranty. 9 | ; 10 | 11 | ; 2012-02: Compile-time option for DPCM_UNSAFE controller reading 12 | ; in programs also using the mouse 13 | ; 2011-09: map Zapper trigger to A Button 14 | ; 2011-07: Damian Yerrick added labels for the local variables and 15 | ; copious comments and made USE_DAS a compile-time option 16 | ; 17 | 18 | .export read_pads 19 | .importzp cur_keys, new_keys 20 | 21 | JOY1 = $4016 22 | JOY2 = $4017 23 | KEY_A = $80 24 | 25 | ; turn USE_DAS on to enable autorepeat support 26 | .ifndef USE_DAS 27 | USE_DAS = 0 28 | .endif 29 | 30 | ; turn ZAPPER_TO_A on to enable pressing A with the light gun 31 | ; NOT compatible with mouse support 32 | .ifndef ZAPPER_TO_A_BUTTON 33 | ZAPPER_TO_A_BUTTON = 0 34 | .endif 35 | 36 | ; if using mouse, use DPCM_UNSAFE controller reading 37 | .ifndef DPCM_UNSAFE 38 | DPCM_UNSAFE = 0 39 | .endif 40 | DPCM_SAFE = !DPCM_UNSAFE 41 | 42 | ; time until autorepeat starts making keypresses 43 | DAS_DELAY = 15 44 | ; time between autorepeat keypresses 45 | DAS_SPEED = 3 46 | 47 | .segment "CODE" 48 | .proc read_pads 49 | thisRead = 0 50 | firstRead = 2 51 | lastFrameKeys = 4 52 | 53 | ; store the current keypress state to detect key-down later 54 | lda cur_keys 55 | sta lastFrameKeys 56 | lda cur_keys+1 57 | sta lastFrameKeys+1 58 | 59 | ; Read the joypads twice in case DMC DMA caused a clock glitch. 60 | jsr read_pads_once 61 | .if ::DPCM_SAFE 62 | lda thisRead 63 | sta firstRead 64 | lda thisRead+1 65 | sta firstRead+1 66 | jsr read_pads_once 67 | .endif 68 | 69 | ; For each player, make sure the reads agree, then find newly 70 | ; pressed keys. 71 | ldx #1 72 | @fixupKeys: 73 | 74 | ; If the player's keys read out the same way both times, update. 75 | ; Otherwise, keep the last frame's keypresses. 76 | lda thisRead,x 77 | .if ::DPCM_SAFE 78 | cmp firstRead,x 79 | bne @dontUpdateGlitch 80 | .endif 81 | sta cur_keys,x 82 | @dontUpdateGlitch: 83 | 84 | .if ::ZAPPER_TO_A_BUTTON 85 | lda $4016,x 86 | and #$10 87 | beq @notZapperTrigger 88 | lda #KEY_A 89 | ora cur_keys,x 90 | sta cur_keys,x 91 | @notZapperTrigger: 92 | .endif 93 | 94 | lda lastFrameKeys,x ; A = keys that were down last frame 95 | eor #$FF ; A = keys that were up last frame 96 | and cur_keys,x ; A = keys down now and up last frame 97 | sta new_keys,x 98 | dex 99 | bpl @fixupKeys 100 | rts 101 | 102 | read_pads_once: 103 | 104 | ; Bits from the controllers are shifted into thisRead and 105 | ; thisRead+1. In addition, thisRead+1 serves as the loop counter: 106 | ; once the $01 gets shifted left eight times, the 1 bit will 107 | ; end up in carry, terminating the loop. 108 | lda #$01 109 | sta thisRead+1 110 | ; Write 1 then 0 to JOY1 to send a latch signal, telling the 111 | ; controllers to copy button states into a shift register 112 | sta JOY1 113 | lsr a 114 | sta JOY1 115 | loop: 116 | ; On NES and AV Famicom, button presses always show up in D0. 117 | ; On the original Famicom, presses on the hardwired controllers 118 | ; show up in D0 and presses on plug-in controllers show up in D1. 119 | ; D2-D7 consist of data from the Zapper, Power Pad, Vs. System 120 | ; DIP switches, and bus capacitance; ignore them. 121 | lda JOY1 ; read player 1's controller 122 | and #%00000011 ; ignore D2-D7 123 | cmp #1 ; CLC if A=0, SEC if A>=1 124 | rol thisRead ; put one bit in the register 125 | lda JOY2 ; read player 2's controller the same way 126 | and #$03 127 | cmp #1 128 | rol thisRead+1 129 | bcc loop ; once $01 has been shifted 8 times, we're done 130 | rts 131 | .endproc 132 | 133 | 134 | ; Optional autorepeat handling 135 | 136 | .if USE_DAS 137 | .export autorepeat 138 | .importzp das_keys, das_timer 139 | 140 | ;; 141 | ; Computes autorepeat (delayed-auto-shift) on the gamepad for one 142 | ; player, ORing result into the player's new_keys. 143 | ; @param X which player to calculate autorepeat for 144 | .proc autorepeat 145 | ; If no keys are held, skip all autorepeat processing 146 | lda cur_keys,x 147 | beq no_das 148 | lda new_keys,x 149 | beq no_restart_das 150 | 151 | ; If any keys were pressed, set them as the autorepeating set 152 | sta das_keys,x 153 | lda #DAS_DELAY 154 | sta das_timer,x 155 | bne no_das 156 | no_restart_das: 157 | 158 | ; If time has expired, merge in the autorepeating set 159 | dec das_timer,x 160 | bne no_das 161 | lda das_keys,x 162 | and cur_keys,x 163 | ora new_keys,x 164 | sta new_keys,x 165 | lda #DAS_SPEED 166 | sta das_timer,x 167 | no_das: 168 | rts 169 | .endproc 170 | 171 | .endif 172 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # 3 | # Makefile for Action 53 multicart engine 4 | # Copyright 2012-2018 Damian Yerrick 5 | # 6 | # Copying and distribution of this file, with or without 7 | # modification, are permitted in any medium without royalty 8 | # provided the copyright notice and this notice are preserved. 9 | # This file is offered as-is, without any warranty. 10 | # 11 | 12 | # These are used in the title of the NES program and the zip file. 13 | title := a53menu 14 | version := 0.06wip2 15 | cfgtitle := demo 16 | othercfgs := # demo 17 | cfgversion := page1 18 | 19 | # Space-separated list of assembly language files that make up the 20 | # PRG ROM. If it gets too long for one line, you can add a backslash 21 | # (the \ character) at the end of the line and continue on the next. 22 | objlist := \ 23 | vwf7 wait_loops vwf_draw quadpcm bcd paldetect zapkernels \ 24 | a53mapper main title cartmenu coredump donut \ 25 | interbank_fetch pads mouse ppuclear identify undte \ 26 | pentlysound pentlymusic checksums 27 | 28 | AS65 = ca65 29 | LD65 = ld65 30 | CFLAGS65 := -DDPCM_UNSAFE=1 31 | objdir = obj/nes 32 | srcdir = src 33 | imgdir = tilesets 34 | 35 | # Needs FCEUX 2.2.2 or later, or preferably SVN version 36 | EMU := fceux 37 | DEBUGEMU := ~/.wine/drive_c/Program\ Files\ \(x86\)/FCEUX/fceux.exe 38 | 39 | # Flags for native tools written in C 40 | CC := gcc 41 | CFLAGS := -std=gnu99 -Wall -Wextra -DNDEBUG -Os 42 | 43 | # Windows needs .exe suffixed to the names of executables; UNIX does 44 | # not. COMSPEC will be set to the name of the shell on Windows and 45 | # not defined on UNIX. Also the Windows Python installer puts 46 | # py.exe in the path, but not python3.exe, which confuses MSYS Make. 47 | ifdef COMSPEC 48 | DOTEXE:=.exe 49 | PY:=py 50 | else 51 | DOTEXE:= 52 | PY:= 53 | endif 54 | 55 | .PHONY: run all debug dist zip 7z clean 56 | 57 | run: $(cfgtitle).nes 58 | $(EMU) $< 59 | debug: $(cfgtitle).nes 60 | $(DEBUGEMU) $< 61 | 62 | tools/donut$(DOTEXE): tools/donut.c 63 | $(CC) -static $(CFLAGS) -o $@ $^ 64 | 65 | tools/dte$(DOTEXE): tools/dte.c 66 | $(CC) -static $(CFLAGS) -o $@ $^ 67 | 68 | tools/dtefe.py: tools/dte$(DOTEXE) 69 | 70 | %.nes: collections/%/a53.cfg $(title).prg tools/a53build.py \ 71 | tools/ines.py tools/innie.py tools/a53charset.py tools/a53screenshot.py \ 72 | tools/dtefe.py tools/donut$(DOTEXE) 73 | $(PY) tools/a53build.py $< $@ 74 | 75 | # Rule to create or update the distribution zipfile by adding all 76 | # files listed in zip.in. Actually the zipfile depends on every 77 | # single file in zip.in, but currently we use changes to the compiled 78 | # program, makefile, and README as a heuristic for when something was 79 | # changed. It won't see changes to docs or tools, but usually when 80 | # docs changes, README also changes, and when tools changes, the 81 | # makefile changes. 82 | dist: zip 7z 83 | 84 | zip: $(title)-$(version).zip 85 | $(title)-$(version).zip: zip.in all README.md CHANGES.txt $(objdir)/index.txt 86 | tools/zip12to0.sh -9u $@ -@ < $< 87 | 88 | 7z: $(cfgtitle)-$(cfgversion).7z 89 | $(cfgtitle)-$(cfgversion).7z: $(cfgtitle).nes $(foreach o,$(othercfgs),$(o).nes) 90 | 7za a $@ $^ 91 | 92 | all: $(title).prg 93 | 94 | clean: 95 | -rm $(objdir)/*.o $(objdir)/*.sav $(objdir)/*.s $(objdir)/*.chr $(objdir)/*.nam $(objdir)/*.pb53 $(objdir)/*.donut $(objdir)/*.qdp tools/donut$(DOTEXE) tools/dte$(DOTEXE) 96 | 97 | $(objdir)/index.txt: makefile 98 | echo Files produced by build tools go here, but caulk goes where? > $@ 99 | 100 | # Rules for PRG ROM 101 | 102 | objlistntsc := $(foreach o,$(objlist),$(objdir)/$(o).o) 103 | 104 | map.txt $(title).prg: lastbank.x $(objlistntsc) $(objdir)/musicseq.o 105 | $(LD65) -o $(title).prg -m map.txt -C $^ 106 | 107 | $(objdir)/%.o: $(srcdir)/%.s $(srcdir)/nes.inc $(srcdir)/global.inc 108 | $(AS65) $(CFLAGS65) $< -o $@ 109 | 110 | $(objdir)/%.o: $(objdir)/%.s 111 | $(AS65) $(CFLAGS65) $< -o $@ 112 | 113 | # Files depending on extra headers 114 | $(objdir)/main.o $(objdir)/cartmenu.o: $(srcdir)/pentlyconfig.inc 115 | $(objdir)/pentlysound.o $(objdir)/pentlymusic.o: \ 116 | $(srcdir)/pentlyconfig.inc $(objdir)/pentlybss.inc 117 | 118 | # Files that depend on .incbin'd files 119 | $(objdir)/cartmenu.o: $(objdir)/select_tiles.chr.donut 120 | $(objdir)/quadpcm.o: $(objdir)/selnow.qdp 121 | $(objdir)/selnow.qdp: tools/quadanalyze.py audio/selnow.wav 122 | $(PY) $^ $@ 123 | 124 | # Rules for CHR data 125 | 126 | $(objdir)/%.pb53: $(objdir)/% 127 | $(PY) tools/pb53.py --raw $< $@ 128 | 129 | $(objdir)/%.donut: $(objdir)/% tools/donut$(DOTEXE) 130 | tools/donut$(DOTEXE) -fq $< $@ 131 | 132 | $(objdir)/%.chr: $(imgdir)/%.png 133 | $(PY) tools/pilbmp2nes.py $< $@ 134 | 135 | $(objdir)/%16.chr: $(imgdir)/%.png 136 | $(PY) tools/pilbmp2nes.py -H 16 $< $@ 137 | 138 | $(objdir)/%.s: tools/vwfbuild.py tilesets/%.png 139 | $(PY) $^ $@ 140 | 141 | # Build RAM map for pently 142 | $(objdir)/pentlybss.inc: tools/pentlybss.py $(srcdir)/pentlyconfig.inc 143 | $(PY) $^ pentlymusicbase -o $@ 144 | 145 | # Translate music project 146 | $(objdir)/%.s: tools/pentlyas.py src/%.pently 147 | $(PY) $^ -o $@ --periods 76 148 | $(objdir)/%-rmarks.s: tools/pentlyas.py src/%.pently 149 | $(PY) $^ -o $@ --periods 76 --rehearse 150 | -------------------------------------------------------------------------------- /src/identify.s: -------------------------------------------------------------------------------- 1 | ; 2 | ; Identify controllers based on which lines are 0, 1, or serial 3 | ; 4 | ; Copyright 2016 Damian Yerrick 5 | ; 6 | ; This software is provided 'as-is', without any express or implied 7 | ; warranty. In no event will the authors be held liable for any damages 8 | ; arising from the use of this software. 9 | ; 10 | ; Permission is granted to anyone to use this software for any purpose, 11 | ; including commercial applications, and to alter it and redistribute it 12 | ; freely, subject to the following restrictions: 13 | ; 14 | ; 1. The origin of this software must not be misrepresented; you must not 15 | ; claim that you wrote the original software. If you use this software 16 | ; in a product, an acknowledgment in the product documentation would be 17 | ; appreciated but is not required. 18 | ; 2. Altered source versions must be plainly marked as such, and must not be 19 | ; misrepresented as being the original software. 20 | ; 3. This notice may not be removed or altered from any source distribution. 21 | ; 22 | .include "nes.inc" 23 | .include "global.inc" 24 | .import wait36k 25 | 26 | .segment "ZEROPAGE" 27 | detected_pads: .res 1 28 | min4016: .res 1 ; Bitwise minimum of $4016 values over 32 reads 29 | max4016: .res 1 ; Bitwise maximum of $4016 values over 32 reads 30 | min4017: .res 1 ; Bitwise minimum of $4017 values over 32 reads 31 | max4017: .res 1 ; Bitwise maximum of $4017 values over 32 reads 32 | 33 | .segment "RODATA" 34 | one_shl_x: .byte $01, $02, $04, $08, $10, $20, $40, $80 35 | 36 | .segment "CODE" 37 | 38 | .proc identify_controllers 39 | reads9to16 = $02 40 | reads17to24 = $03 41 | 42 | ; Identify which lines are constant 0, constant 1, or serial. 43 | ; Do this on a blank screen so that the Zapper can be detected 44 | ; as trigger off (D4=0) and dark (D3=1). 45 | ldx #1 46 | stx $4016 47 | ldy #32 ; allow up to 32 bits of serial 48 | dex 49 | stx max4016 50 | stx max4017 51 | stx detected_pads 52 | stx $4016 53 | dex 54 | stx min4016 55 | stx min4017 56 | 57 | loop401x: ; 53 cycles per iteration 58 | lda $4016 59 | tax 60 | and min4016 61 | sta min4016 62 | txa 63 | ora max4016 64 | sta max4016 65 | lda $4017 66 | tax 67 | and min4017 68 | sta min4017 69 | txa 70 | ora max4017 71 | sta max4017 72 | dey 73 | bne loop401x 74 | 75 | ; Look for a Zapper in port 2 76 | lda min4017 77 | eor max4017 ; A = which bits of port 2 are serial 78 | and #$18 ; The NES Zapper isn't serial. 79 | bne not_zapper 80 | lda max4017 81 | and #$18 82 | cmp #$08 83 | bne not_zapper 84 | ora detected_pads 85 | sta detected_pads 86 | not_zapper: 87 | 88 | ; Look for a Super NES Mouse in port 1 89 | lda min4016 90 | eor max4016 ; A = which bits of port 2 are serial 91 | and #$01 92 | beq not_snes_mouse ; D0 must be serial 93 | 94 | ; Wait and reread signature bits (13 to 16 should be 0001 for a mouse) 95 | jsr wait36k 96 | lda #1 97 | sta $4016 98 | sta reads9to16 99 | lsr a 100 | sta $4016 101 | 102 | ; Ignore first 8 reads from port 1 103 | ldy #8 104 | loop1to8: 105 | lda $4016 106 | dey 107 | bne loop1to8 108 | 109 | ; Save next 8 reads from port 1 bit 0 110 | loop9to16: 111 | lda $4016 112 | lsr a 113 | rol reads9to16 114 | bcc loop9to16 115 | 116 | ; 9-16 and $0F = $01 and responds to speed changes: Super NES Mouse 117 | lda reads9to16 118 | and #$0F 119 | cmp #$01 120 | bne not_snes_mouse 121 | ; lda #$01 ; Mouse signature coincidentally matches desired bitmask 122 | ldx #0 ; Port 1 123 | jsr ident_mouse 124 | ora detected_pads 125 | sta detected_pads 126 | not_snes_mouse: 127 | rts 128 | .endproc 129 | 130 | ;; 131 | ; Ensures the Super NES Mouse's sensitivity (report bits 11 and 12) 132 | ; can be set to 1 then 0. 133 | ; @param X port ID (0: $4016; 1: $4017) 134 | ; @param A bit mask ($01: D0; $02: D1) 135 | ; @return A = DETECT_1P_MOUSE for mouse or 0 for no mouse 136 | .proc ident_mouse 137 | portid = $00 138 | bitmask = $01 139 | targetspeed = $04 140 | triesleft = $05 141 | stx portid 142 | sta bitmask 143 | lda #1 144 | sta targetspeed 145 | 146 | targetloop: 147 | lda #4 148 | sta triesleft 149 | 150 | tryloop: 151 | ; To change the speed, send a clock while strobe is on, 152 | ldy #1 153 | sty $4016 154 | lda $4016,x 155 | dey 156 | sty $4016 157 | 158 | ; Wait and strobe the mouse normally, then skip bits 1-10 159 | jsr wait36k 160 | ldx portid 161 | ldy #1 162 | sty $4016 163 | dey 164 | sty $4016 165 | ldy #10 166 | skip10loop: 167 | lda $4016,x 168 | dey 169 | bne skip10loop 170 | 171 | ; Now read bits 11 and 12 172 | ldy #0 173 | lda $4016,x 174 | and bitmask 175 | beq :+ 176 | ldy #2 177 | : 178 | lda $4016,x 179 | and bitmask 180 | beq :+ 181 | iny 182 | : 183 | cpy targetspeed 184 | beq try_success 185 | dec triesleft 186 | bne tryloop 187 | lda #0 188 | rts 189 | 190 | try_success: 191 | dec targetspeed 192 | bpl targetloop 193 | 194 | ; Setting to both 0 and 1 was successful. 195 | lda #DETECT_1P_MOUSE 196 | rts 197 | .endproc 198 | -------------------------------------------------------------------------------- /tools/soxwave.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | def _open(filename, mode='r'): 4 | return (Wave_write(filename) 5 | if mode.startswith('w') 6 | else Wave_read(filename)) 7 | 8 | class SoxError(IOError): 9 | pass 10 | 11 | def sox_spawn(argv, data=None, **popenkwargs): 12 | import subprocess 13 | 14 | stdin_file = subprocess.PIPE if data else None 15 | 16 | child = subprocess.Popen(argv, 17 | stdin=stdin_file, 18 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 19 | **popenkwargs) 20 | (out, err) = child.communicate(data if data else None) 21 | return (out, err, child.returncode) 22 | 23 | def soxi_measure(filename): 24 | """Measure a wave and return a tuple (channels, rate, samples).""" 25 | soxiout = [] 26 | for measure in ['-c', '-r', '-s']: 27 | argv = ['sox', '--info', measure, filename] 28 | (stdout, stderr, result) = sox_spawn(argv) 29 | if result != 0: 30 | raise SoxError(stderr.strip()) 31 | soxiout.append(int(stdout)) 32 | return soxiout 33 | 34 | class Wave_read(object): 35 | 36 | def __init__(self, filename): 37 | self.crs = soxi_measure(filename) 38 | argv = ['sox', filename, 39 | '-t', 's16', '-L', '-'] 40 | 41 | (stdout, stderr, result) = sox_spawn(argv, '') 42 | stderr = stderr.strip() 43 | if result > 0: 44 | raise SoxError(stderr) 45 | if stderr: 46 | print(stderr, file=sys.stderr) 47 | self.data = stdout 48 | self.pos = 0 49 | 50 | def close(self): 51 | "Dispose of the stream and make it unusable." 52 | self.crs = None 53 | self.data = None 54 | self.pos = None 55 | 56 | def getnchannels(self): 57 | "Return the number of channels in the wave." 58 | return self.crs[0] 59 | 60 | def getsampwidth(self): 61 | "Return sample width in bytes. This is always 2 due to SoX conversion." 62 | return 2 63 | 64 | def getframerate(self): 65 | "Return the number of frames in each second." 66 | return self.crs[1] 67 | 68 | def getnframes(self): 69 | "Return the number of frames in each file." 70 | return self.crs[2] 71 | 72 | __len__ = getnframes 73 | 74 | def getcomptype(self): 75 | "Return a code for the compression type." 76 | return 'NONE' 77 | 78 | def getcompname(self): 79 | "Return a localized name for the compression type." 80 | return 'not compressed' 81 | 82 | def getparams(self): 83 | "Return a tuple (nchannels, sampwidth, framerate, nframes, comptype, compname)." 84 | return (self.getnchannels(), self.getsampwidth(), self.framerate(), 85 | self.getnframes(), self.getcomptype(), self.getcompname()) 86 | 87 | def readframes(self, n=None): 88 | "Read a string of bytes making up to n frames in little-endian format." 89 | n = (n * self.crs[0] * 2 if n is not None else None) 90 | pos = self.pos 91 | remain = len(self.data) - pos 92 | if n is None or n > remain: 93 | n = remain 94 | self.pos += n 95 | return self.data[pos:pos + n] 96 | 97 | def rewind(self): 98 | "Seek to the start of the wave." 99 | self.pos = 0 100 | 101 | def tell(self): 102 | "Save a read position in an implementation-defined format." 103 | return self.pos 104 | 105 | def setpos(self, pos): 106 | "Seek to a read position returned by tell()." 107 | self.pos = pos 108 | 109 | def getmarkers(self): 110 | "Return None, for compatibility with import aifc." 111 | return None 112 | 113 | def getmark(self, index): 114 | "Raise an error, for compatibility with import aifc." 115 | raise NotImplementedError 116 | 117 | def open(filename, mode=None): 118 | """If mode is 'r', open an audio file for reading. 119 | 120 | filename -- a file path, not a file-like object 121 | 122 | Return a class instance with methods similar to those of the instance 123 | returned by wave.open(). 124 | """ 125 | if not isinstance(mode, str): 126 | raise TypeError("mode must be a string") 127 | if not mode.startswith('r'): 128 | raise ValueError("unsupported mode %s (try 'r')" % repr(mode)) 129 | return Wave_read(filename) 130 | 131 | def get_sox_path(): 132 | """Search folders on the PATH for the "sox" program. 133 | 134 | Return the path to "sox" (or "sox.exe" on Windows) or None if not found. 135 | 136 | Per https://stackoverflow.com/a/377028/2738262 137 | """ 138 | import sys 139 | import os 140 | program = "sox.exe" if sys.platform == "win32" else "sox" 141 | 142 | for path in os.environ["PATH"].split(os.pathsep): 143 | exe_file = os.path.join(path, program) 144 | if os.path.isfile(exe_file) and os.access(exe_file, os.X_OK): 145 | return exe_file 146 | 147 | def _main(): 148 | from binascii import b2a_hex 149 | w = _open('fine punch.wav', 'r') 150 | rate = w.getframerate() 151 | print(len(w), 'samples or', 1000*len(w)//rate, 'ms') 152 | r = w.readframes(1000) 153 | print(len(r), 'bytes read') 154 | for i in xrange(0, len(r), 32): 155 | print(b2a_hex(r[i:i + 32])) 156 | 157 | if __name__=='__main__': 158 | _main() 159 | else: 160 | open = _open 161 | -------------------------------------------------------------------------------- /tools/ines.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | iNES executable loader supporting some NES 2.0 features 4 | 5 | Copyright 2012 Damian Yerrick 6 | 7 | Copying and distribution of this file, with or without modification, 8 | are permitted in any medium without royalty provided the copyright 9 | notice and this notice are preserved. This file is offered as-is, 10 | without any warranty. 11 | """ 12 | 13 | from __future__ import with_statement 14 | 15 | def load_ines(filename): 16 | """Load an NES executable in iNES format. 17 | 18 | DiskDude! and other corruptions are automatically recognized and 19 | disregarded. 20 | 21 | Return a dictionary with these keys: 22 | 'prg': PRG ROM data 23 | 'chr' (optional): CHR ROM data 24 | 'prgram' (optional): PRG RAM size (not battery backed) 25 | 'prgsave' (optional): PRG RAM size (battery backed) 26 | 'chrram' (optional): CHR RAM size (not battery backed) 27 | 'chrsave' (optional): CHR RAM size (battery backed) 28 | 'trainer' (optional): 512 bytes to load into PRG RAM $7000-$71FF 29 | 'NES 2.0' (optional): If present, the executable uses Kevin Horton's 30 | format extension. 31 | 'mapper': type of board, largely characterized by bank switching 32 | hardware (1=s*rom, 2=u*rom, 3=cnrom, 4=most t*rom, etc.) 33 | 'submapper' (optional): NES 2.0 mapper variant 34 | 'mirrType': default nametable mirroring (AAAA=1 screen, 35 | AABB=horizontal, ABAB=vertical, ABCD=4 screen) 36 | """ 37 | out = {} 38 | with open(filename, 'rb') as infp: 39 | header = bytearray(infp.read(16)) 40 | if not header.startswith(b"NES\x1a"): 41 | raise ValueError(filename+" is not an iNES ROM") 42 | 43 | # Trainer: data preloaded into PRG RAM $7000-$71FF 44 | # Usually only mapper hacks for Front copiers use one. 45 | trainer = header[6] & 0x04 46 | if trainer: 47 | out['trainer'] = infp.read(512) 48 | 49 | nes2 = (header[7] & 0x0C) == 0x08 50 | if nes2: 51 | out['NES 2.0'] = True 52 | elif header[12] or header[13] or header[14] or header[15]: 53 | # probably a DiskDude! type header 54 | header[7:] = [0 for i in range(9)] 55 | prgSize = header[4] 56 | chrSize = header[5] 57 | if nes2: 58 | prgSize |= (header[8] & 0x0F) << 8 59 | chrSize |= (header[9] & 0xF0) << 4 60 | if not prgSize: 61 | raise ValueError("rom has no PRG memory") 62 | 63 | out['prg'] = infp.read(prgSize * 16384) 64 | if chrSize > 0: 65 | out['chr'] = infp.read(chrSize * 8192) 66 | 67 | # And at this point we've loaded the entire ROM. All that's 68 | # left is to set up the board. 69 | mapperNumber = ((header[6] & 0xF0) >> 4 70 | | (header[7] & 0xF0)) 71 | if nes2: 72 | mapperNumber |= (header[8] & 0x0F) << 8 73 | out['submapper'] = (header[8] & 0xF0) >> 4 74 | out['mapper'] = mapperNumber 75 | 76 | # Save most commonly means battery-backed PRG RAM, but it 77 | # could also be a serial EEPROM. In a couple cases, it's 78 | # even battery-backed CHR RAM. 79 | save = header[6] & 0x04 80 | if nes2: 81 | prgramSize = header[10] & 0x0F 82 | prgramSize = (64 << prgramSize) if prgramSize else 0 83 | prgsaveSize = (header[10] >> 4) & 0x0F 84 | prgsaveSize = (64 << prgsaveSize) if prgsaveSize else 0 85 | chrramSize = header[11] & 0x0F 86 | chrramSize = (64 << chrramSize) if chrramSize else 0 87 | chrsaveSize = (header[11] >> 4) & 0x0F 88 | chrsaveSize = (64 << chrsaveSize) if chrsaveSize else 0 89 | if save and not (prgsaveSize or chrsaveSize): 90 | raise ValueError("save present but size not specified") 91 | if (prgsaveSize or chrsaveSize) and not save: 92 | raise ValueError("save size specified but not present") 93 | if not chrramSize and not chrSize: 94 | raise ValueError("rom has no CHR memory") 95 | else: 96 | # Guess the sizes from the mapper 97 | prgramSize = 8192 if save else 0 98 | if mapperNumber in (16, 86, 159): 99 | # 16, 159: Bandai FCG boards with serial EEPROM save 100 | # 86: Jaleco JF-13, with a sample player in $6000-$7FFF 101 | prgramSize = 0 # none of these support PRG RAM 102 | chrramSize = 8192 if chrSize else 0 103 | if mapperNumber == 13: 104 | # CPROM, the Videomation board 105 | chrramSize = 16384 106 | prgsaveSize = 0 if save else 8192 107 | chrsaveSize = 0 108 | if save: 109 | if mapperNumber == 16: 110 | prgsaveSize = 256 111 | elif mapperNumber == 159: 112 | prgsaveSize = 128 113 | elif mapperNumber == 186: # RacerMate Challenge 114 | prgsaveSize = 0 115 | chrramSize = 0 116 | chrsaveSize = 65536 117 | if ((prgsaveSize or chrsaveSize) 118 | and mapperNumber in (86,)): 119 | raise ValueError("mapper %d does not support save" % mapperNumber) 120 | out['prgram'] = prgramSize 121 | out['prgsave'] = prgsaveSize 122 | out['chrram'] = chrramSize 123 | out['chrsave'] = chrsaveSize 124 | 125 | # Mirroring: mapping between PPU A10-A11 and CIRAM A10 126 | # Some mappers ignore this 127 | mirrType = header[6] & 0x09 128 | if mapperNumber == 7: 129 | mirrType = 'AAAA' # 1-screen, always mapper controlled 130 | elif mirrType == 0: 131 | mirrType = 'AABB' # V arrangement == H mirroring 132 | elif mirrType == 1: 133 | mirrType = 'ABAB' # H arrangement == V mirroring 134 | else: 135 | mirrType = 'ABCD' # Four screens using extra VRAM on cart 136 | out['mirrtype'] = mirrType 137 | 138 | return out 139 | -------------------------------------------------------------------------------- /tools/pentlybss.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Pently RAM map generator 4 | 5 | Copyright 2017 Damian Yerrick 6 | 7 | [Insert zlib License here] 8 | """ 9 | import sys 10 | import re 11 | import argparse 12 | 13 | default_heighttypes = { 14 | 'SINGLETON': 1, 15 | 'PER_TRACK': 5, 16 | 'PER_CHANNEL': 4, 17 | 'PER_PITCHED_CHANNEL': 4 18 | } 19 | num_cols = 4 # spacing between channels' length counters 20 | 21 | # Pitched channels may be reduced to 3 rows in a later commit after 22 | # I can confirm that noise isn't accessing any of them 23 | 24 | specs = """ 25 | # Pitch 26 | chPitchHi PER_CHANNEL 27 | 28 | # Pitch effects 29 | arpPhase PER_CHANNEL ARPEGGIO|ATTACK_TRACK 30 | arpIntervalA PER_PITCHED_CHANNEL ARPEGGIO 31 | arpIntervalB PER_PITCHED_CHANNEL ARPEGGIO 32 | vibratoDepth PER_PITCHED_CHANNEL VIBRATO 33 | vibratoPhase PER_PITCHED_CHANNEL VIBRATO 34 | notePitch PER_PITCHED_CHANNEL PORTAMENTO 35 | chPitchLo PER_PITCHED_CHANNEL PORTAMENTO 36 | chPortamento PER_PITCHED_CHANNEL PORTAMENTO 37 | 38 | # Envelope 39 | attack_remainlen PER_CHANNEL ATTACK_PHASE 40 | attackPitch PER_CHANNEL ATTACK_TRACK 41 | noteEnvVol PER_CHANNEL 42 | noteLegato PER_CHANNEL 43 | channelVolume PER_CHANNEL CHANNEL_VOLUME 44 | 45 | # Pattern reading 46 | noteRowsLeft PER_TRACK 47 | graceTime PER_TRACK 48 | noteInstrument PER_TRACK 49 | musicPattern PER_TRACK 50 | patternTranspose PER_TRACK 51 | music_tempoLo SINGLETON 52 | music_tempoHi SINGLETON 53 | conductorWaitRows SINGLETON 54 | pently_rows_per_beat SINGLETON BPMMATH 55 | pently_row_beat_part SINGLETON BPMMATH 56 | pently_mute_track PER_TRACK VARMIX 57 | 58 | # Visualization and rehearsal 59 | pently_vis_dutyvol PER_CHANNEL VIS 60 | pently_vis_pitchlo PER_CHANNEL VIS 61 | pently_vis_pitchhi PER_CHANNEL VIS 62 | pently_rowshi PER_CHANNEL REHEARSAL 63 | pently_rowslo PER_CHANNEL REHEARSAL 64 | pently_tempo_scale SINGLETON REHEARSAL 65 | 66 | """ 67 | specs = [row.strip() for row in specs.split("\n")] 68 | specs = [row.split() for row in specs if row and not row.startswith('#')] 69 | 70 | def load_uses(config_path): 71 | """Read the set of features that Pently is configured to use.""" 72 | useRE = re.compile(r"PENTLY_USE_([a-zA-Z0-9_]+)\s*=\s*([0-9])+\s*(?:;.*)?") 73 | with open(config_path, "r") as infp: 74 | uses = [useRE.match(line.strip()) for line in infp] 75 | uses = [m.groups() for m in uses if m] 76 | return {name for name, value in uses if int(value)} 77 | 78 | def get_heighttypes(uses): 79 | hts = dict(default_heighttypes) 80 | if 'ATTACK_TRACK' not in uses: 81 | hts['PER_TRACK'] = hts['PER_CHANNEL'] 82 | return hts 83 | 84 | def get_needed_vars(uses): 85 | heighttypes = get_heighttypes(uses) 86 | needed_vars, unneeded_vars = [], [] 87 | for row in specs: 88 | varname, heighttype = row[:2] 89 | height = heighttypes[heighttype] 90 | conditions = row[2].split("|") if len(row) > 2 else None 91 | met = conditions is None or bool(uses.intersection(conditions)) 92 | if met: 93 | needed_vars.append((varname, height)) 94 | else: 95 | unneeded_vars.append((varname, row[1], height, conditions)) 96 | return needed_vars, unneeded_vars 97 | 98 | def format_unneeded(unneeded_vars): 99 | return [ 100 | "; %s (%s, %d %s): %s disabled" 101 | % (varname, heighttype, height, "rows" if height != 1 else "row", 102 | ", ".join(conditions)) 103 | for varname, heighttype, height, conditions in unneeded_vars 104 | ] 105 | 106 | def ffd(needed, num_cols): 107 | def byel1(x): 108 | return x[1] 109 | 110 | needed = sorted(needed, key=byel1, reverse=True) 111 | cols = [[[], 0] for x in range(num_cols)] 112 | for name, height in needed: 113 | lowest = min(cols, key=byel1) 114 | lowest[0].append((name, lowest[1])) 115 | lowest[1] += height 116 | cols.sort(key=byel1, reverse=True) 117 | return cols 118 | 119 | def format_cols(cols, base_label): 120 | return [ 121 | "%s = %s + %d" % (name, base_label, ht * len(cols) + i) 122 | for i, (names, totalht) in enumerate(cols) 123 | for name, ht in names 124 | ] 125 | 126 | def parse_argv(argv): 127 | parser = argparse.ArgumentParser() 128 | parser.add_argument("configpath") 129 | parser.add_argument("base_label") 130 | parser.add_argument("-o", "--output", default='-', 131 | help="write output to file") 132 | return parser.parse_args(argv[1:]) 133 | 134 | def main(argv=None): 135 | args = parse_argv(argv or sys.argv) 136 | 137 | uses = load_uses(args.configpath) 138 | needed_vars, unneeded_vars = get_needed_vars(uses) 139 | out = [] 140 | if unneeded_vars: 141 | out.append("; Variables not needed per configuration") 142 | out.extend(format_unneeded(unneeded_vars)) 143 | unneeded_vars = None 144 | 145 | cols = ffd(needed_vars, num_cols) 146 | minht = min(col[1] for col in cols) 147 | maxht = max(col[1] for col in cols) 148 | sumht = sum(col[1] for col in cols) 149 | belowmax = sum(1 for k, ht in cols if ht < maxht) 150 | bytesneeded = maxht * num_cols - belowmax 151 | 152 | waste = bytesneeded - sumht 153 | out.append("; Columns are %d-%d rows tall, total %d" 154 | % (minht, maxht, sumht)) 155 | out.append("; Below max: %d; layout waste %d" % (belowmax, waste)) 156 | out.append("%s_size = %d" % (args.base_label, bytesneeded)) 157 | out.extend(format_cols(cols, args.base_label)) 158 | outfp = open(args.output, "w") if args.output != '-' else sys.stdout 159 | with outfp: 160 | print("\n".join(out), file=outfp) 161 | 162 | 163 | if __name__=='__main__': 164 | main() 165 | -------------------------------------------------------------------------------- /tools/prgunused.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Tool to find unused space in PRG ROM 4 | 5 | Copyright 2017 Damian Yerrick 6 | 7 | Copying and distribution of this file, with or without modification, 8 | are permitted in any medium without royalty provided the copyright 9 | notice and this notice are preserved. This file is offered as-is, 10 | without any warranty. 11 | """ 12 | assert str is not bytes 13 | import sys 14 | import argparse 15 | import ines 16 | 17 | # Partial list of mappers, including all licensed US releases 18 | NROM = 0 19 | MMC1 = 1 20 | UNROM = 2 21 | CNROM = 3 22 | MMC3 = 4 23 | AOROM = 7 24 | MMC2 = 9 25 | MMC4 = 10 26 | COLORDREAMS = 11 27 | NAMCO163 = 19 28 | VRC4AC = 21 29 | VRC2A = 22 30 | VRC4BD = 23 31 | VRC4E = 25 32 | VRC4WORLDHERO = 27 33 | UNROM512 = 30 34 | BNROM = 34 35 | GNROM = 66 36 | RAMBO1 = 64 37 | SUNSOFT3 = 67 38 | SUNSOFT4 = 68 39 | FME7 = 69 # Gimmick, Batman: ROTJ 40 | CAMERICA = 71 41 | VRC3 = 73 42 | VRC1 = 75 43 | NAMCOT3446 = 76 # Megami Tensei: Digital Devil Story 44 | HOLYDIVER = 78 45 | VRC7 = 85 46 | NAMCOTQUINTY = 88 # Quinty and 2 others 47 | NAMCOT3425 = 95 # Dragon Buster, MIMIC counterpart to TLSROM 48 | TLSROM = 118 49 | TQROM = 119 50 | NAMCOT3453 = 154 # Devil Man 51 | UNROM180 = 180 52 | CNROM185 = 185 # CNROM with only one valid CHR bank 53 | MIMIC1 = 206 # MMC3 subset (Namco 108/109/118/119) 54 | NAMCO340 = 210 55 | 56 | # Some mappers are intentionally omitted from bank size guessing 57 | # because of their variable PRG bank size. These include MMC5, 58 | # VRC6, and most multicarts. But apart from those, these should 59 | # cover all licensed North American releases. 60 | 61 | mappertoprgbanksize = { 62 | MMC1: 16, 63 | NROM: 32, CNROM: 32, CNROM185: 32, 64 | UNROM: 16, UNROM180: 16, UNROM512: 16, 65 | AOROM: 32, 66 | GNROM: 32, 67 | MMC2: 8, 68 | MMC3: 8, TQROM: 8, TLSROM: 8, 69 | MMC4: 16, 70 | SUNSOFT3: 16, 71 | SUNSOFT4: 16, 72 | FME7: 8, 73 | VRC1: 8, 74 | VRC2A: 8, VRC4AC: 8, VRC4BD: 8, VRC4E: 8, VRC4WORLDHERO: 8, 75 | VRC3: 16, 76 | VRC7: 8, 77 | HOLYDIVER: 16, 78 | MIMIC1: 8, NAMCOT3446: 8, NAMCOTQUINTY: 8, NAMCOT3453: 8, 79 | NAMCOT3425: 8, 80 | NAMCO163: 8, NAMCO340: 8, 81 | RAMBO1: 8, 82 | COLORDREAMS: 32, 83 | CAMERICA: 16, 84 | } 85 | 86 | fix8000_mappers = {UNROM180} 87 | 88 | def bankbase(bankindex, numbanks, prgbanksize, fix8000): 89 | """Calculate the base address of a PRG bank. 90 | 91 | bankindex -- the index of a bank (0=first) 92 | numbanks -- the total number of banks in the ROM 93 | prgbanksize -- the size in 1024 byte units of a bank, 94 | usually 8, 16, or 32 95 | fix8000 -- if false, treat the first window ($8000) as 96 | switchable; if true, treat the last window as switchable 97 | """ 98 | if prgbanksize > 32: 99 | return 0 # for future use with Super NES HiROM 100 | numwindows = 32 // prgbanksize 101 | if fix8000: 102 | wndindex = min(bankindex, numwindows - 1) 103 | else: 104 | wndindex = max(0, bankindex + numwindows - numbanks) 105 | return 0x8000 + 0x400 * prgbanksize * wndindex 106 | 107 | def find_runs(it): 108 | rstart, i, rval = 0, 0, 0 109 | for el in it: 110 | if rval != el: 111 | if i > rstart: yield rstart, i, rval 112 | rstart, rval = i, el 113 | i += 1 114 | if i > rstart: yield rstart, i, rval 115 | 116 | def run_is_big_enough(s, e, bankend): 117 | """Check whether a run is big enough to consider. 118 | 119 | A run of $FF or $00 is OK if it's 32 bytes or longer, or if it's 120 | 10 bytes or longer and touches the reset vectors. 121 | """ 122 | if e - s >= 32: 123 | return True 124 | if s <= bankend - 15 and e >= bankend - 6: 125 | return True 126 | return False 127 | 128 | def get_unused(prg, mapper=None, prgbanksize=None, fix8000=None): 129 | 130 | # Guess missing PRG bank size based on mapper 131 | if prgbanksize is None: 132 | try: 133 | prgbanksize = mappertoprgbanksize[mapper] 134 | except KeyError: 135 | raise ValueError("could not guess PRG bank size for mapper %d" 136 | % mapper) 137 | prgbanksize = min(prgbanksize, len(prg) // 1024) 138 | if fix8000 is None: 139 | fix8000 = mapper in fix8000_mappers 140 | 141 | # Find long enough runs of $00 and $FF in each bank 142 | prgbanksize_bytes = 1024 * prgbanksize 143 | runlists = [ 144 | [(s, e) 145 | for s, e, v in find_runs(prg[i:i + prgbanksize_bytes]) 146 | if (v in (0x00, 0xFF) 147 | and run_is_big_enough(s, e, prgbanksize_bytes - 6))] 148 | for i in range(0, len(prg), prgbanksize_bytes) 149 | ] 150 | return [ 151 | (i, bankbase(i, len(runlists), prgbanksize, fix8000), runlist) 152 | for i, runlist in enumerate(runlists) 153 | if runlist 154 | ] 155 | 156 | def parse_argv(argv): 157 | parser = argparse.ArgumentParser() 158 | parser.add_argument("romfile", nargs='+', 159 | help="path to .nes ROM image") 160 | parser.add_argument("--prg-bank-size", "--pbs", 161 | type=int, choices=[4, 8, 16, 32], 162 | help="size in KiB of each PRG bank, overriding mapper autodetection") 163 | parser.add_argument("--fix-8000", "--crazy", 164 | action="store_true", 165 | help="treat last window as switchable instead of first") 166 | parser.add_argument("--a53", 167 | action="store_true", 168 | help="output for Action 53 config file") 169 | return parser.parse_args(argv[1:]) 170 | 171 | def main(argv=None): 172 | args = parse_argv(argv or sys.argv) 173 | runformat = "prgunused%d=%s" if args.a53 else "%d: %s" 174 | banksize = 32 if args.a53 else args.prg_bank_size 175 | for filename in args.romfile: 176 | rom = ines.load_ines(filename) 177 | prg = rom['prg'] 178 | mapper = rom['mapper'] 179 | 180 | runlists = get_unused(prg, mapper, banksize, args.fix_8000) 181 | tunused = [ 182 | runformat 183 | % (i, ",".join( 184 | "%04x-%04x" % (base + start, base + end - 1) 185 | for start, end in runs 186 | )) 187 | for i, base, runs in runlists 188 | ] 189 | if len(args.romfile) > 1: 190 | print(filename) 191 | print("\n".join(tunused)) 192 | 193 | if __name__=='__main__': 194 | ## main("prgunused.py --a53 /home/pino/develop/240pee/240pee.nes".split()) 195 | main() 196 | -------------------------------------------------------------------------------- /src/checksums.s: -------------------------------------------------------------------------------- 1 | .include "nes.inc" 2 | .include "global.inc" 3 | 4 | .import coredump_load_gfx 5 | .import ppu_screen_on_scroll_0, ppu_clear_nt 6 | 7 | .export check_header, compute_cart_checksums 8 | 9 | .segment "CODE" 10 | .proc check_header 11 | ldy #4 12 | check_loop: 13 | clc 14 | lda DIRECTORY_HEADER-1, y 15 | adc header_check_bytes-1, y 16 | bne check_error ; rts with Z = 1 17 | dey 18 | bne check_loop 19 | ; rts with Z = 0 20 | check_error: 21 | rts 22 | header_check_bytes: 23 | .byte $100-165, $100-65, $100-53, $100-51 24 | .endproc 25 | 26 | .proc compute_cart_checksums 27 | crc_hi = $00 28 | crc_lo = $01 29 | read_ptr = $02 ; 2 bytes 30 | read_page_count = $04 31 | ; ram $00 ~ $04 must match parameters in ram code 32 | current_32k_bank = $05 33 | nt_ptr = $06 ; 2 bytes 34 | crc_check_ptr = $08 ; 2 bytes 35 | ram_code_jmp = $0a ; 2 bytes 36 | ldx #0 37 | stx PPUMASK 38 | 39 | ldx #$ff 40 | txs 41 | 42 | lda #$20 43 | ldx #$20 44 | ldy #$00 45 | jsr ppu_clear_nt 46 | 47 | ldx #$01 48 | stx ram_code_jmp+1 49 | 50 | ldx #ram_code_size 51 | copy_ram_code_loop: 52 | lda ram_code_begin-1, x 53 | pha 54 | dex 55 | bne copy_ram_code_loop 56 | tsx 57 | inx 58 | stx ram_code_jmp+0 59 | 60 | ldy #$00 61 | sty read_ptr+0 62 | lda #$80 63 | sta read_ptr+1 64 | 65 | ;,; lda #$80 66 | sta nt_ptr+0 67 | lda #$20 68 | sta nt_ptr+1 69 | 70 | lda BANK_CHECKSUMS+0 71 | sta crc_check_ptr+0 72 | lda BANK_CHECKSUMS+1 73 | sta crc_check_ptr+1 74 | 75 | lda NEG_NUMBER_OF_BANKS 76 | sta current_32k_bank 77 | 78 | jsr check_header 79 | bne db_checksum_fail 80 | 81 | ;,; ldy #$00 82 | ldx #$ff 83 | jsr compute_16K_block 84 | ;,; lda crc_hi 85 | ;,; ldx crc_lo 86 | ;,; ldy #$ff 87 | 88 | ;,; lda crc_hi 89 | ora crc_lo 90 | bne db_checksum_fail 91 | 92 | ;,; ldy #$00 93 | ;,; sty read_ptr+0 94 | lda #$80 95 | sta read_ptr+1 96 | 97 | next_checksum: 98 | ldx #string_at-strings_base 99 | jsr print_cr_and_str 100 | ;,; ldy #$00 101 | 102 | lda read_ptr+1 103 | cmp #$c0 104 | lda current_32k_bank 105 | rol 106 | tax 107 | lda #$00 108 | rol 109 | 110 | ;,; and #$0f 111 | sta PPUDATA 112 | jsr print_byte_x 113 | 114 | lda #0 115 | clc 116 | jsr ppu_screen_on_scroll_0 117 | 118 | ;,; ldy #$00 119 | ldx current_32k_bank 120 | jsr compute_16K_block 121 | ;,; ldx crc_lo 122 | ;,; lda crc_hi 123 | ;,; ldy #$ff 124 | 125 | iny ;,; ldy #$00 126 | ;,; lda crc_hi 127 | cmp (crc_check_ptr), y 128 | bne check_failed 129 | ;,; ldx crc_lo 130 | txa 131 | iny 132 | cmp (crc_check_ptr), y 133 | beq check_succeed 134 | 135 | check_failed: 136 | ldx #string_err-strings_base 137 | jsr print_cr_and_str 138 | 139 | ldx crc_hi 140 | jsr print_byte_x 141 | ldx crc_lo 142 | jsr print_byte_x 143 | advance_nt_ptr: 144 | clc 145 | lda nt_ptr+0 146 | adc #$20 147 | sta nt_ptr+0 148 | bcc no_carry 149 | inc nt_ptr+1 150 | no_carry: 151 | check_succeed: 152 | 153 | clc 154 | lda crc_check_ptr+0 155 | adc #$02 156 | sta crc_check_ptr+0 157 | bcc not_check_ptr_carry 158 | inc crc_check_ptr+1 159 | not_check_ptr_carry: 160 | 161 | lda read_ptr+1 162 | bne next_checksum 163 | lda #$80 164 | sta read_ptr+1 165 | inc current_32k_bank 166 | bne next_checksum 167 | done: 168 | ldx #string_done-strings_base 169 | .byte $2c ; BIT opcode to choose what value to load 170 | db_checksum_fail: 171 | ldx #string_dberr-strings_base 172 | jsr print_cr_and_str 173 | ;,; ldy #$00 174 | 175 | tya ;,; lda #0 176 | clc 177 | jsr ppu_screen_on_scroll_0 178 | 179 | lda #%11000001 180 | ldx #4 181 | end_sfx: 182 | dex 183 | sta $4000, x 184 | bne end_sfx 185 | jam: jmp jam 186 | 187 | print_cr_and_str: 188 | lda nt_ptr+1 189 | sta PPUADDR 190 | lda nt_ptr+0 191 | sta PPUADDR 192 | print_str: 193 | ldy strings_base, x 194 | inx 195 | tab_advance_loop: 196 | lda PPUDATA 197 | dey 198 | bne tab_advance_loop 199 | ldy strings_base, x 200 | inx 201 | print_str_loop: 202 | lda strings_base, x 203 | sta PPUDATA 204 | inx 205 | dey 206 | bne print_str_loop 207 | _byte_with_b6_set: 208 | rts 209 | strings_base: 210 | string_err: 211 | .byte 12, 4, $0E,$16,$16,$20 212 | string_at: 213 | .byte 4, 3, $0A,$22,$21 214 | string_done: 215 | .byte 6, 5, $20,$18,$15,$24,$17 216 | string_dberr: 217 | .byte 12, 6, $0D,$0B,$20,$0E,$16,$16 218 | 219 | print_byte_x: 220 | txa 221 | lsr 222 | lsr 223 | lsr 224 | lsr 225 | sta PPUDATA 226 | txa 227 | and #$0f 228 | sta PPUDATA 229 | rts 230 | 231 | compute_16K_block: 232 | ;,; ldy #$00 233 | sty crc_lo 234 | sty crc_hi 235 | lda #>$4000 236 | sta read_page_count 237 | jmp (ram_code_jmp) 238 | 239 | ram_code_begin: 240 | CRCHI = $00 ; Yes, CRC is big endian 241 | CRCLO = $01 242 | READ_PTR = $02 243 | READ_PAGE_COUNT = $04 244 | stx $8000 245 | check_16k_outer_loop: 246 | check_16k_inner_loop: 247 | lda (READ_PTR),y 248 | CRC16_F: 249 | ; http://www.6502.org/source/integers/crc-more.html 250 | eor CRCHI ; A contained the data 251 | sta CRCHI ; XOR it into high byte 252 | lsr ; right shift A 4 bits 253 | lsr ; to make top of x^12 term 254 | lsr ; ($1...) 255 | lsr 256 | tax ; save it 257 | asl ; then make top of x^5 term 258 | eor CRCLO ; and XOR that with low byte 259 | sta CRCLO ; and save 260 | txa ; restore partial term 261 | eor CRCHI ; and update high byte 262 | sta CRCHI ; and save 263 | asl ; left shift three 264 | asl ; the rest of the terms 265 | asl ; have feedback from x^12 266 | tax ; save bottom of x^12 267 | asl ; left shift two more 268 | asl ; watch the carry flag 269 | eor CRCHI ; bottom of x^5 ($..2.) 270 | sta CRCHI ; save high byte 271 | txa ; fetch temp value 272 | rol ; bottom of x^12, middle of x^5! 273 | eor CRCLO ; finally update low byte 274 | ldx CRCHI ; then swap high and low bytes 275 | sta CRCHI 276 | stx CRCLO 277 | iny 278 | bne check_16k_inner_loop 279 | inc READ_PTR+1 280 | dec READ_PAGE_COUNT 281 | bne check_16k_outer_loop 282 | dey ;,; ldy #$ff 283 | sty $8000 284 | vblank_loop: 285 | bit PPUSTATUS 286 | bpl vblank_loop 287 | rts 288 | ram_code_end: 289 | 290 | ram_code_size = ram_code_end - ram_code_begin 291 | .endproc 292 | -------------------------------------------------------------------------------- /src/pently.inc: -------------------------------------------------------------------------------- 1 | ; 2 | ; Pently audio engine 3 | ; Public API 4 | ; 5 | ; Copyright 2015-2018 Damian Yerrick 6 | ; 7 | ; This software is provided 'as-is', without any express or implied 8 | ; warranty. In no event will the authors be held liable for any damages 9 | ; arising from the use of this software. 10 | ; 11 | ; Permission is granted to anyone to use this software for any purpose, 12 | ; including commercial applications, and to alter it and redistribute it 13 | ; freely, subject to the following restrictions: 14 | ; 15 | ; 1. The origin of this software must not be misrepresented; you must not 16 | ; claim that you wrote the original software. If you use this software 17 | ; in a product, an acknowledgment in the product documentation would be 18 | ; appreciated but is not required. 19 | ; 2. Altered source versions must be plainly marked as such, and must not be 20 | ; misrepresented as being the original software. 21 | ; 3. This notice may not be removed or altered from any source distribution. 22 | ; 23 | 24 | ; Configuration ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 25 | 26 | ; These default values are used if pentlyconfig.inc is not included. 27 | ; If making build-time decisions, include pentlyconfig.inc first. 28 | 29 | ; Enable tvSystem-driven correction for PAL NES and Dendy 30 | .ifndef PENTLY_USE_PAL_ADJUST 31 | PENTLY_USE_PAL_ADJUST = 1 32 | .endif 33 | 34 | ; Enable music playback 35 | .ifndef PENTLY_USE_MUSIC 36 | PENTLY_USE_MUSIC = 1 37 | .endif 38 | 39 | ; Controls how music and sound effects are mixed 40 | .ifndef PENTLY_USE_MUSIC_IF_LOUDER 41 | PENTLY_USE_MUSIC_IF_LOUDER = 1 42 | .endif 43 | .ifndef PENTLY_USE_SQUARE_POOLING 44 | PENTLY_USE_SQUARE_POOLING = 1 45 | .endif 46 | 47 | ; When turned off, and sound effects or instruments on triangle 48 | ; use timbre 0 or 1, sound may be cut off prematurely. 49 | .ifndef PENTLY_USE_TRIANGLE_DUTY_FIX 50 | PENTLY_USE_TRIANGLE_DUTY_FIX = 1 51 | .endif 52 | 53 | ; Enable row callbacks 54 | .ifndef PENTLY_USE_ROW_CALLBACK 55 | PENTLY_USE_ROW_CALLBACK = 0 56 | .endif 57 | 58 | ; Write to visualizer registers 59 | .ifndef PENTLY_USE_VIS 60 | PENTLY_USE_VIS = 0 61 | .endif 62 | 63 | ; Enable seeking to rehearsal marks 64 | .ifndef PENTLY_USE_REHEARSAL 65 | PENTLY_USE_REHEARSAL = 0 66 | .endif 67 | 68 | ; Enable variable mix (track muting) 69 | .ifndef PENTLY_USE_VARMIX 70 | PENTLY_USE_VARMIX = 0 71 | .endif 72 | 73 | ; Enable vibrato (sinusoidal pitch modulation with an LFO of 74 | ; period 12 frames) 75 | .ifndef PENTLY_USE_VIBRATO 76 | PENTLY_USE_VIBRATO = 1 77 | .endif 78 | 79 | ; Enable arpeggio (rapid pitch modulation producing warbly chords of 80 | ; period 2 or 3 frames) 81 | .ifndef PENTLY_USE_ARPEGGIO 82 | PENTLY_USE_ARPEGGIO = 1 83 | .endif 84 | 85 | ; Enable portamento (slide from one pitch to another) 86 | .ifndef PENTLY_USE_PORTAMENTO 87 | PENTLY_USE_PORTAMENTO = 1 88 | .endif 89 | 90 | ; Enable first-order lowpass portamento like Roland TB-303 Bass Line 91 | .ifndef PENTLY_USE_303_PORTAMENTO 92 | PENTLY_USE_303_PORTAMENTO = PENTLY_USE_PORTAMENTO 93 | .endif 94 | 95 | .assert (!PENTLY_USE_303_PORTAMENTO) || PENTLY_USE_PORTAMENTO, error, "cannot use 303 portamento without portamento" 96 | 97 | ; Enable attack envelopes on instruments 98 | .ifndef PENTLY_USE_ATTACK_PHASE 99 | PENTLY_USE_ATTACK_PHASE = 1 100 | .endif 101 | 102 | ; Enable a fifth track that can inject attacks on top of the 103 | ; pulse 1, pulse 2, or triangle channel 104 | .ifndef PENTLY_USE_ATTACK_TRACK 105 | PENTLY_USE_ATTACK_TRACK = PENTLY_USE_ATTACK_PHASE 106 | .endif 107 | 108 | .assert (!PENTLY_USE_ATTACK_TRACK) || PENTLY_USE_ATTACK_PHASE, error, "cannot use attack track without attack phase" 109 | 110 | ; Count rows per beat 111 | .ifndef PENTLY_USE_BPMMATH 112 | PENTLY_USE_BPMMATH = 1 113 | .endif 114 | 115 | ; Methods ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 116 | 117 | ;; 118 | ; Initializes the sound channels and stops all sound effects 119 | ; and music. 120 | .global pently_init, _pently_init 121 | 122 | ;; 123 | ; Updates sound registers. Call this once each frame. 124 | ; Overwrites: ??? 125 | .global pently_update, _pently_update 126 | 127 | ;; 128 | ; Starts a sound effect. 129 | ; @param A the sound effect ID 130 | .global pently_start_sound, _pently_start_sound 131 | 132 | ;; 133 | ; Starts a piece of music. 134 | ; @param A the music ID 135 | .global pently_start_music, _pently_start_music 136 | 137 | ;; 138 | ; Stops the music. 139 | .global pently_stop_music, _pently_stop_music 140 | 141 | ;; 142 | ; Resumes the stopped music. 143 | .global pently_resume_music, _pently_resume_music 144 | 145 | ;; 146 | ; Plays note A on channel X (0, 4, 8, 12) with instrument Y. 147 | ; Trashes 0-1 and preserves X. 148 | .global pently_play_note 149 | 150 | ;; 151 | ; Returns the playing position as a fraction of a beat from 0 to 95. 152 | ; (Implemented in bpmmath.s) 153 | .global pently_get_beat_fraction, _pently_get_beat_fraction 154 | 155 | ;; 156 | ; Skips to a given row in the sequence, which must be later than the 157 | ; current row. (Requires PENTLY_USE_REHEARSAL) 158 | ; @param X rows*256 159 | ; @param A rows 160 | .global pently_skip_to_row, _pently_skip_to_row 161 | 162 | ; Constant arrays ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 163 | 164 | ; Frames per minute for each TV system 165 | .global pently_fpmLo, pently_fpmHi 166 | 167 | ; Fields ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 168 | ; 169 | ; It's highly discouraged to write to any of these. 170 | ; Use the methods above instead. 171 | 172 | ;; 173 | ; Nonzero if music is currently playing 174 | .globalzp pently_music_playing 175 | 176 | ;; 177 | ; The number of rows per beat set by the current music track. 178 | .global pently_rows_per_beat 179 | 180 | ;; 181 | ; The row within a beat, in the range 0 to pently_rows_per_beat 182 | .global pently_row_beat_part 183 | 184 | ;; 185 | ; The fraction of a row until the next row is processed. 186 | ; Will range from -3000 to 0 on PAL or -3606 to 0 on NTSC. 187 | .globalzp pently_tempoCounterLo, pently_tempoCounterHi 188 | 189 | ; Used for enhanced visualization 190 | .global pently_vis_pitchlo, pently_vis_pitchhi, pently_vis_dutyvol 191 | .global pently_vis_arpphase, pently_vis_note 192 | 193 | ;; 194 | ; Number of sequence rows that have been processed 195 | ; (Requires PENTLY_USE_REHEARSAL) 196 | .global pently_rowslo, pently_rowshi 197 | 198 | ; Modifiable fields ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 199 | 200 | ;; 201 | ; Play all notes in this track as rests if bit 7 is set. 202 | ; (Requires PENTLY_USE_VARMIX) 203 | .global pently_mute_track 204 | 205 | ;; 206 | ; Bit 7: Pause playback for step at a time playback 207 | ; Bits 2-0: Scale tempo by a factor of 4 208 | ; (Requires PENTLY_USE_REHEARSAL) 209 | .global pently_tempo_scale 210 | 211 | ; Size (for debugging) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 212 | .global PENTLYSOUND_SIZE, PENTLYMUSIC_SIZE -------------------------------------------------------------------------------- /tools/quadanalyze.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from contextlib import closing 3 | import sys 4 | import array 5 | import argparse 6 | 7 | # Use SoX 8 | import wave as wavewriter 9 | try: 10 | import soxwave 11 | except ImportError: 12 | sox_path = None 13 | else: 14 | sox_path = soxwave.get_sox_path() 15 | if sox_path: 16 | wave = soxwave 17 | else: 18 | soxwave = None 19 | import wave 20 | 21 | framelen = (256 - 1) * 4 22 | little = array.array("i",[1]).tobytes()[0] 23 | assert isinstance(little, int) 24 | 25 | def load_file(filename): 26 | with closing(wave.open(filename, "r")) as inwv: 27 | c = inwv.getnchannels() 28 | if c != 1: 29 | raise ValueError("only mono is supported, not %d channels" % c) 30 | c = inwv.getsampwidth() 31 | if c != 2: 32 | raise ValueError("only 16 bit is supported, not %d bit" % (8 * c)) 33 | nframes = inwv.getnframes() 34 | data = array.array('h', inwv.readframes(nframes)) 35 | if not little: 36 | data.byteswap() 37 | return data 38 | 39 | def play_data(data, rate): 40 | if not little: 41 | data = data[:] 42 | data.byteswap() 43 | args = ['play', '-t', 's16', '-r', str(rate), '-c', '1', '-L', '-'] 44 | wave.sox_spawn(args, data.tostring()) 45 | 46 | def save_wave_as_mono16(filename, freq, data): 47 | data = array.array('h', (min(max(s, -32767), 32767) for s in data)) 48 | if not little: 49 | data.byteswap() 50 | with closing(wavewriter.open(filename, "wb")) as outfp: 51 | outfp.setnchannels(1) 52 | outfp.setsampwidth(2) 53 | outfp.setframerate(freq) 54 | outfp.writeframes(data.tobytes()) 55 | 56 | deltas = [0, 1, 4, 9, 16, 25, 36, 49, 57 | 64, -49, -36, -25, -16, -9, -4, -1] 58 | def quadpcm_enc(data, startval=64): 59 | """Encode one packet""" 60 | out = bytearray() 61 | scmin = 64 62 | scmax = 64 63 | byt = None 64 | for s in data: 65 | scaled = min((s + 32768 + 64) // 512, 127) 66 | cands = [(abs(((startval + d) & 0x7F) - scaled), i) 67 | for (i, d) in enumerate(deltas)] 68 | enc = min(cands)[1] 69 | if byt is None: 70 | byt = enc 71 | else: 72 | out.append(byt | (enc << 4)) 73 | byt = None 74 | startval = (startval + deltas[enc]) & 0x7F 75 | return (bytes(out), startval) 76 | 77 | def quadpcm_dec(data, startval=64): 78 | """Decode one packet""" 79 | out = array.array('h') 80 | for c in data: 81 | for enc in (c & 0x0F, c >> 4): 82 | startval = (startval + deltas[enc]) & 0x7F 83 | out.append((startval - 64) * 512) 84 | return (out, startval) 85 | 86 | def halve_rate(iterable): 87 | """Convolve with [-1 0 9 16 9 0 -1]/32 and decimate by 2""" 88 | flp = array.array('h', [0] * 3) 89 | flp.extend(iterable) 90 | flp.extend([0]*3) 91 | fil = (int(round((16 * flp[i + 3] 92 | - (flp[i] + flp[i + 6]) 93 | + 9 * (flp[i + 2] + flp[i + 4])) 94 | / 32)) 95 | for i in range(0, len(flp) - 6, 2)) 96 | return array.array('h', fil) 97 | 98 | def lerp_double_rate(iterable): 99 | """Double rate with linear interpolation""" 100 | fil = array.array('h', iterable) 101 | fil.append(0) 102 | lerp1 = ((a, (a + b) // 2) 103 | for (a, b) in zip(fil[:-1], fil[1:])) 104 | return array.array('h', (s for r in lerp1 for s in r)) 105 | 106 | def quads_enc(data): 107 | data_frames = [data[i:i + framelen] 108 | for i in range(0, len(data), framelen)] 109 | del data 110 | 111 | # For each frame, I choose only low frequencies (0-4000 Hz) 112 | # or high frequencies (4000-8000 Hz), not both. 113 | # Use autocorrelation at lag 1 to see which to use. 114 | correls = [(sum(a * b for (a, b) in zip(f[1:], f[:-1])) 115 | / sum(a * a for a in f)) 116 | for f in data_frames] 117 | # flip_frames: these frames shall be decoded with the 118 | # interpolated samples flipped 119 | flip_frames = [r < 0 for r in correls] 120 | flp1 = (-s if (flip and (i & 1)) else s 121 | for (f, flip) in zip(data_frames, flip_frames) 122 | for (i, s) in enumerate(f)) 123 | fil = halve_rate(flp1) 124 | correls = flp1 = None 125 | 126 | fil_frames = [fil[i:i + framelen // 2] 127 | for i in range(0, len(fil), framelen // 2)] 128 | 129 | # At this point, the signal is (fil, flip_frames) 130 | # Encode bitstream 131 | bitstream = array.array('B') 132 | last = 64 133 | for i in range(len(flip_frames)): 134 | base = framelen // 2 * i 135 | f = fil[base:base + framelen // 2] 136 | flipval = 0x7F if flip_frames[i] else 0 137 | ## print("frame %d flip %02x" % (i, flipval)) 138 | bitstream.append(flipval) 139 | (enc, last) = quadpcm_enc(f, last) 140 | bitstream.fromstring(enc) 141 | return bitstream 142 | 143 | def quads_dec(bitstream): 144 | 145 | # Validation to see if we can decode this bitstream. 146 | enc_frames = [bitstream[i:i + framelen // 4 + 1] 147 | for i in range(0, len(bitstream), framelen // 4 + 1)] 148 | del bitstream 149 | flip_frames = [f[0] for f in enc_frames] 150 | fil = array.array('h') 151 | last = 64 152 | for f in enc_frames: 153 | (dec, last) = quadpcm_dec(f[1:], last) 154 | fil.extend(dec) 155 | 156 | # Reconstruct at full rate 157 | lerp = lerp_double_rate(fil) 158 | lerp_frames = [lerp[i:i + framelen] 159 | for i in range(0, len(lerp), framelen)] 160 | unflp = (min(32767, -s if (flip and (i & 1)) else s) 161 | for (f, flip) in zip(lerp_frames, flip_frames) 162 | for (i, s) in enumerate(f)) 163 | return array.array('h', unflp) 164 | 165 | def parse_argv(argv): 166 | a = argparse.ArgumentParser() 167 | a.add_argument("infile") 168 | a.add_argument("outfile") 169 | a.add_argument("-d", "--decode", action="store_true", 170 | help="convert qdp to wav (default: wav to qdp)") 171 | return a.parse_args(argv[1:]) 172 | 173 | def main(argv=None): 174 | args = parse_argv(argv or sys.argv) 175 | infilename = args.infile 176 | outfilename = args.outfile 177 | if args.decode: 178 | with open(infilename, "rb") as infp: 179 | bitstream = infp.read() 180 | wavedata = quads_dec(bitstream) 181 | save_wave_as_mono16(outfilename, 16000, wavedata) 182 | else: 183 | wavedata = load_file(infilename) 184 | bitstream = quads_enc(wavedata) 185 | if len(bitstream) % 256 > 0: 186 | bitstream.extend([0] * (256 - (len(bitstream) % 256))) 187 | with open(outfilename, "wb") as outfp: 188 | outfp.write(bitstream.tostring()) 189 | 190 | if __name__=='__main__': 191 | main() 192 | -------------------------------------------------------------------------------- /src/title.s: -------------------------------------------------------------------------------- 1 | .include "nes.inc" 2 | .include "global.inc" 3 | .include "pently.inc" 4 | 5 | .segment "ZEROPAGE" 6 | ciDst: .res 2 ; Moved from unpb53.s as it's only used here. 7 | 8 | .segment "CODE" 9 | .proc title_screen 10 | bit PPUSTATUS 11 | lda #VBLANK_NMI 12 | sta PPUCTRL 13 | 14 | lda TITLESCREEN+1 15 | sta donut_stream_ptr+1 16 | lda TITLESCREEN+0 17 | sta donut_stream_ptr+0 18 | 19 | ; Unpack tiles 20 | ldy #0 21 | sty PPUMASK 22 | sty PPUADDR 23 | sty PPUADDR 24 | lda (donut_stream_ptr),y 25 | tax 26 | inc donut_stream_ptr+0 27 | bne :+ 28 | inc donut_stream_ptr+1 29 | : 30 | jsr donut_block_x 31 | 32 | ; Unpack nametable 33 | lda #$20 34 | sta PPUADDR 35 | lda #$00 36 | sta PPUADDR 37 | ldx #1024/64 38 | jsr donut_block_x 39 | 40 | ; fill in the palette 41 | ldx #$3F 42 | stx PPUADDR 43 | ldy #$00 44 | sty PPUADDR 45 | lda nmis 46 | : 47 | cmp nmis 48 | beq :- 49 | palloop: 50 | ; ldy #$00 51 | lda (donut_stream_ptr),y 52 | sta PPUDATA 53 | iny 54 | cpy #16 55 | bcc palloop 56 | jsr draw_title_strings 57 | 58 | lda #0 59 | jsr pently_start_music 60 | 61 | title_wait_A: 62 | lda #VBLANK_NMI|BG_0000 63 | sta PPUCTRL 64 | jsr ppu_wait_vblank 65 | lda #$00 66 | sta PPUSCROLL 67 | sta PPUSCROLL 68 | lda #BG_ON 69 | sta PPUMASK 70 | 71 | jsr pently_update 72 | lda pently_music_playing 73 | beq selnow 74 | 75 | jsr read_pads 76 | lda detected_pads 77 | and #DETECT_1P_MOUSE 78 | beq no_mouse 79 | ldx #0 80 | jsr read_mouse_with_backward_buttons 81 | no_mouse: 82 | 83 | jsr read_zapper_trigger 84 | ora new_keys 85 | and #KEY_START|KEY_A 86 | beq title_wait_A 87 | rts 88 | 89 | ; If time expired, blank the screen and play audio 90 | selnow: 91 | jsr pently_init ; kill sound 92 | lda #0 93 | sta PPUMASK 94 | jmp quadpcm_test 95 | .endproc 96 | 97 | ; Each string consists of several parts: 98 | ; control and Y byte, X byte, a53-encoded characters, 0 byte 99 | ; Terminated by $FF, or otherwise with Y part >= 30 100 | ; 101 | ; 7654 3210 Control and Y byte 102 | ; |||| |||| 103 | ; |||+-++++- Vertical position of text 104 | ; ||+------- 0: Clear other bitplane; 1: $FF other bitplane 105 | ; |+-------- 0: Don't invert; 1: Invert bitplane 106 | ; +--------- 0: Affect bit 0; 1: affect bit 1 107 | 108 | .proc draw_title_strings 109 | lda TITLESTRINGS+1 110 | ldy TITLESTRINGS 111 | .endproc 112 | .proc draw_title_strings_ay 113 | total_tiles = tab_tilelens+0 114 | xand7 = tab_tilelens+1 115 | strstart = tab_tilelens+2 116 | str = $00 117 | 118 | ; Count the total tiles used by title screen strings 119 | cmp #$FF 120 | bcc :+ 121 | rts 122 | : 123 | sta str+1 124 | sta strstart+1 125 | sty str+0 126 | sty strstart+0 127 | lda #0 128 | sta total_tiles 129 | measureloop: 130 | ldy #0 131 | lda (str),y 132 | and #$1F 133 | cmp #$1E 134 | bcs measuredone 135 | iny 136 | 137 | ; At X position 0 to 7, add 7 to 14 and divide by 8 rounding down 138 | lda (str),y 139 | and #$07 140 | clc 141 | adc #7 142 | sta xand7 143 | 144 | ; Measure the string itself 145 | ;clc ; previous addition result never exceeds 14 146 | lda str 147 | adc #2 148 | tay 149 | lda str+1 150 | adc #0 151 | jsr vwfStrWidth ; A = pixel width, X = string length 152 | clc 153 | adc xand7 154 | lsr a 155 | lsr a 156 | lsr a 157 | clc 158 | adc total_tiles 159 | sta total_tiles 160 | 161 | tya 162 | sec 163 | adc str 164 | sta str 165 | bcc measureloop 166 | inc str+1 167 | bcs measureloop 168 | measuredone: 169 | 170 | ; Actually draw the strings to the screen 171 | lda #0 172 | sec 173 | sbc total_tiles 174 | sta total_tiles 175 | lda strstart+1 176 | sta str+1 177 | lda strstart+0 178 | sta str+0 179 | drawloop: 180 | ldy #0 181 | lda (str),y 182 | sta ciDst+1 183 | and #$1F 184 | cmp #$1E 185 | bcc drawnotdone 186 | rts 187 | drawnotdone: 188 | 189 | jsr clearLineImg 190 | ldy #1 191 | lda (str),y 192 | sta ciDst+0 193 | and #$07 194 | tax 195 | clc 196 | lda str 197 | adc #2 198 | tay 199 | lda str+1 200 | adc #0 201 | jsr vwfPuts 202 | inc str 203 | bne :+ 204 | inc str+1 205 | : 206 | ; X = horizontal position 207 | txa 208 | clc 209 | adc #7 210 | lsr a 211 | lsr a 212 | lsr a ; A = number of tiles used 213 | pha 214 | bit ciDst+1 215 | bvc :+ 216 | jsr invertTiles 217 | pla 218 | pha 219 | : 220 | 221 | ; Write tiles to destination in pattern table 222 | lda ciDst+1 223 | asl a ; C = which bitplane (0xx0 or 0xx8) 224 | lda total_tiles 225 | rol a 226 | sta xand7 227 | lda #0 228 | rol a 229 | .repeat 3 230 | asl xand7 231 | rol a 232 | .endrepeat 233 | pha 234 | ldy xand7 235 | jsr copyLineImg 236 | 237 | ; Write other bitplane as $00 or $FF 238 | jsr clearLineImg 239 | lda #$20 240 | and ciDst+1 241 | beq :+ 242 | lda #16 243 | jsr invertTiles 244 | : 245 | lda xand7 246 | eor #$08 247 | tay 248 | pla 249 | jsr copyLineImg 250 | 251 | ; Write corresponding tile numbers to nametable 252 | sec 253 | lda ciDst+0 254 | sta xand7 255 | lda ciDst+1 256 | and #$1F 257 | ror a 258 | ror xand7 259 | lsr a 260 | ror xand7 261 | lsr a 262 | ror xand7 263 | sta PPUADDR 264 | lda xand7 265 | sta PPUADDR 266 | lda #VBLANK_NMI 267 | sta PPUCTRL 268 | pla 269 | tax 270 | drawtileloop: 271 | lda total_tiles 272 | inc total_tiles 273 | sta PPUDATA 274 | dex 275 | bne drawtileloop 276 | jmp drawloop 277 | .endproc 278 | 279 | .segment "RODATA" 280 | COLOR1ON0 = $00 281 | COLOR3ON2 = $20 282 | COLOR0ON1 = $40 283 | COLOR2ON3 = $60 284 | COLOR2ON0 = $80 285 | COLOR3ON1 = $A0 286 | COLOR0ON2 = $C0 287 | COLOR1ON3 = $E0 288 | 289 | ; Error if no games are added ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 290 | .segment "CODE" 291 | .proc no_games_error 292 | ; Clear tiles 0-63 293 | lda #0 294 | tay 295 | ldx #$00 296 | jsr ppu_clear_nt 297 | ldx #$0C 298 | jsr ppu_clear_nt 299 | ; and the first nametable 300 | ldx #$20 301 | jsr ppu_clear_nt 302 | 303 | ; White text on a black background 304 | lda #$3F 305 | sta PPUADDR 306 | lda #$00 307 | sta PPUADDR 308 | lda #$16 309 | sta PPUDATA 310 | lda #$20 311 | sta PPUDATA 312 | 313 | ; Draw message 314 | lda #>no_games_title_strings 315 | ldy # 0 and isinstance(self.pairs[-1][1], list): 82 | (k, v) = self.pairs.pop() 83 | v = "\n".join(v) 84 | self.addfilteredpair(k, v) 85 | 86 | def addline(self, s): 87 | """Parse and add a single line of text.""" 88 | s = s.rstrip('\r\n') 89 | if len(self.pairs) > 0 and isinstance(self.pairs[-1][1], list): 90 | if s == '.': 91 | self.close_multiline() 92 | return 93 | 94 | # Initial periods are escaped with a period as in SMTP 95 | if s.startswith('.'): 96 | s = s[1:] 97 | self.pairs[-1][1].append(s) 98 | return 99 | 100 | # at this point we're not in a multiline 101 | s = s.lstrip() 102 | if not s: 103 | return 104 | m = self._firstlineRE.match(s) or self._secttitleRE.match(s) 105 | if m is None: 106 | raise ValueError("unrecognized pair: " + repr(s)) 107 | (k, v) = m.groups() 108 | if k.endswith('='): 109 | k = k[:-1].rstrip() 110 | elif k.endswith(':'): 111 | k = k[:-1].rstrip() 112 | v = [v] if v else [] 113 | self.pairs.append((k, v)) 114 | return 115 | self.addfilteredpair(k, v) 116 | 117 | def readfp(self, infp): 118 | """Add pairs from an open file-like object. 119 | 120 | infp must be a file or other object supporting iteration over lines. 121 | 122 | """ 123 | for line in infp: 124 | self.addline(line) 125 | 126 | def readstring(self, s): 127 | """Add pairs from a string.""" 128 | self.readfp(s.split('\n')) 129 | 130 | def read(self, filenames): 131 | """Attempt to read and parse one or more files. 132 | 133 | filenames can be a list of paths or a str being a single path. 134 | If a file cannot be read, it is ignored. Otherwise, it is parsed 135 | using readfp(). Returns a list of all files that were successfully 136 | read. 137 | 138 | """ 139 | if isinstance(filenames, str): 140 | filenames = [filenames] 141 | oknames = [] 142 | for filename in filenames: 143 | self.cur_path = filename 144 | try: 145 | with open(filename, 'r', encoding="utf-8") as infp: 146 | oknames.append(filename) 147 | lines = list(infp) 148 | except OSError: 149 | pass 150 | else: 151 | oknames = [] 152 | self.readfp(lines) 153 | finally: 154 | self.close_multiline() 155 | return oknames 156 | 157 | # "Why don't you use ConfigParser.RawConfigParser?" 158 | # No support for whitespace after a newline, which means no 159 | # double spacing and no indentation. 160 | 161 | testdata1 = """ 162 | name=value 163 | other=something 164 | duplicates=Multiple values with the same name are allowed. 165 | duplicates=Some filters may interpret this as a command to start a section. 166 | multiline: 167 | Using a colon instead of an equals sign begins a multi-line 168 | value. A period on its own line ends it. 169 | . 170 | dot warning: 171 | As with SMTP, the first period on any other line is removed. 172 | So any line starting with a dot should have this dot escaped 173 | .... like this. (Four periods become three.) 174 | . 175 | # A line starting with # or ; outside a multiline produces a comment. 176 | # Comments are treated as values with the name '#' or ';' 177 | # so that they can be round-tripped. 178 | 179 | blank value= 180 | name rule=Names may not begin or end with whitespace. 181 | name rule=Names may not begin with ';', '#', or '['. 182 | name rule = Nor may they contain '=' or ':', but anything else is fair game. 183 | NAME RULE = By default, names are case-insensitive; override optionxform to change this. 184 | """ 185 | 186 | def show_appends(k, v): 187 | item = (k, v) 188 | print("Appending %s" % repr(item)) 189 | return item 190 | 191 | if __name__ == '__main__': 192 | parser = InnieParser() 193 | ## parser.addfilter(show_appends) 194 | ## parser.readstring(testdata1) 195 | ## print("\n".join(repr(r) for r in parser.pairs)) 196 | ## parser.clear() 197 | parser.read("roms.cfg") 198 | print("\n".join(repr(r) for r in parser.pairs)) 199 | -------------------------------------------------------------------------------- /src/vwf_draw.s: -------------------------------------------------------------------------------- 1 | ; NES variable width font drawing library 2 | ; Copyright 2006-2015 Damian Yerrick 3 | ; 4 | ; Copying and distribution of this file, with or without 5 | ; modification, are permitted in any medium without royalty provided 6 | ; the copyright notice and this notice are preserved in all source 7 | ; code copies. This file is offered as-is, without any warranty. 8 | 9 | ; Change history: 10 | ; 2006-03: vwfPutTile rewritten by "Blargg" (Shay Green) 11 | ; and then adapted by Damian Yerrick to match old semantics 12 | ; 2010-06: DY skipped completely transparent pattern bytes 13 | ; 2011-11: DY added string length measuring 14 | ; 2012-01: DY added support for inverse video 15 | ; 2015-02: vwfPutTile rewritten by DY again; no more Blargg code 16 | 17 | .include "nes.inc" 18 | .export vwfPutTile, vwfPuts, vwfPuts0 19 | .export vwfGlyphWidth, vwfStrWidth, vwfStrWidth0 20 | .export clearLineImg, lineImgBuf, invertTiles, copyLineImg 21 | .exportzp lineImgBufLen, FIRST_PRINTABLE_CU 22 | .import vwfChrData, vwfChrWidths 23 | 24 | lineImgBuf = $0100 25 | lineImgBufLen = 128 26 | FONT_HT = 8 27 | FIRST_PRINTABLE_CU = $11 ;$20 for starting on ascii printable 28 | 29 | srcStr = $00 30 | horzPos = $04 31 | shiftedByte = $05 32 | tileAddr = $06 33 | shiftContinuation = $08 34 | leftMask = $0A 35 | rightMask = $0B 36 | 37 | .segment "CODE" 38 | ;; 39 | ; Clears the line image buffer. 40 | ; Does not modify Y or zero page. 41 | .proc clearLineImg 42 | ldx #lineImgBufLen/4-1 43 | lda #0 44 | : 45 | .repeat 4, I 46 | sta lineImgBuf+lineImgBufLen/4*I,x 47 | .endrepeat 48 | dex 49 | bpl :- 50 | rts 51 | .endproc 52 | 53 | .macro getTileAddr 54 | sec 55 | sbc #FIRST_PRINTABLE_CU 56 | ; Find source address 57 | asl a ; 7 6543 210- 58 | adc #$80 ; 6 -543 2107 59 | rol a ; - 5432 1076 60 | asl a ; 5 4321 076- 61 | tay 62 | and #%00000111 63 | adc #>vwfChrData 64 | sta tileAddr+1 65 | tya 66 | and #%11111000 67 | sta tileAddr 68 | .endmacro 69 | 70 | ;.res 1 ; 1 or 32 to adjust shiftslide 71 | 72 | ;; 73 | ; Puts a 1-bit tile to position X in the line image buffer. 74 | ; In: A = tile number 75 | ; X = destination X position 76 | ; Trash: AXY, $05-$0B 77 | .proc vwfPutTile 78 | getTileAddr 79 | 80 | ; Construct fast shifter 81 | txa 82 | and #%00000111 83 | tay 84 | lda leftMasks,y 85 | sta leftMask 86 | eor #$FF 87 | sta rightMask 88 | lda shiftContinuations,y 89 | sta shiftContinuation 90 | lda #>shiftslide 91 | sta shiftContinuation+1 92 | 93 | ; Process scanlines from the bottom up 94 | txa 95 | .if ::FONT_HT = 8 96 | ora #8-1 97 | .elseif ::FONT_HT = 16 98 | asl a 99 | ora #16-1 100 | .else 101 | .assert 0, error, "font size must be 8 or 16" 102 | .endif 103 | tax 104 | ldy #FONT_HT - 1 105 | chrbyteloop: 106 | lda (tileAddr),y 107 | beq isBlankByte 108 | jmp (shiftContinuation) 109 | shiftslide: 110 | rol a 111 | rol a 112 | rol a 113 | rol a 114 | rol a 115 | rol a 116 | rol a 117 | sta shiftedByte 118 | and rightMask 119 | ora lineImgBuf+FONT_HT,x 120 | sta lineImgBuf+FONT_HT,x 121 | lda shiftedByte 122 | rol a 123 | and leftMask 124 | dontshift: 125 | ora lineImgBuf,x 126 | sta lineImgBuf,x 127 | isBlankByte: 128 | dex 129 | dey 130 | bpl chrbyteloop 131 | rts 132 | 133 | ; If you get this error, go up to "adjust shiftslide" 134 | .assert >shiftslide = >dontshift, error, "shiftslide in vwfPutTile crosses page boundary" 135 | 136 | 137 | .pushseg 138 | .segment "RODATA" 139 | leftMasks: 140 | .repeat 8, I 141 | .byte $FF >> I 142 | .endrepeat 143 | shiftContinuations: 144 | .byte irpm, 69 | * Docs: Mention new features related to Action 53 mapper 70 | 71 | 0.05wip3 (2017-04-08) 72 | * Menu: Integrate JRoatch's coredump tool 73 | * Menu: Remove debug code that corrupted the background when Up is 74 | held (reported by Greg Caldwell) 75 | * exitpatch: Exit patches do not set stack pointer, so that coredump 76 | (A+B+Reset) can see the stack pointer 77 | * exitpatch: Fixed marking exitmethod=none banks as already patched 78 | * a53build: 32K mappers (BNROM, AOROM) default to last PRG bank 79 | instead of first, working around an exit patch problem 80 | * a53build: Load ROMs on a blank-named page but don't list them 81 | as titles, to allow including unreachable TCRF-bait content 82 | * Add tools/prgunused.py to find runs of 32 or more bytes of 83 | value $00 or $FF in the PRG ROM of an iNES image 84 | 85 | 0.05wip2 (2017-03-09) 86 | * Menu: Include title screen in data part of last bank (near ROM 87 | directory) instead of menu executable 88 | * a53build: Set filename and palette of title screen with config 89 | file keywords titlescreen= and titlepalette= 90 | * a53build: Handle grayscale mode screenshots and indexed 91 | screenshots with black not first 92 | * a53build: Correctly exit-patch larger ROMs (breaks a53extract) 93 | 94 | 0.05wip1 (2017-02-25) 95 | * Menu: Update music engine from Pently 3 to Pently current 96 | * Menu: Update VWF engine to unrolled shift version 97 | * Menu: Update source code file naming conventions 98 | * Menu: Remove antialiasing from start button notice 99 | * tools/*: Convert to Python 3 100 | * makefile: Work around quirks in Python for Windows 101 | * a53build: Specify unused ranges for multiple PRG banks in one 102 | activity 103 | * a53build: ROMs using Action 53 mapper (28) boot as if they're 104 | UxROM (2) 105 | * a53build: Refactor title validation and ROM patch parsing into 106 | separate subroutine 107 | * a53build: If prgbank not given, guess last ROM bank for 108 | UxROM (2) and Action 53 or first for others 109 | * a53build: Fixed several title parsing error messages 110 | * a53build: Sort ROMs by descending PRG size before inserting them, 111 | fixing arbitrary mixes of C?NROM and [ABU]NROM 112 | * autosubmulti: Don't limit input or output to one directory 113 | 114 | 0.04 (2014-11-06) 115 | * Menu: Fixed certain combinations of tab lengths causing blank 116 | block in 11th line of tab display 117 | * Menu: Bank arrow sprite positioned correctly even if tab width 118 | differs from first tab's width 119 | * Menu: Fixed uncleared line caused by rapidly switching from a 120 | long to medium to short page 121 | * Menu: Performs a write sequence to fully initialize mapper 28 122 | without losing support for mapper 34 123 | * Menu: Can customize the title screen using a .png file and a 124 | palette specified in the makefile 125 | * a53build: startbank= option to leave several 32K banks blank 126 | at the start of the ROM, for adding mapper 28 UNROM games 127 | * a53build: blank banks inserted as power-of-two padding are no 128 | longer mistakenly included in the ROM directory 129 | * innie: Supports [section title] syntax 130 | * a53extract: support ROMs created with startbank= 131 | * Collection: moved NES15 into unused space in Concentration Room 132 | * Collection: added Russian Roulette to unused space in 133 | Concentration Room 134 | * Collection: abandoned author-based groups 135 | * Collection is 12 games and 3 toys in 256 KiB 136 | 137 | 0.03 (2012-06-21) 138 | * FFD: pads to power of 2 banks early instead of adding banks on 139 | demand during CHR and screenshot insertion 140 | * Can insert CHR and screenshot data into the last bank if needed 141 | * New values for number of players: 2-4 alt and 2-6 alt 142 | * "Make your selection now!" 143 | * Collection: properly clear top score in Munchie Attack 144 | * Collection: replaced LJ65 with NES15 145 | * Collection: added Pogo Cats 146 | * Collection is 12 games and 2 toys in 512 KiB 147 | 148 | 0.02 (2012-02-10) 149 | * Menu: easier to see which tab is selected 150 | * Menu: select an activity with Super NES Mouse in port 1 151 | * Menu: select an activity with Zapper in port 2 152 | * Menu: controller reading is DPCM-unsafe for mouse compatibility 153 | * Menu is 7.5 KiB 154 | * FFD: allocating end of slice no longer invalidates entire slice 155 | * Reset patch: made the relocatable patch actually work 156 | * Collection: added Munchie Attack to unused space in LAN Master 157 | * Collection: moved FHBG into unused space in Slappin' 158 | * Collection: moved MineShaft into unused space in Zap Ruder 159 | * Collection is 11 games and 2 toys in 256 KiB 160 | 161 | 0.01 (2012-02-02) 162 | * Initial release 163 | * Collection is 10 games and 2 toys in 512 KiB 164 | -------------------------------------------------------------------------------- /tools/pilbmp2nes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Bitmap to multi-console CHR converter using Pillow, the 4 | # Python Imaging Library 5 | # 6 | # Copyright 2014-2015 Damian Yerrick 7 | # Copying and distribution of this file, with or without 8 | # modification, are permitted in any medium without royalty 9 | # provided the copyright notice and this notice are preserved. 10 | # This file is offered as-is, without any warranty. 11 | # 12 | from __future__ import with_statement, print_function, unicode_literals 13 | from PIL import Image 14 | from time import sleep 15 | 16 | def formatTilePlanar(tile, planemap, hflip=False, little=False): 17 | """Turn a tile into bitplanes. 18 | 19 | Planemap opcodes: 20 | 10 -- bit 1 then bit 0 of each tile 21 | 0,1 -- planar interleaved by rows 22 | 0;1 -- planar interlaved by planes 23 | 0,1;2,3 -- SNES/PCE format 24 | 25 | """ 26 | hflip = 7 if hflip else 0 27 | if (tile.size != (8, 8)): 28 | return None 29 | pixels = list(tile.getdata()) 30 | pixelrows = [pixels[i:i + 8] for i in range(0, 64, 8)] 31 | if hflip: 32 | for row in pixelrows: 33 | row.reverse() 34 | out = bytearray() 35 | 36 | planemap = [[[int(c) for c in row] 37 | for row in plane.split(',')] 38 | for plane in planemap.split(';')] 39 | # format: [tile-plane number][plane-within-row number][bit number] 40 | 41 | # we have five (!) nested loops 42 | # outermost: separate planes 43 | # within separate planes: pixel rows 44 | # within pixel rows: row planes 45 | # within row planes: pixels 46 | # within pixels: bits 47 | for plane in planemap: 48 | for pxrow in pixelrows: 49 | for rowplane in plane: 50 | rowbits = 1 51 | thisrow = bytearray() 52 | for px in pxrow: 53 | for bitnum in rowplane: 54 | rowbits = (rowbits << 1) | ((px >> bitnum) & 1) 55 | if rowbits >= 0x100: 56 | thisrow.append(rowbits & 0xFF) 57 | rowbits = 1 58 | out.extend(thisrow[::-1] if little else thisrow) 59 | return bytes(out) 60 | 61 | def pilbmp2chr(im, tileWidth=8, tileHeight=8, 62 | formatTile=lambda im: formatTilePlanar(im, "0;1")): 63 | """Convert a bitmap image into a list of byte strings representing tiles.""" 64 | im.load() 65 | (w, h) = im.size 66 | outdata = [] 67 | for mt_y in range(0, h, tileHeight): 68 | for mt_x in range(0, w, tileWidth): 69 | metatile = im.crop((mt_x, mt_y, 70 | mt_x + tileWidth, mt_y + tileHeight)) 71 | for tile_y in range(0, tileHeight, 8): 72 | for tile_x in range(0, tileWidth, 8): 73 | tile = metatile.crop((tile_x, tile_y, 74 | tile_x + 8, tile_y + 8)) 75 | data = formatTile(tile) 76 | outdata.append(data) 77 | return outdata 78 | 79 | def parse_argv(argv): 80 | from optparse import OptionParser 81 | parser = OptionParser(usage="usage: %prog [options] [-i] INFILE [-o] OUTFILE") 82 | parser.add_option("-i", "--image", dest="infilename", 83 | help="read image from INFILE", metavar="INFILE") 84 | parser.add_option("-o", "--output", dest="outfilename", 85 | help="write CHR data to OUTFILE", metavar="OUTFILE") 86 | parser.add_option("-W", "--tile-width", dest="tileWidth", 87 | help="set width of metatiles", metavar="HEIGHT", 88 | type="int", default=8) 89 | parser.add_option("--packbits", dest="packbits", 90 | help="use PackBits RLE compression", 91 | action="store_true", default=False) 92 | parser.add_option("-H", "--tile-height", dest="tileHeight", 93 | help="set height of metatiles", metavar="HEIGHT", 94 | type="int", default=8) 95 | parser.add_option("-1", dest="planes", 96 | help="set 1bpp mode (default: 2bpp NES)", 97 | action="store_const", const="0", default="0;1") 98 | parser.add_option("--planes", dest="planes", 99 | help="set the plane map (1bpp: 0) (NES: 0;1) (GB: 0,1) (SMS:0,1,2,3) (TG16/SNES: 0,1;2,3) (MD: 3210)") 100 | parser.add_option("--hflip", dest="hflip", 101 | help="horizontally flip all tiles (most significant pixel on right)", 102 | action="store_true", default=False) 103 | parser.add_option("--little", dest="little", 104 | help="reverse the bytes within each row-plane (needed for GBA and a few others)", 105 | action="store_true", default=False) 106 | parser.add_option("--add", dest="addamt", 107 | help="value to add to each pixel", 108 | type="int", default=0) 109 | parser.add_option("--add0", dest="addamt0", 110 | help="value to add to pixels of color 0 (if different)", 111 | type="int", default=None) 112 | (options, args) = parser.parse_args(argv[1:]) 113 | 114 | tileWidth = int(options.tileWidth) 115 | if tileWidth <= 0: 116 | raise ValueError("tile width '%d' must be positive" % tileWidth) 117 | 118 | tileHeight = int(options.tileHeight) 119 | if tileHeight <= 0: 120 | raise ValueError("tile height '%d' must be positive" % tileHeight) 121 | 122 | # Fill unfilled roles with positional arguments 123 | argsreader = iter(args) 124 | try: 125 | infilename = options.infilename 126 | if infilename is None: 127 | infilename = next(argsreader) 128 | except StopIteration: 129 | raise ValueError("not enough filenames") 130 | 131 | outfilename = options.outfilename 132 | if outfilename is None: 133 | try: 134 | outfilename = next(argsreader) 135 | except StopIteration: 136 | outfilename = '-' 137 | if outfilename == '-': 138 | import sys 139 | if sys.stdout.isatty(): 140 | raise ValueError("cannot write CHR to terminal") 141 | 142 | addamt, addamt0 = options.addamt, options.addamt0 143 | if addamt0 is None: addamt0 = addamt 144 | 145 | return (infilename, outfilename, tileWidth, tileHeight, 146 | options.packbits, options.planes, options.hflip, options.little, 147 | addamt, addamt0) 148 | 149 | argvTestingMode = True 150 | 151 | def make_stdout_binary(): 152 | """Ensure that sys.stdout is in binary mode, with no newline translation.""" 153 | 154 | # Recipe from 155 | # http://code.activestate.com/recipes/65443-sending-binary-data-to-stdout-under-windows/ 156 | # via http://stackoverflow.com/a/2374507/2738262 157 | if sys.platform == "win32": 158 | import os, msvcrt 159 | msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) 160 | 161 | def main(argv=None): 162 | import sys 163 | if argv is None: 164 | argv = sys.argv 165 | if (argvTestingMode and len(argv) < 2 166 | and sys.stdin.isatty() and sys.stdout.isatty()): 167 | argv.extend(input('args:').split()) 168 | try: 169 | (infilename, outfilename, tileWidth, tileHeight, 170 | usePackBits, planes, hflip, little, 171 | addamt, addamt0) = parse_argv(argv) 172 | except Exception as e: 173 | sys.stderr.write("%s: %s\n" % (argv[0], str(e))) 174 | sys.exit(1) 175 | 176 | im = Image.open(infilename) 177 | 178 | # Subpalette shift 179 | if addamt or addamt0: 180 | px = bytearray(im.getdata()) 181 | for i in range(len(px)): 182 | thispixel = px[i] 183 | px[i] = thispixel + (addamt if thispixel else addamt0) 184 | im.putdata(px) 185 | 186 | outdata = pilbmp2chr(im, tileWidth, tileHeight, 187 | lambda im: formatTilePlanar(im, planes, hflip, little)) 188 | outdata = b''.join(outdata) 189 | if usePackBits: 190 | from packbits import PackBits 191 | sz = len(outdata) % 0x10000 192 | outdata = PackBits(outdata).flush().tostring() 193 | outdata = b''.join([chr(sz >> 8), chr(sz & 0xFF), outdata]) 194 | 195 | # Write output file 196 | outfp = None 197 | try: 198 | if outfilename != '-': 199 | outfp = open(outfilename, 'wb') 200 | else: 201 | outfp = sys.stdout 202 | make_stdout_binary() 203 | outfp.write(outdata) 204 | finally: 205 | if outfp and outfilename != '-': 206 | outfp.close() 207 | 208 | if __name__=='__main__': 209 | main() 210 | ## main(['pilbmp2nes.py', '../tilesets/char_pinocchio.png', 'char_pinocchio.chr']) 211 | ## main(['pilbmp2nes.py', '--packbits', '../tilesets/char_pinocchio.png', 'char_pinocchio.pkb']) 212 | -------------------------------------------------------------------------------- /src/pentlysound.s: -------------------------------------------------------------------------------- 1 | ; 2 | ; Pently audio engine 3 | ; Sound effect player and "mixer" 4 | ; Copyright 2009-2018 Damian Yerrick 5 | ; 6 | ; This software is provided 'as-is', without any express or implied 7 | ; warranty. In no event will the authors be held liable for any damages 8 | ; arising from the use of this software. 9 | ; 10 | ; Permission is granted to anyone to use this software for any purpose, 11 | ; including commercial applications, and to alter it and redistribute it 12 | ; freely, subject to the following restrictions: 13 | ; 14 | ; 1. The origin of this software must not be misrepresented; you must not 15 | ; claim that you wrote the original software. If you use this software 16 | ; in a product, an acknowledgment in the product documentation would be 17 | ; appreciated but is not required. 18 | ; 2. Altered source versions must be plainly marked as such, and must not be 19 | ; misrepresented as being the original software. 20 | ; 3. This notice may not be removed or altered from any source distribution. 21 | ; 22 | 23 | .include "pentlyconfig.inc" 24 | .include "pently.inc" 25 | .if PENTLY_USE_MUSIC 26 | .import pently_update_music, pently_update_music_ch 27 | .endif 28 | .import periodTableLo, periodTableHi, pently_sfx_table 29 | .if PENTLY_USE_PAL_ADJUST 30 | .importzp tvSystem 31 | .endif 32 | .exportzp pentlyBSS 33 | .exportzp pently_zp_state 34 | 35 | .assert (pently_zptemp + 5) <= $100, error, "pently_zptemp must be within zero page" 36 | 37 | SNDCHN = $4015 38 | 39 | PULSE1_CH = $00 40 | PULSE2_CH = $04 41 | TRIANGLE_CH = $08 42 | NOISE_CH = $0C 43 | 44 | .if PENTLY_USE_MUSIC = 0 45 | PENTLYZP_SIZE = 16 46 | .elseif PENTLY_USE_ATTACK_PHASE 47 | PENTLYZP_SIZE = 32 48 | .else 49 | PENTLYZP_SIZE = 21 50 | .endif 51 | 52 | .zeropage 53 | pently_zp_state: .res PENTLYZP_SIZE 54 | sfx_datalo = pently_zp_state + 0 55 | sfx_datahi = pently_zp_state + 1 56 | ;.bss 57 | 58 | ; The statically allocated prefix of pentlyBSS 59 | pentlyBSS: .res 18 60 | 61 | sfx_rate = pentlyBSS + 0 62 | sfx_ratecd = pentlyBSS + 1 63 | ch_lastfreqhi = pentlyBSS + 2 64 | sfx_remainlen = pentlyBSS + 3 65 | 66 | .segment PENTLY_CODE 67 | pentlysound_code_start = * 68 | 69 | ;; 70 | ; Initializes all sound channels. 71 | ; Call this at the start of a program or as a "panic button" before 72 | ; entering a long stretch of code where you don't call pently_update. 73 | ; 74 | .proc pently_init 75 | ; Turn on all channels 76 | lda #$0F 77 | sta SNDCHN 78 | ; Disable pulse sweep 79 | lda #8 80 | sta $4001 81 | sta $4005 82 | ; Invalidate last frequency high byte 83 | lda #$30 84 | sta ch_lastfreqhi+0 85 | sta ch_lastfreqhi+4 86 | ; Ignore length counters and use software volume 87 | sta $4000 88 | sta $4004 89 | sta $400C 90 | lda #$80 91 | sta $4008 92 | ; Clear high period, forcing a phase reset 93 | asl a 94 | sta $4003 95 | sta $4007 96 | sta $400F 97 | ; Clear sound effects state 98 | sta sfx_remainlen+0 99 | sta sfx_remainlen+4 100 | sta sfx_remainlen+8 101 | sta sfx_remainlen+12 102 | sta sfx_ratecd+0 103 | sta sfx_ratecd+4 104 | sta sfx_ratecd+8 105 | sta sfx_ratecd+12 106 | .if ::PENTLY_USE_MUSIC 107 | sta pently_music_playing 108 | .endif 109 | ; Set DAC value, which controls pulse vs. not-pulse balance 110 | lda #PENTLY_INITIAL_4011 111 | sta $4011 112 | rts 113 | .endproc 114 | 115 | ;; 116 | ; Starts a sound effect. 117 | ; (Trashes pently_zptemp+0 through +4 and X.) 118 | ; 119 | ; @param A sound effect number (0-63) 120 | ; 121 | .proc pently_start_sound 122 | snddatalo = pently_zptemp + 0 123 | snddatahi = pently_zptemp + 1 124 | sndlen = pently_zptemp + 3 125 | sndrate = pently_zptemp + 4 126 | 127 | asl a 128 | asl a 129 | tax 130 | lda pently_sfx_table,x 131 | sta snddatalo 132 | lda pently_sfx_table+1,x 133 | sta snddatahi 134 | lda pently_sfx_table+2,x 135 | lsr a 136 | lsr a 137 | lsr a 138 | lsr a 139 | sta sndrate 140 | lda pently_sfx_table+3,x 141 | sta sndlen 142 | lda pently_sfx_table+2,x 143 | and #$0C 144 | tax 145 | 146 | ; Split up square wave sounds between pulse 1 ($4000) and 147 | ; pulse 2 ($4004) depending on which has less data left to play 148 | .if ::PENTLY_USE_SQUARE_POOLING 149 | bne not_ch0to4 ; if not ch 0, don't try moving it 150 | lda sfx_remainlen+4 151 | cmp sfx_remainlen 152 | bcs not_ch0to4 153 | ldx #4 154 | not_ch0to4: 155 | .endif 156 | 157 | ; If this sound effect is no shorter than the existing effect 158 | ; on the same channel, replace the current effect if any 159 | lda sndlen 160 | cmp sfx_remainlen,x 161 | bcc ch_full 162 | sta sfx_remainlen,x 163 | lda snddatalo 164 | sta sfx_datalo,x 165 | lda snddatahi 166 | sta sfx_datahi,x 167 | lda sndrate 168 | sta sfx_rate,x 169 | sta sfx_ratecd,x 170 | ch_full: 171 | 172 | rts 173 | .endproc 174 | 175 | ;; 176 | ; Updates sound effect channels. 177 | ; 178 | .proc pently_update 179 | .if ::PENTLY_USE_MUSIC 180 | jsr pently_update_music 181 | .endif 182 | ldx #12 183 | loop: 184 | .if ::PENTLY_USE_MUSIC 185 | jsr pently_update_music_ch 186 | .endif 187 | jsr pently_update_one_ch 188 | dex 189 | dex 190 | dex 191 | dex 192 | bpl loop 193 | rts 194 | .endproc 195 | 196 | out_volume = pently_zptemp + 2 197 | out_pitch = pently_zptemp + 3 198 | out_pitchadd = pently_zptemp + 4 199 | 200 | .proc pently_update_one_ch 201 | srclo = pently_zptemp + 0 202 | srchi = pently_zptemp + 1 203 | 204 | ; At this point, pently_update_music_ch should have left 205 | ; duty and volume in out_volume and pitch in out_pitch. 206 | lda sfx_remainlen,x 207 | bne ch_not_done 208 | 209 | ; Only music is playing on this channel, no sound effect 210 | .if ::PENTLY_USE_MUSIC 211 | lda out_volume 212 | .if ::PENTLY_USE_VIS 213 | sta pently_vis_dutyvol,x 214 | .endif 215 | bne update_channel_hw 216 | .endif 217 | 218 | ; Turn off the channel and force a reinit of the length counter. 219 | cpx #TRIANGLE_CH 220 | beq not_triangle_kill 221 | lda #$30 222 | not_triangle_kill: 223 | sta $4000,x 224 | lda #$FF 225 | sta ch_lastfreqhi,x 226 | rts 227 | ch_not_done: 228 | 229 | ; Get the sound effect word's address 230 | lda sfx_datalo+1,x 231 | sta srchi 232 | lda sfx_datalo,x 233 | sta srclo 234 | 235 | ; Advance if playback rate divider says so 236 | dec sfx_ratecd,x 237 | bpl no_next_word 238 | clc 239 | adc #2 240 | sta sfx_datalo,x 241 | bcc :+ 242 | inc sfx_datahi,x 243 | : 244 | lda sfx_rate,x 245 | sta sfx_ratecd,x 246 | dec sfx_remainlen,x 247 | no_next_word: 248 | 249 | ; fetch the instruction 250 | ldy #0 251 | .if ::PENTLY_USE_MUSIC 252 | .if ::PENTLY_USE_MUSIC_IF_LOUDER 253 | lda out_volume 254 | pha 255 | and #$0F 256 | sta out_volume 257 | lda (srclo),y 258 | and #$0F 259 | 260 | ; At this point: A = sfx volume; out_volume = music volume 261 | cmp out_volume 262 | pla 263 | sta out_volume 264 | bcc update_channel_hw 265 | .endif 266 | .if ::PENTLY_USE_VIBRATO || ::PENTLY_USE_PORTAMENTO 267 | sty out_pitchadd ; sfx don't support fine pitch adjustment 268 | .if ::PENTLY_USE_VIS 269 | tya 270 | sta pently_vis_pitchlo,x 271 | .endif 272 | .endif 273 | .endif 274 | lda (srclo),y 275 | sta out_volume 276 | iny 277 | lda (srclo),y 278 | sta out_pitch 279 | ; jmp update_channel_hw 280 | .endproc 281 | 282 | .proc update_channel_hw 283 | ; XXX vis does not work with no-music 284 | .if ::PENTLY_USE_VIS 285 | lda out_pitch 286 | sta pently_vis_pitchhi,x 287 | .endif 288 | lda out_volume 289 | .if ::PENTLY_USE_VIS 290 | sta pently_vis_dutyvol,x 291 | .endif 292 | ora #$30 293 | cpx #NOISE_CH 294 | bne notnoise 295 | sta $400C 296 | lda out_pitch 297 | sta $400E 298 | rts 299 | notnoise: 300 | 301 | ; If triangle, keep linear counter load (bit 7) on while playing 302 | ; so that envelopes don't terminate prematurely 303 | .if ::PENTLY_USE_TRIANGLE_DUTY_FIX 304 | cpx #8 305 | bne :+ 306 | and #$0F 307 | beq :+ 308 | ora #$80 ; for triangle keep bit 7 (linear counter load) on 309 | : 310 | .endif 311 | 312 | sta $4000,x 313 | ldy out_pitch 314 | .if ::PENTLY_USE_PAL_ADJUST 315 | ; Correct pitch for PAL NES only, not NTSC (0) or PAL famiclone (2) 316 | lda tvSystem 317 | lsr a 318 | bcc :+ 319 | iny 320 | : 321 | .endif 322 | 323 | lda periodTableLo,y 324 | .if ::PENTLY_USE_VIBRATO || ::PENTLY_USE_PORTAMENTO 325 | clc 326 | adc out_pitchadd 327 | sta $4002,x 328 | lda out_pitchadd 329 | and #$80 330 | bpl :+ 331 | lda #$FF 332 | : 333 | adc periodTableHi,y 334 | .else 335 | sta $4002,x 336 | lda periodTableHi,y 337 | .endif 338 | cpx #8 339 | beq always_write_high_period 340 | cmp ch_lastfreqhi,x 341 | beq no_change_to_hi_period 342 | sta ch_lastfreqhi,x 343 | always_write_high_period: 344 | sta $4003,x 345 | no_change_to_hi_period: 346 | 347 | rts 348 | .endproc 349 | 350 | PENTLYSOUND_SIZE = * - pentlysound_code_start 351 | 352 | ; aliases for cc65 353 | _pently_init = pently_init 354 | _pently_start_sound = pently_start_sound 355 | _pently_update = pently_update 356 | -------------------------------------------------------------------------------- /src/donut.s: -------------------------------------------------------------------------------- 1 | ; "Donut", NES CHR codec decompressor, 2 | ; Copyright (c) 2018 Johnathan Roatch 3 | ; 4 | ; Copying and distribution of this file, with or without 5 | ; modification, are permitted in any medium without royalty provided 6 | ; the copyright notice and this notice are preserved in all source 7 | ; code copies. This file is offered as-is, without any warranty. 8 | ; 9 | ; Version History: 10 | ; 2019-02-15: Swapped the M and L bits, for conceptual consistency. 11 | ; Also rearranged branches for speed. 12 | ; 2019-02-07: Removed "Duplicate" block type, and moved 13 | ; Uncompressed block to below 0xc0 to make room 14 | ; for block handling commands in the 0xc0~0xff space 15 | ; 2018-09-29: Removed block option of XORing with existing block 16 | ; for extra speed in decoding. 17 | ; 2018-08-13: Changed the format of raw blocks to not be reversed. 18 | ; Register X is now an argument for the buffer offset. 19 | ; 2018-04-30: Initial release. 20 | ; 21 | 22 | .export donut_decompress_block, donut_block_ayx, donut_block_x 23 | .export donut_block_buffer 24 | .exportzp donut_stream_ptr 25 | .exportzp donut_block_count 26 | 27 | temp = $00 ; 15 bytes are used 28 | 29 | donut_block_buffer = $0100 ; 64 bytes 30 | 31 | .segment "ZEROPAGE" 32 | donut_stream_ptr: .res 2 33 | donut_block_count: .res 1 34 | 35 | .segment "LOWCODE" 36 | 37 | ;; 38 | ; Decompresses a single variable sized block pointed to by donut_stream_ptr 39 | ; Outputing 64 bytes to donut_block_buffer offsetted by the X register. 40 | ; On success, 64 will be added to the X register, donut_block_count 41 | ; will be decremented, and Y will contain the number of bytes read. 42 | ; 43 | ; Block header: 44 | ; LMlmbbBR 45 | ; |||||||+-- Rotate plane bits (135° reflection) 46 | ; ||||000--- All planes: 0x00 47 | ; ||||010--- L planes: 0x00, M planes: pb8 48 | ; ||||100--- L planes: pb8, M planes: 0x00 49 | ; ||||110--- All planes: pb8 50 | ; ||||001--- In another header byte, For each bit starting from MSB 51 | ; |||| 0: 0x00 plane 52 | ; |||| 1: pb8 plane 53 | ; ||||011--- In another header byte, Decode only 1 pb8 plane and 54 | ; |||| duplicate it for each bit starting from MSB 55 | ; |||| 0: 0x00 plane 56 | ; |||| 1: duplicated plane 57 | ; |||| If extra header byte = 0x00, no pb8 plane is decoded. 58 | ; ||||1x1--- Reserved for Uncompressed block bit pattern 59 | ; |||+------ M planes predict from 0xff 60 | ; ||+------- L planes predict from 0xff 61 | ; |+-------- M = M XOR L 62 | ; +--------- L = M XOR L 63 | ; 00101010-- Uncompressed block of 64 bytes (bit pattern is ascii '*' ) 64 | ; Header >= 0xc0: Error, avaliable for outside processing. 65 | ; X >= 192: Also returns in Error, the buffer would of unexpectedly page warp. 66 | ; 67 | ; Trashes Y, A, temp 0 ~ temp 15. 68 | ; bytes: 253, cycles: 1269 ~ 7238. 69 | .proc donut_decompress_block 70 | plane_buffer = temp+0 ; 8 bytes 71 | pb8_ctrl = temp+8 72 | temp_y = pb8_ctrl 73 | even_odd = temp+9 74 | block_offset = temp+10 75 | plane_def = temp+11 76 | block_offset_end = temp+12 77 | block_header = temp+13 78 | is_rotated = temp+14 79 | ;_donut_unused_temp = temp+15 80 | ldy #$00 81 | txa 82 | clc 83 | adc #64 84 | bcs exit_error 85 | sta block_offset_end 86 | 87 | lda (donut_stream_ptr), y 88 | iny ; Reading input bytes are now post-increment. 89 | sta block_header 90 | 91 | cmp #$2a 92 | beq do_raw_block 93 | ;,; bne do_normal_block 94 | do_normal_block: 95 | cmp #$c0 96 | bcc continue_normal_block 97 | ;,; bcs exit_error 98 | exit_error: 99 | rts 100 | ; If we don't exit here, xor_l_onto_m can underflow into zeropage. 101 | 102 | ; I'm inserting these things here instead of above the donut_decompress_block 103 | ; at the cost of 1 cycle with the continue_normal_block branch for these reasons: 104 | ; The start of the main routine remains at the start of the .proc scope 105 | ; and I can save 1 byte with 'bcs end_block' 106 | 107 | read_plane_def_from_stream: 108 | ror 109 | lda (donut_stream_ptr), y 110 | iny 111 | bne plane_def_ready ;,; jmp plane_def_ready 112 | 113 | do_raw_block: 114 | ;,; ldx block_offset 115 | raw_block_loop: 116 | lda (donut_stream_ptr), y 117 | iny 118 | sta donut_block_buffer, x 119 | inx 120 | cpy #65 ; size of a raw block 121 | bcc raw_block_loop 122 | bcs end_block ;,; jmp end_block 123 | 124 | continue_normal_block: 125 | stx block_offset 126 | 127 | ;,; lda block_header 128 | and #%11011111 129 | ; The 0 are bits selected for the even ("lower") planes 130 | ; The 1 are bits selected for the odd planes 131 | ; bits 0~3 should be set to allow the mask after this to work. 132 | sta even_odd 133 | ; even_odd toggles between the 2 fields selected above for each plane. 134 | 135 | ;,; lda block_header 136 | lsr 137 | ror is_rotated 138 | lsr 139 | bcs read_plane_def_from_stream 140 | ;,; bcc unpack_shorthand_plane_def 141 | unpack_shorthand_plane_def: 142 | and #$03 143 | tax 144 | lda shorthand_plane_def_table, x 145 | plane_def_ready: 146 | ror is_rotated 147 | sta plane_def 148 | sty temp_y 149 | 150 | clc 151 | lda block_offset 152 | plane_loop: 153 | adc #8 154 | sta block_offset 155 | 156 | lda even_odd 157 | eor block_header 158 | sta even_odd 159 | 160 | ;,; lda even_odd 161 | and #$30 162 | beq not_predicted_from_ff 163 | lda #$ff 164 | not_predicted_from_ff: 165 | ; else A = 0x00 166 | 167 | asl plane_def 168 | bcc do_zero_plane 169 | ;,; bcs do_pb8_plane 170 | do_pb8_plane: 171 | ldy temp_y 172 | bit is_rotated 173 | bpl no_rewind_input_pointer 174 | ldy #$02 175 | no_rewind_input_pointer: 176 | tax 177 | lda (donut_stream_ptr), y 178 | iny 179 | sta pb8_ctrl 180 | txa 181 | 182 | ;,; bit is_rotated 183 | bvs do_rotated_pb8_plane 184 | ;,; bvc do_normal_pb8_plane 185 | do_normal_pb8_plane: 186 | ldx block_offset 187 | ;,; sec ; C is set from 'asl plane_def' above 188 | rol pb8_ctrl 189 | pb8_loop: 190 | bcc pb8_use_prev 191 | lda (donut_stream_ptr), y 192 | iny 193 | pb8_use_prev: 194 | dex 195 | sta donut_block_buffer, x 196 | asl pb8_ctrl 197 | bne pb8_loop 198 | sty temp_y 199 | ;,; beq end_plane ;,; jmp end_plane 200 | end_plane: 201 | bit even_odd 202 | bpl not_xor_m_onto_l 203 | xor_m_onto_l: 204 | ldy #8 205 | xor_m_onto_l_loop: 206 | dex 207 | lda donut_block_buffer, x 208 | eor donut_block_buffer+8, x 209 | sta donut_block_buffer, x 210 | dey 211 | bne xor_m_onto_l_loop 212 | not_xor_m_onto_l: 213 | 214 | bvc not_xor_l_onto_m 215 | xor_l_onto_m: 216 | ldy #8 217 | xor_l_onto_m_loop: 218 | dex 219 | lda donut_block_buffer, x 220 | eor donut_block_buffer+8, x 221 | sta donut_block_buffer+8, x 222 | dey 223 | bne xor_l_onto_m_loop 224 | not_xor_l_onto_m: 225 | 226 | lda block_offset 227 | cmp block_offset_end 228 | bcc plane_loop 229 | ldy temp_y 230 | end_block: 231 | ;,; sec 232 | clc 233 | tya 234 | adc donut_stream_ptr+0 235 | sta donut_stream_ptr+0 236 | bcc add_stream_ptr_no_inc_high_byte 237 | inc donut_stream_ptr+1 238 | add_stream_ptr_no_inc_high_byte: 239 | ldx block_offset_end 240 | dec donut_block_count 241 | rts 242 | 243 | do_zero_plane: 244 | ldx block_offset 245 | ldy #8 246 | fill_plane_loop: 247 | dex 248 | sta donut_block_buffer, x 249 | dey 250 | bne fill_plane_loop 251 | beq end_plane ;,; jmp end_plane 252 | 253 | do_rotated_pb8_plane: 254 | ldx #8 255 | buffered_pb8_loop: 256 | asl pb8_ctrl 257 | bcc buffered_pb8_use_prev 258 | lda (donut_stream_ptr), y 259 | iny 260 | buffered_pb8_use_prev: 261 | dex 262 | sta plane_buffer, x 263 | bne buffered_pb8_loop 264 | sty temp_y 265 | ldy #8 266 | ldx block_offset 267 | flip_bits_loop: 268 | asl plane_buffer+0 269 | ror 270 | asl plane_buffer+1 271 | ror 272 | asl plane_buffer+2 273 | ror 274 | asl plane_buffer+3 275 | ror 276 | asl plane_buffer+4 277 | ror 278 | asl plane_buffer+5 279 | ror 280 | asl plane_buffer+6 281 | ror 282 | asl plane_buffer+7 283 | ror 284 | dex 285 | sta donut_block_buffer, x 286 | dey 287 | bne flip_bits_loop 288 | beq end_plane ;,; jmp end_plane 289 | 290 | shorthand_plane_def_table: 291 | .byte $00, $55, $aa, $ff 292 | .endproc 293 | 294 | .segment "CODE" 295 | 296 | ;; 297 | ; helper subroutine for passing parameters with registers 298 | ; decompress X*64 bytes starting at AAYY to PPU_DATA 299 | .proc donut_block_ayx 300 | sty donut_stream_ptr+0 301 | sta donut_stream_ptr+1 302 | ;,; jmp donut_block_x 303 | .endproc 304 | .proc donut_block_x 305 | PPU_DATA = $2007 306 | stx donut_block_count 307 | block_loop: 308 | ldx #64 309 | jsr donut_decompress_block 310 | cpx #128 311 | bne end_block_upload 312 | ; bail if donut_decompress_block does not 313 | ; advance X by 64 bytes, indicating a header error. 314 | 315 | ldx #64 316 | upload_loop: 317 | lda donut_block_buffer, x 318 | sta PPU_DATA 319 | inx 320 | bpl upload_loop 321 | ldx donut_block_count 322 | bne block_loop 323 | end_block_upload: 324 | rts 325 | .endproc 326 | -------------------------------------------------------------------------------- /tools/ineschr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | 4 | Program to insert or extract CHR data in an iNES ROM as PNG 5 | 6 | See versionText below for copyright notice. 7 | 8 | Delayed by one day due to the blackout of GNU.org with no 9 | clickthrough during the SOPA strike of January 18, 2012. 10 | 11 | """ 12 | from __future__ import division, with_statement, print_function 13 | from PIL import Image 14 | from itertools import chain 15 | import ines 16 | 17 | descriptionText = """Lists, extracts, or inserts 8192-byte CHR ROM banks in an iNES executable.""" 18 | versionText = """%prog 0.02 19 | 20 | Copyright 2012 Damian Yerrick 21 | 22 | Copying and distribution of this file, with or without modification, 23 | are permitted in any medium without royalty provided the copyright 24 | notice and this notice are preserved. This file is offered as-is, 25 | without any warranty. 26 | """ 27 | 28 | def sliver_to_texels(lo, hi): 29 | return bytearray(((lo >> i) & 1) | (((hi >> i) & 1) << 1) 30 | for i in range(7, -1, -1)) 31 | 32 | def chrrow_to_texels(chrdata): 33 | from itertools import chain 34 | 35 | _z = zip 36 | _slv = sliver_to_texels 37 | _r8 = bytes(range(8)) 38 | scanlines = ((_slv(bp0, bp1) 39 | for (bp0, bp1) 40 | in _z(chrdata[scanline::16], 41 | chrdata[scanline + 8::16])) 42 | for scanline in _r8) 43 | scanlines = [bytearray(chain(*scanline)) 44 | for scanline in scanlines] 45 | return scanlines 46 | 47 | def chrbank_to_texels(chrdata, tile_width=16): 48 | """Convert an 8-bit string containing chrdata to a list of pixel arrays.""" 49 | from itertools import chain 50 | 51 | # Break CHR data into rows of tiles 52 | tile_row_bytes = 16 * tile_width 53 | chrrows = [chrdata[i:i + tile_row_bytes] 54 | for i in range(0, len(chrdata), tile_row_bytes)] 55 | if len(chrrows[-1]) < tile_row_bytes: 56 | chrrows[-1] = chrrows[-1] + '\0'*(tile_row_bytes - len(chrrows[-1])) 57 | 58 | # Convert each row to CHR 59 | return list(chain(*(chrrow_to_texels(row) for row in chrrows))) 60 | 61 | def chrbank_to_pil(chrdata, tile_width=16): 62 | txls = chrbank_to_texels(chrdata, tile_width) 63 | ht = len(txls) 64 | im = Image.frombytes('P', (8 * tile_width, ht), b''.join(txls)) 65 | im.putpalette(b'\x00\x00\x00\x66\x66\x66\xb2\xb2\xb2\xff\xff\xff'*64) 66 | return im 67 | 68 | def texels_to_sliver(seq): 69 | from operator import or_ 70 | seq = map(None, *((s & 1, s & 2) for s in seq)) 71 | seq = tuple(reduce(or_, 72 | (1 << pv 73 | for (s, pv) in zip(s, range(len(s) - 1, -1, -1)) 74 | if s), 75 | 0) 76 | for s in seq) 77 | return seq 78 | 79 | def texels_to_chrrow(texels): 80 | from itertools import chain 81 | 82 | # Convert each sliver to a pair of bytes, then collect each plane 83 | # of each tile 84 | tiles = (map(None, *(texels_to_sliver(row[x:x + 8]) for row in texels)) 85 | for x in range(0, len(texels[0]), 8)) 86 | 87 | # Convert a list of lists of planes in tiles to a byte string 88 | return ''.join(chr(x) for x in chain(*chain(*tiles))) 89 | 90 | def test_chrrow(): 91 | a_half = [ 92 | [0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3], 93 | [0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 3, 0], 94 | [0, 2, 2, 0, 0, 2, 2, 0, 0, 1, 0, 0, 0, 3, 0, 0], 95 | [0, 2, 2, 2, 2, 2, 2, 0, 0, 1, 0, 0, 3, 0, 0, 0], 96 | [0, 3, 3, 0, 0, 3, 3, 0, 0, 0, 0, 3, 0, 2, 2, 0], 97 | [0, 3, 3, 0, 0, 3, 3, 0, 0, 0, 3, 0, 0, 0, 0, 2], 98 | [0, 3, 3, 0, 0, 3, 3, 0, 0, 3, 0, 0, 0, 0, 2, 0], 99 | [0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 2, 2, 2] 100 | ] 101 | from binascii import b2a_hex 102 | print(b2a_hex(texels_to_chrrow(a_half))) 103 | 104 | def texels_to_chrbank(texels): 105 | """Convert a sequence of sequences of texel values to a CHR bank.""" 106 | 107 | if len(texels) % 8 != 0: 108 | raise ValueError("image height should be a multiple of 8, not %d" 109 | % len(texels)) 110 | if len(texels[0]) % 8 != 0: 111 | raise ValueError("image width should be a multiple of 8, not %d" 112 | % len(texels)) 113 | texrows = (texels_to_chrrow(texels[i:i + 8]) 114 | for i in range(0, len(texels), 8)) 115 | return ''.join(texrows) 116 | 117 | def pil_to_chrbank(im): 118 | if im.mode != 'P': 119 | raise TypeError("image mode should be 'P' (palette), not %s" % im.mode) 120 | (w, h) = im.size 121 | 122 | # Get texels from tilesheet 123 | im = im.tostring() 124 | assert w * h == len(im) 125 | 126 | # Get rows of texels 127 | im = [bytearray(im[i:i + w]) for i in xrange(0, len(im), w)] 128 | return texels_to_chrbank(im) 129 | 130 | def test_roundtrip(): 131 | filename = "../../my_games/lj65 0.41.nes" 132 | chrdata = ines.load_ines(filename)['chr'][:8192] 133 | im = chrbank_to_pil(chrdata) 134 | otherdata = pil_to_chrbank(im) 135 | assert otherdata == chrdata 136 | del chrdata, otherdata 137 | assert parse_bank_list('0-3,c,e,d-f') == [0,1,2,3,12,13,14,15] 138 | return im 139 | 140 | def parse_bank_list(bank_str): 141 | from itertools import chain 142 | 143 | ranges = ([int(bn.strip(), 16) for bn in br.split('-',1)] 144 | for br in bank_str.split(',')) 145 | ranges = (range(br[0], br[1] + 1) if len(br) > 1 else br 146 | for br in ranges) 147 | ranges = sorted(set(chain(*ranges))) 148 | return ranges 149 | 150 | def command_list(filename, chrrom, banks): 151 | from hashlib import sha1 152 | 153 | for bank in banks: 154 | m = sha1() 155 | m.update(chrrom[bank * 8192:bank * 8192 + 8192]) 156 | print("%s *%s-%02x" % (m.hexdigest(), filename, bank)) 157 | 158 | def command_extract(dstprefix, chrrom, banks): 159 | from hashlib import sha1 160 | 161 | for bank in banks: 162 | im = chrbank_to_pil(chrrom[bank * 8192:bank * 8192 + 8192]) 163 | imgname = "%s-%02x.png" % (dstprefix, bank) 164 | im.save(imgname) 165 | 166 | def command_insert(filename, chrbase, banks, srcprefix): 167 | from hashlib import sha1 168 | 169 | with open(filename, 'rb+') as outfp: 170 | for bank in banks: 171 | imgname = "%s-%02x.png" % (srcprefix, bank) 172 | chrbank = pil_to_chrbank(Image.open(imgname)) 173 | seekpos = chrbase + 8192 * bank 174 | outfp.seek(seekpos) 175 | outfp.write(chrbank) 176 | 177 | def main(argv=None): 178 | from optparse import OptionParser 179 | import sys 180 | 181 | if argv is None: 182 | argv = sys.argv 183 | parser = OptionParser(usage="usage: %prog [options] NESFILE", 184 | description=descriptionText, 185 | version=versionText) 186 | parser.add_option('-l', '--list', dest="cmd", 187 | action="store_const", const="l", default="l", 188 | help="list SHA-1 sums of all 8 KiB CHR ROM banks") 189 | parser.add_option('-x', '--extract', dest="cmd", 190 | action="store_const", const="x", 191 | help="extract CHR banks to e.g. something.nes-00.png") 192 | parser.add_option('-i', '--insert', dest="cmd", 193 | action="store_const", const="i", 194 | help="reinsert 128x256 pixel indexed images to CHR ROM banks") 195 | parser.add_option('-o', '--output-prefix', dest="prefix", 196 | help="name of images (followed by -00.png, -01.png, etc.)") 197 | parser.add_option('-b', '--banks', dest="banks", default="", 198 | help="specify banks or ranges of banks by hexadecimal numbers, e.g. 00,01,1c-1f") 199 | parser.add_option('--prg-rom', dest="prgrom", 200 | action="store_true", default=False, 201 | help="use PRG ROM instead of CHR ROM") 202 | (options, filenames) = parser.parse_args(argv[1:]) 203 | 204 | if len(filenames) > 1: 205 | parser.error("too many filenames") 206 | if len(filenames) < 1: 207 | import os 208 | parser.error("no executable given; try %s --help" 209 | % os.path.basename(argv[0])) 210 | try: 211 | banks = parse_bank_list(options.banks) if options.banks else None 212 | except ValueError as e: 213 | parser.error("invalid bank list: "+options.banks) 214 | try: 215 | rom = ines.load_ines(filenames[0]) 216 | except ValueError as e: 217 | parser.error(str(e)) 218 | 219 | chrdata = rom['prg'] if options.prgrom else rom.get('chr', b'') 220 | 221 | if len(chrdata) < 8192: 222 | parser.error("%s has no CHR ROM; try --prg-rom" % filenames[0]) 223 | if banks is None: 224 | banks = range(len(chrdata) // 8192) 225 | 226 | filenameprefix = (options.prefix 227 | if options.prefix is not None 228 | else filenames[0]) 229 | 230 | if options.cmd == 'l': 231 | command_list(filenames[0], chrdata, banks) 232 | elif options.cmd == 'x': 233 | command_extract(filenameprefix, chrdata, banks) 234 | elif options.cmd == 'i': 235 | chrbase = 16 + len(rom.get('trainer', '')) 236 | if not options.prgrom: 237 | chrbase += len(rom['prg']) 238 | print("chrbase is", chrbase) 239 | command_insert(filenames[0], chrbase, banks, filenameprefix) 240 | else: 241 | parser.error("unknown command -%s" % options.cmd) 242 | 243 | if __name__=='__main__': 244 | main() 245 | ## main(['ineschr.py', '-x', 246 | ## '../../thwaite/thwaite.nes']) 247 | -------------------------------------------------------------------------------- /docs/file_system.md: -------------------------------------------------------------------------------- 1 | Action 53 file system 2 | ===================== 3 | 4 | The Action 53 ROM builder stores several data structures related to 5 | the ROMs in a collection. 6 | 7 | Key block 8 | --------- 9 | The **key block** is a set of metadata about the entire collection. 10 | It's analogous to the volume boot record in the boot sector of a 11 | computer storage device's file system. 12 | 13 | As of the 2018 version of _Action 53_, used for volumes 3 and 4, the 14 | key block begins 256 bytes from the end of the ROM, at offset $7F00 15 | in the last 32 KiB bank. Because the NES maps ROM to $8000-$FFFF, 16 | it appears at $FF00 in CPU address space. A ROM image in iNES format 17 | (or its offshoot NES 2.0) carries a 16-byte NES 2.0 header before the 18 | ROM data, and the key block for a 512 KiB collection in iNES format 19 | is thus at file offset $07FF10. 20 | 21 | All CPU addresses in the key block and elsewhere are little-endian, 22 | meaning bits 7-0 come before bits 15-8, and they have bit 15 set to 23 | true ($8000-$FFFF not $0000-$7FFF). Thus an offset of $4321 in a 24 | bank is stored as `21 C3`. All addresses in the key block refer to 25 | the last bank of the ROM unless stated otherwise. 26 | 27 | * $FF00: Address of ROM directory 28 | * $FF02: Address of CHR directory 29 | * $FF04: Address of screenshot directory 30 | * $FF06: Address of activity directory 31 | * $FF08: Address of page directory 32 | * $FF0A: Address of activity name directory 33 | * $FF0C: Address of descriptions (possibly in another bank) 34 | * $FF0E: 32K bank number where descriptions are stored 35 | * $FF0F: Unused 36 | * $FF10: Address of compressed title screen 37 | * $FF12: Address of title strings 38 | * $FF14: Address of replacement table for DTE decompression 39 | 40 | ROM directory 41 | ------------- 42 | The menu does not use this; it's for the extraction tool. 43 | Records in the following format describe each ROM in a collection: 44 | 45 | * 1 byte: Size of PRG ROM in 16384 byte half banks (same as header 46 | byte 4). Usually 1, 2, 4, 8, or 16. 47 | * 1 byte: Size of CHR ROM in 8192 byte units (same as header byte 5). 48 | Usually 0, 1, 2, or 4. 49 | * Variable: An unpatch record for each 32768-byte PRG bank. 50 | * 0-4 bytes: The CHR directory index for each CHR ROM bank, if any. 51 | 52 | A PRG ROM size of 0 ends the ROM directory. 53 | 54 | Unpatch records contain the data that the exit patch overwrote in 55 | each PRG ROM bank. 56 | 57 | * 1 byte: PRG ROM bank number 58 | * 2 bytes: Original reset vector at $FFFC 59 | * 1 byte: Length of unpatch data in bytes 60 | * n bytes: Length of data 61 | 62 | The unpatch data starts at the patched reset vector. If the 63 | length is less than 128, it consists of that many literal bytes. 64 | Otherwise, a single byte is stored, and the exit patch consists 65 | of _n_ minus 128 repetitions of a single byte. 66 | 67 | Unpatch data for CHR ROM, screenshots, and the description block 68 | is treated as solid $FF. 69 | 70 | CHR directory 71 | ------------- 72 | To be rewritten after interleave for Donut is rethought. 73 | 74 | *The following refers to a previous version* 75 | 76 | Each 8192-byte CHR ROM bank consists of two 4096-byte half banks 77 | compressed with PB53. They are decoded in parallel because many 78 | games, especially those by Shiru, have many identical tiles across 79 | the two banks for use in CHR animation. 80 | 81 | * 1 byte: PRG bank holding compressed data 82 | * 2 bytes: Address of first half bank's data 83 | * 2 bytes: Address of second half bank's data 84 | 85 | Screenshots 86 | ----------- 87 | The screenshot directory: 88 | 89 | * 1 byte: PRG bank holding compressed data 90 | * 2 bytes: Address of screenshot 91 | 92 | Each screenshot converted with `form_screenshot()` in 93 | `a53screenshot.py` consists of a 13-byte header followed by 94 | a block of Donut compressed tile data. 95 | 96 | * 3 bytes: Colors used for color set 0 97 | * 3 bytes: Colors used for color set 1 98 | * 7 bytes: 64x56-bit bitmap of which color set each tile uses 99 | 100 | Tiles have 3 bits per pixel and are stored in 14 groups of four 101 | tiles, each of which decompresses to 128 bytes (two Donut blocks). 102 | Each group consists of planes 0 and 1 for four tiles (64 bytes) 103 | followed by plane 2 for all four tiles (32 bytes) and 32 zero bytes. 104 | A 0 in plane 2 means this pixel is gray (0, 1, 2, or 3 meaning black, 105 | dark gray, light gray, or white); 1 means it uses a color from the 106 | tile's color set. 107 | 108 | Activities 109 | ---------- 110 | Each activity has a 32-byte record: 111 | 112 | * 1 byte: Starting PRG bank number 113 | * 1 byte: Starting CHR bank number 114 | * 1 byte: Screenshot directory index 115 | * 1 byte: Year of first publication minus 1970 (48 means 2018) 116 | * 1 byte: Number of players type 117 | * 1 byte: Number of CHR banks 118 | * 2 bytes: Unused 119 | * 2 bytes: Offset in activity names to start of title and author 120 | * 2 bytes: Offset in descriptions to start of description 121 | * 2 bytes: Reset vector 122 | * 1 byte: Mapper configuration 123 | * 1 byte: Unused 124 | * 16 bytes: Music player garbage 125 | 126 | The total number of activities is the sum of the number of activities 127 | on all pages. 128 | 129 | Mapper configuration consits of a bit field with four members. 130 | The meaning of bits 3-0 is similar to that of MMC1. 131 | 132 | 7654 3210 133 | | || ||++- Nametable arrangement 134 | | || || 0: AAAA (single screen) 135 | | || || 2: ABAB (horizontal arrangement or vertical mirroring) 136 | | || || 3: AABB (vertical arrangement or horizontal mirroring) 137 | | || ++--- PRG ROM bank style 138 | | || 0: 32 KiB banks 139 | | || 2: 16 KiB banks, $8000-$BFFF fixed to first bank 140 | | || 3: 16 KiB banks, $C000-$FFFF fixed to last bank 141 | | ++------ PRG ROM size 142 | | 0: 32 KiB; 1: 64 KiB; 2: 128 KiB; 3: 256 KiB 143 | +--------- Starting register 144 | 0: CHR ROM bank (CNROM); 1: PRG ROM bank (other) 145 | 146 | Page directory 147 | -------------- 148 | The menu is divided into several pages. 149 | 150 | * 1 byte: Number of pages 151 | * n bytes: For each page, the last index of activities plus 1. 152 | * NUL-terminated string: Title of first page 153 | * NUL-terminated string: Title of second page 154 | * etc. 155 | 156 | The first page is assumed to start at index 0, 157 | subsequent pages start at n-1 of "last index" list. 158 | 159 | The encoding of page titles and all other text in Action 53 160 | is an ASCII superset defined by `a53charset.py`. 161 | 162 | Activity names 163 | -------------- 164 | Each activity's name consists of a title, a newline ($0A), the name 165 | of the author, and a NUL terminator ($00). To find the address of an 166 | activity name, add the offset in the activity directory to the start 167 | address of the activity names block. 168 | 169 | The title can be up to 128 pixels long as defined in `vwf7.png`, 170 | or about 28 characters. Because the year of first publication 171 | precedes the author's name, it can be only 101 pixels long. 172 | 173 | Descriptions 174 | ------------ 175 | Each activity's description is up to 16 lines of up to 128 pixels. 176 | As with title and author, $0A separates lines, and $00 ends the 177 | description. Addresses of descriptions are calculated the same way 178 | as title addresses, except that the description block can be stored 179 | in a PRG bank other than the last. 180 | 181 | Unlike title and author, descriptions may be compressed using digram 182 | tree encoding (DTE) using a replacement table of up to 256 bytes. 183 | Decompression replaces each byte of compressed text from $80 through 184 | $FF with the corresponding pair of bytes in the replacement table. 185 | The "tree" in DTE means that the replacement table is recursive: 186 | entries refer to literal code units or to previous entries. 187 | (If `DTE_MIN_CODEUNIT` exceeds 128, decompression ignores the first 188 | `DTE_MIN_CODEUNIT - 128` entries of the table and instead copies 189 | DTE bytes from $80 through `DTE_MIN_CODEUNIT - 1` literally.) 190 | 191 | Title screen 192 | ------------- 193 | This is similar to the "sb53" format also used in the NES port of 194 | 240p Test Suite, except that PB53 is replaced with Donut. 195 | 196 | * 1 byte: Number of distinct tiles, with $00 meaning 256 197 | * Variable: Donut compressed tile data 198 | * Variable: Donut compressed nametable; decompresses to 960 199 | bytes of tilemap and 64 bytes of color attributes 200 | * 16 bytes: Palette 201 | 202 | Title strings 203 | ------------- 204 | Text drawn on the title screen as text compresses better than 205 | text drawn as pixels into the sb53 data. This is used for 206 | gift messages, copyright notices, and the like. 207 | 208 | * 1 byte: Color code (bits 7-5) and Y position in tiles (bits 4-0) 209 | * 1 byte: X position (in pixels) 210 | * NUL-terminated string 211 | 212 | Y positions 0-2 are above the 80% safe area on NTSC, and 27-29 213 | are below it. Y positions 30 and 31 are invalid. A byte with 214 | such a position, such as $FF, terminates the list. 215 | 216 | Color codes are all combinations of 2 colors where the foreground 217 | color is 1 bit different from the background color. 218 | 219 | * $00: 1 on 0 220 | * $20: 3 on 2 221 | * $40: 0 on 1 222 | * $60: 2 on 3 223 | * $80: 2 on 0 224 | * $A0: 3 on 1 225 | * $C0: 0 on 2 226 | * $E0: 1 on 3 227 | 228 | This can also be interpreted as a bit field: 229 | 230 | 7654 3210 231 | |||+-++++- Y position 232 | ||+------- Value of plane not containing text. 0: $00; 1: $FF 233 | |+-------- 1: Invert plane containing text 234 | +--------- Bit plane containing text. 0: bit 0; 1: bit 1 235 | 236 | Credits 237 | ------- 238 | This document is under the same license as the Action 53 builder: 239 | 240 | Copyright 2018 Damian Yerrick 241 | 242 | Copying and distribution of this file, with or without 243 | modification, are permitted in any medium without royalty provided 244 | the copyright notice and this notice are preserved in all source 245 | code copies. This file is offered as-is, without any warranty. 246 | --------------------------------------------------------------------------------