├── .gitignore ├── LICENSE ├── README.md ├── core.mrc ├── debug.mrc ├── mem.mrc ├── ops.ini ├── ops.mrc ├── ppu.mrc └── rom.mrc /.gitignore: -------------------------------------------------------------------------------- 1 | /ROMs -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lynn Drumm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mNES 2 | NES emulator in mIRCscript 3 | 4 | ![image](https://github.com/LynnDrumm/mNES/assets/80856352/cff090ab-e14c-46e6-a0ed-bccdd5f9d22d) 5 | 6 | this is a learning experience for me. Figuring things out as I go. I don't know what I'm doing, please keep that in mind if you read the source. 7 | 8 | only 51 opcodes are (probably poorly) emulated so far, don't expect much to run (yet), but feel free to look around and give me feedback ^-^ 9 | 10 | 11 | # how to use: 12 | 13 | put a folder named ROMs in the same directory as all the files, and put a valid NES rom there. 14 | currently, it just picks the first file it finds in there. 15 | 16 | then you can load the script(s) with `/load -rs path\to\core.mrc`, followed by `/nes.init` to run it. It will not show (much) output by default. 17 | if you want the pretty CPU output (about the only interesting thing to look at currently), `/nes.init full` will accomplish that, or `/nes.init error` if you (mostly) just want to show errors. 18 | 19 | You can also manually turn on the output while the emulator runs by typing `/hadd nes.cpu debug full`, or you can set it to `error` to only show output when an error occurs. You can manipulate the status registers (`nes.cpu status.xxxxx`), accumulator, x, y, program counter, and so on the same way, if you like. 20 | 21 | `/nes.init` will always cleanly start the emulator, `/nes.cpu.start [n]` will start the emulator from the last known state, where `n` is the amount of miliseconds to wait between each cpu cycle. `/nes.cpu.stop` will stop the emulator (and is (or should be) automatically triggered when an error occurs). 22 | 23 | Somewhat compatible with Adiirc 24 | -------------------------------------------------------------------------------- /core.mrc: -------------------------------------------------------------------------------- 1 | ;; mIRC NES emulator 2 | 3 | ;; da main loop! 4 | alias nes.cpu.loop { 5 | 6 | ;; make RAM available as binvar 7 | noop $hget(nes.mem, ram, &RAM) 8 | 9 | ;; loop "forever". we'll break out at regular intervals 10 | ;; and call this again with a timer, but hopefully this 11 | ;; should be a little bit faster than a pure timer loop? 12 | while ($true) { 13 | 14 | ;; instruction profiling start 15 | hadd nes.cpu ticks.instruction $ticksqpc 16 | 17 | ;; increment the current program counter 18 | hinc nes.cpu programCounter 19 | 20 | ;; assign it to %pc so it stays persistent for debug output 21 | var %pc $hget(nes.cpu, programCounter) 22 | 23 | ;; get opcode byte (in hex) at program counter's address 24 | var %opcode $hex($bvar(&RAM, $hget(nes.cpu, programCounter))) 25 | 26 | ;; get mnemonic, instruction length (bytes), and mode 27 | tokenize 32 $hget(nes.cpu.opcode, %opcode) 28 | 29 | var %mnemonic $1 30 | var %length $2 31 | var %mode $3 32 | 33 | ;; set %operand to the next 1-2 bytes. will be ignored if mode is implicit. 34 | var %operand $bvar(&RAM, $calc($hget(nes.cpu, programCounter) + 1), $calc(%length - 1)) 35 | 36 | ;; increment the program counter by operand length. 37 | ;; this must be done BEFORE executing the instruction. 38 | hinc nes.cpu programCounter $calc(%length - 1) 39 | 40 | ;; execute the instruction 41 | ;; currently we're dynamically calling individual instructions 42 | ;; based on mnemonic name (and using if/then conditioning for 43 | ;; different modes), because I suspect it might be faster, 44 | ;; however, if it turns out to be more practical to have one 45 | ;; big function and put the mnemonics in if/then blocks too, 46 | ;; we can always switch to that later. 47 | nes.cpu.mnemonic. $+ [ %mnemonic ] %length %mode %operand 48 | 49 | ;; show pretty output 50 | ;; if we gott a return value, it's in $result 51 | nes.debug.cpu %pc %opcode %length %mode %mnemonic %operand $result %ticks 52 | 53 | ;; just count single cycles for now 54 | hinc nes.cpu cycles 55 | 56 | if ($hget(nes.cpu, cyclesPerTimer) // $hget(nes.cpu, cycles)) { 57 | 58 | break 59 | } 60 | } 61 | 62 | ;; lmfao i didn't notice for DAYS that I was never actually writing &RAM out, 63 | ;; so i was just loading in a fresh copy of it as set during init on 64 | ;; every 65 | ;; single 66 | ;; cycle. 67 | ;; ...anyway. fixed now >.>; 68 | hadd -b nes.mem RAM &RAM 69 | 70 | ;; end of cpu loop 71 | return 72 | 73 | ;; if something goes wrong, halt the cpu emulation 74 | :error 75 | nes.debug.cpu $hget(nes.cpu, programCounter) %opcode 0 76 | nes.cpu.stop 77 | halt 78 | } 79 | 80 | ;; resumes CPU from last stop point. $1 is optional timer interval 81 | alias nes.cpu.start { 82 | 83 | ;; reload opcode table -- just for testing. 84 | nes.cpu.loadOpcodeTable 85 | 86 | ;; get cycle delay value 87 | var %cycleDelay $1 88 | 89 | ;; number of cycles to run per timer interval. 90 | ;; if mIRC locks up too much, adjust this down. 91 | ;; anything 100 or less is a sane value, above that it gets 92 | ;; *really* impractical as this also affects keyboard input. 93 | var %cyclesPerInterval 50 94 | 95 | ;; if cycleDelay is set at all, cycles per interval is always 1. 96 | hadd nes.cpu cyclesPerTimer $iif(%cycleDelay, 1, %cyclesPerInterval) 97 | 98 | ;; start cpu loop 99 | iline @nes.debug $line(@nes.debug, 0) resuming cpu 100 | .timernes.cpu.loop -h 0 $iif(%cycleDelay, %cycleDelay, 0) nes.cpu.loop 101 | 102 | ;; start instructions-per-second timer 103 | hadd nes.cpu ips.last 0 104 | .timernes.ips.loop 0 1 nes.ips.calc 105 | 106 | ;; benchmark start 107 | set %debug.ticks.start $ticks 108 | } 109 | 110 | alias nes.cpu.stop { 111 | 112 | .timernes.cpu.loop off 113 | .timernes.ips.loop off 114 | 115 | iline @nes.debug $line(@nes.debug, 0) cpu loop stopped. 116 | 117 | ;; benchmark output 118 | echo @nes.debug execution took $calc(($ticks - %debug.ticks.start) / 1000) seconds. 119 | unset %debug.ticks.start 120 | 121 | halt 122 | } 123 | 124 | alias nes.cpu.step { 125 | 126 | nes.cpu.loop 127 | } 128 | 129 | alias nes.ips.calc { 130 | 131 | var %last $hget(nes.cpu, ips.last) 132 | var %current $hget(nes.cpu, cycles) 133 | 134 | ;echo -s ips: $calc(%current - %last) 135 | 136 | hadd nes.cpu ips $calc(%current - %last) 137 | hadd nes.cpu ips.last %current 138 | } 139 | 140 | ;; get 6502 status flags as a single byte, represented in binary 141 | alias nes.cpu.statusFlags { 142 | 143 | ;; from most to least significant (byte 7 - 0) 144 | var %N $hget(nes.cpu, status.negative) 145 | var %V $hget(nes.cpu, status.overflow) 146 | 147 | ;; bits 5 and 4 are not used and always 0 148 | ;; -- i was wrong, they absolutely get used, 149 | ;; just not when programming FOR the cpu. 150 | ;; ...usually anyway. it's complicated. 151 | 152 | var %5 $hget(nes.cpu, status.5) 153 | var %B $hget(nes.cpu, status.break) 154 | 155 | var %D $hget(nes.cpu, status.decimal) 156 | var %I $hget(nes.cpu, status.interrupt) 157 | var %Z $hget(nes.cpu, status.zero) 158 | var %C $hget(nes.cpu, status.carry) 159 | 160 | ;; merge 'em all together and return the result! 161 | return $+(%N,%V,%5,%B,%D,%I,%Z,%C) 162 | } 163 | 164 | alias nes.cpu.loadOpcodeTable { 165 | 166 | if ($hget(nes.cpu.opcode) != $null) { 167 | 168 | hfree nes.cpu.opcode 169 | } 170 | 171 | hmake nes.cpu.opcode 256 172 | 173 | ;; file containing list of all ops 174 | var %file $scriptdir $+ ops.ini 175 | hload -i nes.cpu.opcode $qt(%file) opcodes 176 | 177 | iline @nes.debug $line(@nes.debug, 0) >> opcode table loaded. 178 | } 179 | 180 | alias nes.init { 181 | 182 | ;; open our own window 183 | ;; add a side listbox to display stack and other info 184 | window -el16 @nes.debug 185 | 186 | echo @nes.debug ------------------------------------- 187 | echo @nes.debug mNES v0.4 188 | echo @nes.debug (c) Lynn Drumm 2023 - 2025 189 | echo @nes.debug All rights and/or wrongs reserved. 190 | echo @nes.debug ------------------------------------- 191 | 192 | ;; create hash table for global storage 193 | if ($hget(nes.data) != $null) { 194 | 195 | hfree nes.data 196 | } 197 | 198 | hmake nes.data 10 199 | 200 | ;; load other scripts 201 | var %i 1 202 | var %scripts ops ppu mem rom debug 203 | 204 | while (%i <= $numtok(%scripts, 32)) { 205 | 206 | var %file $gettok(%scripts, %i, 32) $+ .mrc 207 | echo @nes.debug loading %file 208 | .load -rs $scriptdir $+ %file 209 | inc %i 210 | } 211 | 212 | ;; generate the opcode table 213 | nes.cpu.loadOpcodeTable 214 | 215 | ;; initialise memory 216 | nes.mem.init 217 | 218 | ;; load rom and parse header 219 | nes.rom.load 220 | 221 | ;; initialise the PPU 222 | nes.ppu.init 223 | 224 | ;; save RAM 225 | hadd -b nes.mem RAM &RAM 226 | 227 | ;; create table for 6502 registers and state, and set initial values 228 | if ($hget(nes.cpu) != $null) { 229 | 230 | hfree nes.cpu 231 | } 232 | 233 | hmake nes.cpu 10 234 | 235 | ;; address space is 64k, or $FFFF. 236 | ;; our ROM maps to $8000, and is 16k (PRG) + 8k (CHR) 237 | 238 | ;; big revelation: the NES has _two_ address busses connected to the catridge, 239 | ;; one for CPU (which is PRG), and one for PPU (which is CHR) 240 | 241 | ;; the program counter should be initialised by 242 | ;; reading whatever address is stored at $FFFC and then 243 | ;; jump there and start execution. 244 | var %startAddressLo $hex($bvar(&RAM, $dec(FFFC))) 245 | var %startAddressHi $hex($bvar(&RAM, $dec(FFFD))) 246 | 247 | ;; we decrease it by 1 so it's in the correct position when the cpu loop starts 248 | hadd nes.cpu programCounter $calc($dec($+(%startAddressHi,%startAddressLo)) - 1) 249 | 250 | ;; 8 bits indicating the status of the CPU. maybe we should 251 | ;; split this up into separate values instead of emulating 252 | ;; a byte. i don't know what's most practical yet. 253 | 254 | ;; the flags are: Negative, Overflow, n/a, n/a, Decimal, 255 | ;; Interrupt Disable, Zero, and Carry 256 | 257 | ;; NVsBDIZC 258 | ;hadd nes.cpu status 00000100 259 | 260 | ;; ok, we forgot about bits 4 and 5. 261 | ;; these are handled differently, so, maybe separating things 262 | ;; out like this was not the best idea, and maybe we should 263 | ;; write a function to handle this stuff instead. 264 | 265 | ;; writing a function to handle it ended up being more of a 266 | ;; pain than solution, so we'll leave it for now. 267 | 268 | ;; bit 5 has no name. i don't like that but it's how it is. 269 | 270 | hadd nes.cpu status.negative 0 271 | hadd nes.cpu status.overflow 0 272 | hadd nes.cpu status.5 0 273 | hadd nes.cpu status.break 0 274 | hadd nes.cpu status.decimal 0 275 | hadd nes.cpu status.interrupt 1 276 | hadd nes.cpu status.zero 0 277 | hadd nes.cpu status.carry 0 278 | 279 | ;; set the accumulator and general purpose x/y registers 280 | ;; to zero, it doesn't matter. 281 | hadd nes.cpu accumulator 0 282 | hadd nes.cpu x 0 283 | hadd nes.cpu y 0 284 | 285 | ;; just counting single cycles for now 286 | hadd nes.cpu cycles 0 287 | 288 | ;; "high resolution" profiling 289 | hadd nes.cpu ticks.start $ticksqpc 290 | 291 | ;; debug init 292 | nes.debug.init $1 293 | 294 | ;; start main cpu loop 295 | nes.cpu.start $iif($1 isnum, $1, $2) 296 | } 297 | 298 | ;; explicit hex -> decimal conversion 299 | alias -l dec { 300 | 301 | return $base($1, 16, 10) 302 | } 303 | 304 | ;; explicit decimal -> hex conversion 305 | alias -l hex { 306 | 307 | return $base($1, 10, 16, 2) 308 | } -------------------------------------------------------------------------------- /debug.mrc: -------------------------------------------------------------------------------- 1 | alias nes.debug.cpu { 2 | 3 | var %cycles $+(72,$padstring(6, $hget(nes.cpu, cycles)).pre) 4 | var %pc $+(41$,65,$base($1, 10, 16, 4)) 5 | var %opcode $+(4468,$2) 6 | var %length $3 7 | 8 | ;; instructions are at least 1 byte long. 9 | ;; if not, well, it's just not implemented yet! 10 | if (%length > 0) { 11 | 12 | if ($hget(nes.cpu, debug) == full) { 13 | 14 | var %mode $4 15 | var %mnemonic $5 16 | 17 | ;; special handling for how to display the operand/result depending on length/mode 18 | if (%mode == implicit) { 19 | 20 | ;; since implicit instructions have no operand or "result", 21 | var %result %operand 22 | } 23 | 24 | elseif (%mode == immediate) { 25 | 26 | ;; immediate mode means the operand is a single byte direct value, 27 | ;; to be prefixed with #$, not to be confused with zeropage, which 28 | ;; is also a single byte operand but is displayed as an 8-bit address. 29 | var %operand $+(57,$base($6, 10, 16, 2)) 30 | var %result $+(50#$,74,$base($6, 10, 16, 2)) 31 | 32 | } 33 | 34 | elseif (%mode == absolute) { 35 | 36 | ;; this is an address. for display purposes, swap them bytes again. 37 | var %operand $+(57,$base($6, 10, 16, 2)) $+(69,$base($7, 10, 16, 2)) 38 | var %result $+(50$,74,$+($hex($7),$hex($6))) 39 | } 40 | 41 | elseif (%mode == relative) { 42 | 43 | ;; if mode is relative, we'd rather display the result of the 44 | ;; instruction, so we can see where a branch ends up, 45 | ;; rather than the offset we may or not be adding/subtracting. 46 | 47 | ;; we're still keeping the original operand as well though, 48 | ;; just to keep things clear 49 | var %operand $+(57,$base($6, 10, 16, 2)) 50 | var %result $+(50$,74,$base($7, 10, 16, 4)) 51 | } 52 | 53 | elseif (%mode == zeropage) { 54 | 55 | ;; operands on single page operations are only 1 byte long. 56 | var %operand $+(57,$base($6, 10, 16, 2)) 57 | var %result $+(50$,74,$base($6, 10, 16, 2)) 58 | } 59 | 60 | elseif (%mode == indirect,y) { 61 | 62 | ;; this is an address. for display purposes, swap them bytes again. 63 | var %operand $+(57,$base($6, 10, 16, 2)) 64 | var %result $+(50$,74,$base($7, 10, 16, 4),50,$chr(44),74,$hex($hget(nes.cpu, y))) 65 | } 66 | 67 | ;; calculate n prettify execution time 68 | var %ticks $+(96,$calc($ticksqpc - $hget(nes.cpu, ticks.instruction)),94ms) 91/96 $hget(nes.cpu, ips) 69 | 70 | ;; prettify the status flag display 71 | var %flags $replace($nes.cpu.statusFlags, 0, $+(30,0), 1, $+(66,1)) 72 | 73 | var %regs 85 $padString(2, $hex($hget(nes.cpu, accumulator))) $padString(2, $hex($hget(nes.cpu, x))) $padString(2, $hex($hget(nes.cpu, y))) 74 | 75 | ;; the big line that put da stuff on screen~ 76 | ;; this is getting a bit unwieldy, lol 77 | iline @nes.debug $line(@nes.debug, 0) %cycles %pc 93: %opcode $padString(5, %operand) 93-> $+(71,%mnemonic) $padString(8, %result) $padString(10, %regs) $padString(11, $+(94,%mode)) $padString(10, %flags) %ticks 78 | ;echo @nes.debug %cycles %pc 93: %opcode $padString(5, %operand) 93-> $+(71,%mnemonic) $padString(6, %result) $padString(10, %regs) $padString(11, $+(94,%mode)) $padString(10, %flags) %ticks 79 | } 80 | } 81 | 82 | else { 83 | 84 | ;; print a warning if we encounter an unimplemented opcode 85 | echo @nes.debug %cycles %pc 93: %opcode $padString(5, %operand) 93-> 54,52 $+ /!\66,28 $+ $+($chr(160),unimplemented instruction,$chr(160),) $+(96,$calc($ticksqpc - $hget(nes.cpu, ticks.start)),94ms) 86 | ;echo -a stack dump: 87 | ;echo -a . $bvar(&RAM, $dec(0101), $dec(ff)) 88 | } 89 | } 90 | 91 | alias nes.debug.init { 92 | 93 | ;; debug output selector. 94 | ;; full shows everything, error only shows errors, none shows... none! 95 | ;; this is not completely implemented but it's a start 96 | hadd nes.cpu debug $iif($1, $1, error) 97 | 98 | if ($hget(nes.cpu, debug) == full) { 99 | 100 | ;; print debug header, twice. this is because our cpu output 101 | ;; gets printed right in between, so that it's easier to read 102 | ;; the output even if we're all the way at the end. 103 | echo @nes.debug $debugHeader 104 | echo @nes.debug $debugHeader 105 | } 106 | } 107 | 108 | alias nes.debug.stackDump { 109 | 110 | ;noop $hget(nes.mem, ram, &RAM) 111 | echo -a . $bvar(&RAM, $dec(0101), 256) 112 | } 113 | 114 | alias -l debugHeader { 115 | 116 | return 91---95cyl91-95pc91------95op91-95oprnd91----95mnm91-95result91----95A91--95X91--95Y91----95mode91--------95NVssDIZC91---95exec91--95ips91-- 117 | } 118 | 119 | ;; pad $2- up to $1 characters, using $chr(160) ((unicode space)) 120 | alias -l padString { 121 | 122 | var %stringLength $len($strip($2-)) 123 | var %newLength $1 124 | var %padLength $calc(%newLength - %stringLength) 125 | var %padding $str($chr(160),%padLength) 126 | 127 | return $iif($prop == pre, $+(%padding,$2-), $+($2-,%padding)) 128 | } 129 | 130 | 131 | ;; explicit hex -> decimal conversion 132 | alias -l dec { 133 | 134 | return $base($1, 16, 10) 135 | } 136 | 137 | ;; explicit decimal -> hex conversion 138 | alias -l hex { 139 | 140 | return $base($1, 10, 16, 2) 141 | } -------------------------------------------------------------------------------- /mem.mrc: -------------------------------------------------------------------------------- 1 | ;; okay, let's write a stack handler now!! :3 2 | 3 | ;; complete rewrite 4 | alias nes.mem.stack { 5 | 6 | ;; stack starts at $01FF and decreases down to $0100 7 | var %startAddress $base(01FF, 16, 10) 8 | 9 | ;; calculate the current stack address. 10 | ;; start - pointer 11 | var %stackAddress $calc(%startAddress - $hget(nes.mem, stackPointer)) 12 | 13 | var %mode $1 14 | 15 | if (%mode == push) { 16 | 17 | ;; check if we haven't reached stack overflow yet 18 | if ($hget(nes.mem, stackPointer) < 255) { 19 | 20 | ;; if pushing, there's a value. 21 | var %value $2 22 | 23 | ;; write value to the stack 24 | nes.mem.write %stackAddress %value 25 | 26 | ;; increment the stack pointer 27 | hinc nes.mem stackPointer 28 | } 29 | 30 | else { 31 | 32 | echo @nes.debug /!\66,28 $+ $+($chr(160),stack overflow,$chr(160),) $+(96,$calc($ticksqpc - $hget(nes.cpu, ticks.start)),94ms) 33 | nes.cpu.stop 34 | } 35 | } 36 | 37 | elseif (%mode == pop) { 38 | 39 | ;; read value from the current stack address + 1 40 | ;; this is what was wrong the whole time. we were reading from 41 | ;; the *next* stack address, rather than the last one we wrote to. 42 | 43 | ;; big thanks to zowie for talking me through debugging this on discord <3 44 | var %value $nes.mem.read($calc(%stackAddress + 1)) 45 | 46 | ;; decrease stack pointer 47 | hdec nes.mem stackPointer 48 | } 49 | 50 | echo -s . $time : stack $1 -> $base(%value, 10, 16) 51 | 52 | updateStackDisplay 53 | 54 | return %value 55 | } 56 | 57 | ;; since stupid. fuckin. starting at 1. 58 | ;; yeah. so. here's an alternative that does the extra math for us. 59 | ;; assumes decimal input. 60 | alias nes.mem.write { 61 | 62 | bset &RAM $calc($1 + 1) $2 63 | 64 | ;hadd nes.mem $1 $2 65 | } 66 | 67 | alias nes.mem.read { 68 | 69 | return $bvar(&RAM, $calc($1 + 1)) 70 | 71 | ;return $hget(nes.mem, $1) 72 | } 73 | 74 | alias nes.mem.init { 75 | 76 | 77 | ;; set up RAM. just fill it with zeroes first. 78 | echo @nes.debug setting up RAM 79 | 80 | if ($hget(nes.mem) != $null) { 81 | 82 | hfree nes.mem 83 | } 84 | 85 | ;; 64k RAM space, plus a lil extra for other stuff. 86 | hmake nes.mem $calc(64 * 128) 87 | 88 | bset &RAM $calc(64 * 1024) 0 89 | 90 | ;; stack init 91 | 92 | ;; stack is 256 bytes from $0100 - $01FF. 93 | ;; it starts at $01FF and is filled from there, backwards. 94 | hadd nes.mem stackPointer 0 95 | 96 | ;; update stack in listbox 97 | initStackDisplay 98 | updateStackDisplay 99 | } 100 | 101 | alias -l initStackDisplay { 102 | 103 | clear -l @nes.debug 104 | 105 | var %i 0 106 | 107 | while (%i < 256) { 108 | 109 | aline -l @nes.debug . 110 | inc %i 111 | } 112 | } 113 | 114 | alias -l updateStackDisplay { 115 | 116 | var %i 0 117 | 118 | while (%i < 256) { 119 | 120 | var %address $calc($base(0100, 16, 10) + %i) 121 | var %value $base($nes.mem.read(%address), 10, 16, 2) 122 | 123 | rline -l @nes.debug $calc(%i + 1) $+($,$base(%address, 10, 16, 4)) : %value 124 | inc %i 125 | } 126 | 127 | ;; select last item on the list to force scroll 128 | sline -l @nes.debug 256 129 | 130 | ;; select current stack address 131 | sline -l @nes.debug $calc(256 - $hget(nes.mem, stackPointer)) 132 | } -------------------------------------------------------------------------------- /ops.ini: -------------------------------------------------------------------------------- 1 | [opcodes] 2 | 00=BRK 1 implicit 3 | 0A=ASL 1 accumulator 4 | 06=ASL 2 zeropage 5 | 09=ORA 2 immediate 6 | 10=BPL 2 relative 7 | 18=CLC 1 implicit 8 | 20=JSR 3 absolute 9 | 25=AND 2 zeropage 10 | 29=AND 2 immediate 11 | 4A=LSR 1 accumulator 12 | 4C=JMP 3 absolute 13 | 48=PHA 1 implicit 14 | 60=RTS 1 implicit 15 | 65=ADC 2 zeropage 16 | 68=PLA 1 implicit 17 | 6A=ROR 1 accumulator 18 | 78=SEI 1 implicit 19 | 84=STY 2 zeropage 20 | 85=STA 2 zeropage 21 | 88=DEY 1 implied 22 | 8A=TXA 1 implicit 23 | 8C=STY 3 absolute 24 | 91=STA 2 indirect,y 25 | 98=TYA 1 implicit 26 | 8D=STA 3 absolute 27 | 8E=STX 3 absolute 28 | 90=BCC 2 relative 29 | 9A=TXS 1 implicit 30 | 9D=STA 3 absolute,x 31 | A0=LDY 2 immediate 32 | A2=LDX 2 immediate 33 | A5=LDA 2 zeropage 34 | A9=LDA 2 immediate 35 | AA=TAX 1 implicit 36 | AD=LDA 3 absolute 37 | AE=LDX 3 absolute 38 | B0=BCS 2 relative 39 | B1=LDA 2 indirect,y 40 | BD=LDA 3 absolute,x 41 | CA=DEX 1 implicit 42 | C2=NOP 2 immediate 43 | C6=DEC 2 zeropage 44 | C8=INY 1 implicit 45 | C9=CMP 2 immediate 46 | CA=DEX 1 implicit 47 | D0=BNE 2 relative 48 | D8=CLD 1 implicit 49 | E0=CPX 2 immediate 50 | E6=INC 2 zeropage 51 | E8=INX 1 implicit 52 | F0=BEQ 2 relative -------------------------------------------------------------------------------- /ops.mrc: -------------------------------------------------------------------------------- 1 | ;;set interrupt 2 | alias nes.cpu.mnemonic.sei { 3 | 4 | ;; SEI is always implict 5 | hadd nes.cpu status.interrupt 1 6 | } 7 | 8 | ;; clear decimal flag 9 | alias nes.cpu.mnemonic.cld { 10 | 11 | ;; CLD is always implicit 12 | hadd nes.cpu status.decimal 0 13 | } 14 | 15 | ;; clear carry flag 16 | alias nes.cpu.mnemonic.clc { 17 | 18 | ;; always implicit 19 | hadd nes.cpu status.carry 0 20 | } 21 | 22 | ;; load accumulator 23 | alias nes.cpu.mnemonic.lda { 24 | 25 | var %length $1 26 | var %mode $2 27 | var %operand $3- 28 | 29 | if (%mode == immediate) { 30 | 31 | var %result %operand 32 | } 33 | 34 | elseif (%mode == absolute) { 35 | 36 | var %result $nes.mem.read($mergeBytes(%operand)) 37 | } 38 | 39 | elseif (%mode == absolute,x) { 40 | 41 | var %address $calc($mergeBytes(%operand) + $hget(nes.cpu, x)) 42 | 43 | if (%address > $dec(ffff)) { 44 | 45 | var %address $calc(%address - $dec(ffff)) 46 | } 47 | 48 | var %result $nes.mem.read(%address) 49 | } 50 | 51 | elseif (%mode == zeropage) { 52 | 53 | var %result $nes.mem.read(%operand) 54 | } 55 | 56 | elseif (%mode == indirect,y) { 57 | 58 | ;; get lower byte of address from zeropage 59 | var %addressLower $hex($nes.mem.read(%operand)) 60 | 61 | ;; get higher byte of address from consecutive zeropage address 62 | ;; if crossing zeropage address ff, loop back around. 63 | if (%operand < 255) { 64 | 65 | var %addressHigher $hex($nes.mem.read(%operand + 1)) 66 | } 67 | 68 | else { 69 | 70 | var %addressHigher $hex($nes.mem.read(0)) 71 | } 72 | 73 | ;; combine higher/lower, add y index, that is our target address. 74 | var %address $calc($dec($+(%addressHigher,%addressLower)) + $hget(nes.cpu, y)) 75 | 76 | ;; loop back to $0000 if crossing $ffff 77 | if (%address > $hex(ffff)) { 78 | 79 | var %address $calc(%address - $hex(ffff)) 80 | } 81 | 82 | ;; get value from address 83 | var %result $nes.mem.read(%address) 84 | } 85 | 86 | ;; store result in accumulator 87 | hadd nes.cpu accumulator %result 88 | 89 | setFlag zero $hget(nes.cpu, accumulator) 90 | setFlag negative $hget(nes.cpu, accumulator) 91 | 92 | return %result 93 | } 94 | 95 | ;; store accumulator 96 | alias nes.cpu.mnemonic.sta { 97 | 98 | var %length $1 99 | var %mode $2 100 | var %operand $3- 101 | 102 | if (%mode == absolute) { 103 | 104 | var %address $mergeBytes(%operand) 105 | } 106 | 107 | elseif (%mode == indirect,y) { 108 | 109 | ;; get lower byte of address from zeropage 110 | var %addressLower $hex($nes.mem.read(%operand)) 111 | 112 | ;; get higher byte of address from consecutive zeropage address 113 | ;; if crossing zeropage address ff, loop back around. 114 | if (%operand < 255) { 115 | 116 | var %addressHigher $hex($nes.mem.read(%operand + 1)) 117 | } 118 | 119 | else { 120 | 121 | var %addressHigher $hex($nes.mem.read(0)) 122 | } 123 | 124 | ;; combine higher/lower, add y index, that is our target address. 125 | var %address $calc($dec($+(%addressHigher,%addressLower)) + $hget(nes.cpu, y)) 126 | 127 | ;; loop back to $0000 if crossing $ffff 128 | if (%address > $hex(ffff)) { 129 | 130 | var %address $calc(%address - $hex(ffff)) 131 | } 132 | } 133 | 134 | elseif (%mode == zeropage) { 135 | 136 | var %address %operand 137 | } 138 | 139 | elseif (%mode == absolute,x) { 140 | 141 | var %address $calc($mergeBytes(%operand) + $hget(nes.cpu, x)) 142 | 143 | ;; handle overflow 144 | if (%address > $dec(ffff)) { 145 | 146 | var %address $calc(%address - $dec(ffff)) 147 | } 148 | } 149 | 150 | nes.mem.write %address $hget(nes.cpu, accumulator) 151 | 152 | return %address 153 | } 154 | 155 | ;; push accumulator to stack 156 | alias nes.cpu.mnemonic.pha { 157 | 158 | nes.mem.stack push $hget(nes.cpu, accumulator) 159 | } 160 | 161 | ;; pull accu from stack 162 | alias nes.cpu.mnemonic.pla { 163 | 164 | hadd nes.cpu accumulator $nes.mem.stack(pop) 165 | 166 | setFlag zero $hget(nes.cpu, accumulator) 167 | setFlag negative $hget(nes.cpu, accumulator) 168 | } 169 | 170 | ;; LoaD X index with memory 171 | alias nes.cpu.mnemonic.ldx { 172 | 173 | var %length $1 174 | var %mode $2 175 | var %operand $3 176 | 177 | if (%mode == immediate) { 178 | 179 | var %result %operand 180 | } 181 | 182 | elseif (%mode == absolute) { 183 | 184 | var %address $mergeBytes(%operand) 185 | var %result $nes.mem.read(%address) 186 | } 187 | 188 | ;; set x register to result 189 | hadd nes.cpu x %result 190 | 191 | setFlag zero %result 192 | setFlag negative %result 193 | 194 | return %result 195 | } 196 | 197 | alias nes.cpu.mnemonic.stx { 198 | 199 | var %mode $2 200 | var %operand $3 201 | 202 | if (%mode == absolute) { 203 | 204 | var %address $mergeBytes(%operand) 205 | } 206 | 207 | nes.mem.write %address $hget(nes.cpu, x) 208 | 209 | return %address 210 | } 211 | 212 | ;; transfer X to accumulator 213 | alias nes.cpu.mnemonic.txa { 214 | 215 | var %value $hget(nes.cpu, x) 216 | 217 | ;; write contents of x to accumulator 218 | hadd nes.cpu accumulator %value 219 | 220 | setFlag zero $hget(nes.cpu, accumulator) 221 | setFlag negative $hget(nes.cpu, accumulator) 222 | } 223 | 224 | ;; transfer accumulator to X 225 | alias nes.cpu.mnemonic.tax { 226 | 227 | var %value $hget(nes.cpu, accumulator) 228 | 229 | ;; write contents of accumulator to x 230 | hadd nes.cpu x %value 231 | 232 | setFlag zero $hget(nes.cpu, accumulator) 233 | setFlag negative $hget(nes.cpu, accumulator) 234 | } 235 | 236 | ;;Transfer X to StackPointer 237 | alias nes.cpu.mnemonic.txs { 238 | 239 | ;; i was so wrong about this. I thought this pushed 240 | ;; the current value of X onto the stack, but all it 241 | ;; does is set the stack pointer of whatever value is 242 | ;; in the x register. this has been wrong for weeks. 243 | ;; end me 😭 244 | hadd nes.cpu StackPointer $hget(nes.cpu, x) 245 | } 246 | 247 | ;;compare X to memory 248 | alias nes.cpu.mnemonic.cpx { 249 | 250 | var %mode $2 251 | var %operand $3 252 | 253 | 254 | if (%mode == immediate) { 255 | 256 | var %value %operand 257 | } 258 | 259 | var %result $calc($hget(nes.cpu, x) - %value) 260 | 261 | if (%result < 0) { 262 | 263 | var %result $calc(%result + 255) 264 | } 265 | 266 | ;; set zero flag on "equal" comparison (i.e., both accu and 267 | ;; operand are the same value and result is 0) 268 | setFlag zero %result 269 | ;; negative flag is set if bit 7 is set 270 | setFlag negative %result 271 | 272 | ;; carry is set if value is <= to accumulator, 273 | ;; reset if greater than. 274 | ;; hardcoded for now until I'm sure I can re-use 275 | ;; this code for other instructions 276 | if (%result <= $hget(nes.cpu, x)) { 277 | 278 | hadd nes.cpu status.carry 1 279 | } 280 | 281 | else { 282 | 283 | hadd nes.cpu status.carry 0 284 | } 285 | } 286 | 287 | ;; load y index with memory 288 | alias nes.cpu.mnemonic.ldy { 289 | 290 | var %length $1 291 | var %mode $2 292 | var %operand $3 293 | 294 | if (%mode == immediate) { 295 | 296 | var %result %operand 297 | } 298 | 299 | ;; set y register to result 300 | hadd nes.cpu y %result 301 | 302 | setFlag zero $hget(nes.cpu, y) 303 | setFlag negative $hget(nes.cpu, y) 304 | 305 | return %result 306 | } 307 | 308 | ;; store value of y to location 309 | alias nes.cpu.mnemonic.sty { 310 | 311 | var %length $1 312 | var %mode $2 313 | var %operand $3 314 | 315 | if (%mode == zeropage) { 316 | 317 | ;; no need to merge anything, 318 | ;; since zeropage addresses are 1 byte. 319 | var %address %operand 320 | } 321 | 322 | elseif (%mode == absolute) { 323 | 324 | var %address $mergeBytes(%operand) 325 | } 326 | 327 | ;; store the value of y at the given address. 328 | nes.mem.write %address $hget(nes.cpu, y) 329 | 330 | return %address 331 | } 332 | 333 | ;; increment y register 334 | alias nes.cpu.mnemonic.iny { 335 | 336 | hinc nes.cpu y 337 | 338 | ;; if y is now less than 0, roll back over to $ff 339 | if ($hget(nes.cpu, y) > 255) { 340 | 341 | hadd nes.cpu y 0 342 | } 343 | 344 | setFlag zero $hget(nes.cpu, y) 345 | setFlag negative $hget(nes.cpu, y) 346 | } 347 | 348 | ;; decrement y register 349 | alias nes.cpu.mnemonic.dey { 350 | 351 | hdec nes.cpu y 352 | 353 | ;; if y is now less than 0, roll back over to $ff 354 | if ($hget(nes.cpu, y) < 0) { 355 | 356 | hadd nes.cpu y $dec(ff) 357 | } 358 | 359 | setFlag zero $hget(nes.cpu, y) 360 | setFlag negative $hget(nes.cpu, y) 361 | } 362 | 363 | ;; increment x register 364 | alias nes.cpu.mnemonic.inx { 365 | 366 | hinc nes.cpu x 367 | 368 | ;; if x is now less than 0, roll back over to $ff 369 | if ($hget(nes.cpu, x) > 255) { 370 | 371 | hadd nes.cpu x 0 372 | } 373 | 374 | setFlag zero $hget(nes.cpu, x) 375 | setFlag negative $hget(nes.cpu, x) 376 | } 377 | 378 | ;; decrement x register 379 | alias nes.cpu.mnemonic.dex { 380 | 381 | hdec nes.cpu x 382 | 383 | if ($hget(nes.cpu, x) < 0) { 384 | 385 | hadd nes.cpu x $dec(ff) 386 | } 387 | 388 | setFlag zero $hget(nes.cpu, x) 389 | setFlag negative $hget(nes.cpu, x) 390 | } 391 | 392 | ;; transfer y to accumulator 393 | alias nes.cpu.mnemonic.tya { 394 | 395 | var %value $hget(nes.cpu, y) 396 | 397 | hadd nes.cpu accumulator %value 398 | 399 | setFlag zero $hget(nes.cpu, y) 400 | setFlag negative $hget(nes.cpu, y) 401 | } 402 | 403 | alias nes.cpu.mnemonic.sty { 404 | 405 | var %mode $2 406 | var %operand $3 407 | 408 | if (%mode == absolute) { 409 | 410 | var %address $mergeBytes(%operand) 411 | } 412 | 413 | nes.mem.write %address $hget(nes.cpu, y) 414 | 415 | return %address 416 | } 417 | 418 | ;; Logical AND memory with accumulator 419 | alias nes.cpu.mnemonic.and { 420 | 421 | var %length $1 422 | var %mode $2 423 | var %operand $3 424 | 425 | var %accumulator $hget(nes.cpu, accumulator) 426 | 427 | if (%mode == immediate) { 428 | 429 | var %value %operand 430 | 431 | } 432 | 433 | elseif (%mode == zeropage) { 434 | 435 | ;; get value from zeropage 436 | var %value $nes.mem.read(%operand) 437 | } 438 | 439 | ;; logical AND between operand and accumulator 440 | ;; 6502.org says bit by bit, so... let's try that? 441 | 442 | var %i 0 443 | 444 | while (%i < 8) { 445 | 446 | var %and $and($getBit(%value, %i), $getBit(%accumulator, %i)) 447 | 448 | ;echo -s > %a AND %b = %and 449 | 450 | var %result $+(%and,%result) 451 | inc %i 452 | } 453 | 454 | ;; not entirely convinced the above does anything different 455 | ;; than just straight using $and() on the operand/accu, 456 | ;; so uncomment this line to double check in the future. 457 | ;echo -s . %operand AND %accumulator = $and(%operand, %accumulator) 458 | 459 | ;; convert result back to decimal 460 | ;echo -s b: %result d: $dec(%result).bin h: $hex($dec(%result).bin) 461 | var %result $dec(%result).bin 462 | 463 | ;; push the result to the accumulator 464 | hadd nes.cpu accumulator %result 465 | 466 | setFlag zero $hget(nes.cpu, accumulator) 467 | setFlag negative $hget(nes.cpu, accumulator) 468 | } 469 | 470 | ;; compare contents of accumulator with another value 471 | alias nes.cpu.mnemonic.cmp { 472 | 473 | var %mode $2 474 | var %operand $3 475 | 476 | if (%mode == immediate) { 477 | 478 | var %value %operand 479 | } 480 | 481 | var %result $calc($hget(nes.cpu, accumulator) - %value) 482 | 483 | ;; set zero flag on "equal" comparison (i.e., both accu and 484 | ;; operand are the same value and result is 0) 485 | setFlag zero %result 486 | ;; negative flag is set if bit 7 is set 487 | setFlag negative %result 488 | 489 | ;; carry is set if value is <= to accumulator, 490 | ;; reset if greater than. 491 | ;; hardcoded for now until I'm sure I can re-use 492 | ;; this code for other instructions 493 | if (%result <= $hget(nes.cpu, accumulator)) { 494 | 495 | hadd nes.cpu status.carry 1 496 | } 497 | 498 | else { 499 | 500 | hadd nes.cpu status.carry 0 501 | } 502 | } 503 | 504 | ;; add with carry 505 | alias nes.cpu.mnemonic.adc { 506 | 507 | var %mode $2 508 | var %operand $3- 509 | var %accumulator $hget(nes.cpu, accumulator) 510 | 511 | if (%mode == zeropage) { 512 | 513 | var %value $nes.mem.read(%operand) 514 | } 515 | 516 | ;; i'm so incredibly grateful for everyone who has patiently 517 | ;; helped me and explained stuff ❤ special shoutout to 518 | ;; TheMogMiner for this one: 519 | 520 | ;; add accumulator + value... plus the carry bit? somehow? 521 | ;; ...maybe i don't understand just yet... 522 | var %value $calc(%value + $hget(nes.cpu, status.carry)) 523 | var %result $calc(%accumulator + %value) 524 | 525 | ;; if >255, loop around, and set the carry flag 526 | if (%result > 255) { 527 | 528 | ;; loop result around 529 | var %result $calc(%result - 255) 530 | 531 | ;; set carry flag 532 | hadd nes.cpu status.carry 1 533 | } 534 | 535 | else { 536 | 537 | ;; always gotta unset things if any condition 538 | ;; that sets a flag isn't met. 539 | hadd nes.cpu status.carry 0 540 | } 541 | 542 | ;; compare bit 7 of accu and value. 543 | if ($getBit(%accumulator, 7) == $getBit(%value, 7)) { 544 | 545 | ;; if identical, we check if bit 7 of the result is different 546 | if ($getBit(%accumulator, 7) != $getBit(%result, 7)) { 547 | 548 | ;; set overflow flag if bit 7 of accu and result are different 549 | hadd nes.cpu status.overflow 1 550 | } 551 | 552 | else { 553 | 554 | hadd nes.cpu status.overflow 0 555 | } 556 | } 557 | 558 | else { 559 | 560 | hadd nes.cpu status.overflow 0 561 | } 562 | 563 | setFlag negative 564 | } 565 | 566 | ;; ROtate Right 567 | alias nes.cpu.mnemonic.ror { 568 | 569 | var %mode $2 570 | 571 | if (%mode == accumulator) { 572 | 573 | var %value $hget(nes.cpu, accumulator) 574 | } 575 | 576 | ;; get bit 0 577 | var %bit0 $getbit(%value, 0) 578 | 579 | ;; get carry 580 | var %carry $hget(nes.cpu, status.carry) 581 | 582 | ;; result is all bits shifted 1 place right, 583 | ;; bit 7 is set to current value of carry. 584 | var %result $dec($+(%carry,$left(%value, 7))).bin 585 | 586 | ;; set N flag to "input carry". like this? 587 | hadd nes.cpu status.negative $hget(nes.cpu, status.carry) 588 | ;; set carry to previous value of bit 0 589 | hadd nes.cpu status.carry %bit0 590 | 591 | if (%result == 0) { 592 | 593 | hadd nes.cpu status.zero 1 594 | } 595 | 596 | else { 597 | 598 | hadd nes.cpu status.zero 0 599 | } 600 | 601 | } 602 | 603 | ;; logical shift right 604 | alias nes.cpu.mnemonic.lsr { 605 | 606 | var %mode $2 607 | var %operand $3- 608 | 609 | ;; lowest bit is shifted into the carry flag, 610 | ;; highest bit is set to 0 611 | if (%mode == accumulator) { 612 | 613 | var %value $bin($hget(nes.cpu, accumulator)) 614 | } 615 | 616 | ;; get the bit to shift into the carry flag 617 | var %carry $right($bin(%value), 1) 618 | 619 | ;; get leftmost 7 bits (strip lowest bit), 620 | ;; and stick 0 in front of it. 621 | ;; yeaaaaaaaah 622 | var %result $dec($+(0,$left(%value, 7))).bin 623 | 624 | ;; i can't think of a better way to do this right now 625 | if (%mode == accumulator) { 626 | 627 | hadd nes.cpu accumulator %result 628 | } 629 | 630 | ;; set status flags 631 | setFlag zero %result 632 | ;; negative flag is always set to 0 633 | hadd nes.cpu status.negative 0 634 | hadd nes.cpu status.carry %carry 635 | 636 | } 637 | 638 | alias nes.cpu.mnemonic.asl { 639 | 640 | var %mode $2 641 | var %operand $3 642 | 643 | if (%mode == zeropage) { 644 | 645 | var %address %operand 646 | 647 | var %value $nes.mem.read(%address) 648 | } 649 | 650 | if (%mode == accumulator) { 651 | 652 | var %value $hget(nes.cpu, accumulator) 653 | } 654 | 655 | var %bit7 $left($bin(%value), 1) 656 | 657 | var %result $dec($+($right(%value, 7),0)).bin 658 | 659 | hadd nes.cpu status.carry %bit7 660 | hadd nes.cpu status.negative %bit7 661 | setFlag zero %result 662 | 663 | 664 | } 665 | 666 | ;; branch if equal 667 | alias nes.cpu.mnemonic.beq { 668 | 669 | ;; wrong. it's wrong. it's all wrong. 670 | ;; aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 671 | ;; *stabs brain with fork* 672 | 673 | ;; mode is always relative 674 | var %operand $3 675 | 676 | ;; i've been doing this completely wrong. oops. 677 | ;; look at https://twitter.com/kebby/status/1658532782803410946 678 | ;; and https://www.cs.cornell.edu/~tomf/notes/cps104/twoscomp.html 679 | 680 | ;; signed bytes use the most significant bit (7) as the sign 681 | ;; if 0, it's a negative number, and if 1, positive. 682 | ;; we'll prepare for some mIRC interpreter abuse later 683 | ;; by setting %sign to either - or + depending on the result, 684 | ;; which we can then just feed into $calc(), which will 685 | ;; happily be interpreted as the correct mathmetical operator. 686 | var %sign $iif($getBit(%operand, 7) == 1, -, +) 687 | 688 | ;; two's complement 689 | ;; we'll make the value binary 690 | var %value $bin(%operand) 691 | ;; invert the bits 692 | var %value $invert(%value) 693 | ;; and add 1 to get the result 694 | var %value $calc($dec(%value).bin + 1) 695 | 696 | ;; here comes the interpreter abuse! since variables and everything 697 | ;; are evaluated first, %sign will just become - or +, and $calc() 698 | ;; will just accept this and perform our desired operation. 699 | var %result $calc($hget(nes.cpu, programCounter) %sign %value) 700 | 701 | ;; branch only if the Zero flag of the status register is 1 702 | if ($hget(nes.cpu, status.zero) == 1) { 703 | 704 | hadd nes.cpu programCounter %result 705 | } 706 | 707 | ;; add 1 to the output result for display purposes. 708 | ;; the actually calculated value is correct, setting the program 709 | ;; counter 1 before the desired branch address, which it will 710 | ;; be set to as soon as the CPU loop restarts 711 | return $calc(%result + 1) 712 | } 713 | 714 | ;; branch if not equal 715 | alias nes.cpu.mnemonic.bne { 716 | 717 | ;; most of this is like `beq` above. 718 | 719 | ;; mode is always relative 720 | var %operand $3 721 | 722 | ;; get sign to determine branching forward or backward 723 | var %sign $iif($getBit(%operand, 7) == 1, -, +) 724 | 725 | ;; two's complement, condensed! 726 | ;var %value $calc($dec($invert($bin(%operand))).bin + 1) 727 | ;; above line used $invert, which is a function i wrote (find it lower 728 | ;; in this file...) -- it seemed a bit silly though, but... 729 | ;; $not returns a 32-bit value, i don't know how to cull it 730 | ;; apart from converting from decimal to binary, then keeping 731 | ;; only the rightmost 8 bits and converting that back to decimal... 732 | ;; so this isn't really much better at all. 733 | var %value $calc($base($right($base($not(%operand), 10, 2), 8), 2, 10) + 1) 734 | 735 | ;; interpreter abuse, as above 736 | var %result $calc($hget(nes.cpu, programCounter) %sign %value) 737 | 738 | if ($hget(nes.cpu, status.zero) == 0) { 739 | 740 | hadd nes.cpu programCounter %result 741 | } 742 | 743 | ;; add 1 to the output result for display purposes. 744 | ;; the actually calculated value is correct, setting the program 745 | ;; counter 1 before the desired branch address, which it will 746 | ;; be set to as soon as the CPU loop restarts 747 | return $calc(%result + 1) 748 | } 749 | 750 | ;; branch if carry set 751 | alias nes.cpu.mnemonic.bcs { 752 | 753 | var %operand $3 754 | 755 | ;; get sign to determine branching forward or backward 756 | var %sign $iif($getBit(%operand, 7) == 1, -, +) 757 | 758 | ;; two's complement, condensed! 759 | var %value $calc($dec($invert($bin(%operand))).bin + 1) 760 | 761 | ;; interpreter abuse, as above 762 | var %result $calc($hget(nes.cpu, programCounter) %sign %value) 763 | 764 | if ($hget(nes.cpu, status.carry) == 1) { 765 | 766 | hadd nes.cpu programCounter %result 767 | } 768 | 769 | ;; add 1 to the output result for display purposes. 770 | ;; the actually calculated value is correct, setting the program 771 | ;; counter 1 before the desired branch address, which it will 772 | ;; be set to as soon as the CPU loop restarts 773 | return $calc(%result + 1) 774 | } 775 | 776 | ;; branch on carry clear 777 | alias nes.cpu.mnemonic.bcc { 778 | 779 | var %operand $3 780 | 781 | ;; get sign to determine branching forward or backward 782 | var %sign $iif($getBit(%operand, 7) == 1, -, +) 783 | 784 | ;; two's complement, condensed! 785 | var %value $calc($dec($invert($bin(%operand))).bin + 1) 786 | 787 | ;; interpreter abuse, as above 788 | var %result $calc($hget(nes.cpu, programCounter) %sign %value) 789 | 790 | if ($hget(nes.cpu, status.carry) == 0) { 791 | 792 | hadd nes.cpu programCounter %result 793 | } 794 | 795 | ;; add 1 to the output result for display purposes. 796 | ;; the actually calculated value is correct, setting the program 797 | ;; counter 1 before the desired branch address, which it will 798 | ;; be set to as soon as the CPU loop restarts 799 | return $calc(%result + 1) 800 | } 801 | 802 | ;; branch if positive 803 | alias nes.cpu.mnemonic.bpl { 804 | 805 | ;; mode is always relative 806 | var %operand $3 807 | 808 | ;; get sign to determine branching forward or backward 809 | var %sign $iif($getBit(%operand, 7) == 1, -, +) 810 | 811 | ;; two's complement, condensed! 812 | var %value $calc($dec($invert($bin(%operand))).bin + 1) 813 | 814 | ;; interpreter abuse, as above 815 | var %result $calc($hget(nes.cpu, programCounter) %sign %value) 816 | 817 | if ($hget(nes.cpu, status.negative) == 0) { 818 | 819 | hadd nes.cpu programCounter %result 820 | } 821 | 822 | return %result 823 | } 824 | 825 | alias nes.cpu.mnemonic.jmp { 826 | 827 | var %mode $2 828 | var %operand $3- 829 | 830 | if (%mode == absolute) { 831 | 832 | ;; subtract 1 from target address so we actually end up 833 | ;; in the right place... 834 | var %address $calc($mergeBytes(%operand) - 1) 835 | } 836 | 837 | hadd nes.cpu programCounter %address 838 | 839 | return %address 840 | } 841 | 842 | ;; Jump to SubRoutine 843 | alias nes.cpu.mnemonic.jsr { 844 | 845 | echo -s 56 JSR 846 | 847 | ;; mode is always absolute 848 | var %operand $3- 849 | 850 | var %address $calc($mergeBytes(%operand) - 1) 851 | 852 | ;; calculate the return point. 853 | ;; this should be the current value of the program counter, minus a few. 854 | ;; Everywhere I'm reading it says it should be -1, but I'm having doubts. 855 | ;; the program counter, at this point, is increased by operand length, 856 | ;; mainly because that's what various sources led me to believe. 857 | ;; so if we subtract 1, we would point back to the 2nd byte of the 858 | ;; operand... which... actually makes sense, because next cycle we 859 | ;; increment the program counter again and end up at the next instruction! 860 | 861 | ;; ...thanks for listening, sometimes you just gotta talk through 862 | ;; a problem to figure it out ^-^ 863 | 864 | var %returnAddress $base($calc($hget(nes.cpu, programCounter) - 1), 10, 16, 4) 865 | 866 | ;; split into upper / lower byte 867 | var %upper $dec($left(%returnAddress, 2)) 868 | var %lower $dec($right(%returnAddress, 2)) 869 | 870 | ;; now we push this to the stack. 871 | echo -s . nes.mem.stack push %upper 872 | nes.mem.stack push %upper 873 | echo -s . nes.mem.stack push %upper 874 | nes.mem.stack push %lower 875 | 876 | echo -s . jsr return: %returnAddress 877 | 878 | ;; and now we set the program counter to our target address! 879 | ;; we do gotta subtract... no, add?? 1 tho. stubid off by one errors... 880 | hadd nes.cpu programCounter %address 881 | 882 | return %address 883 | } 884 | 885 | ;; ReTurn from Subroutine 886 | alias nes.cpu.mnemonic.rts { 887 | 888 | echo -s 52 RTS 889 | 890 | ;; get topmost 2 values from the stack, that is the return address. 891 | 892 | ;; i love how incredibly cursed this is. if you performed an odd 893 | ;; number of push/pop operations in between a jsr and rts, you have 894 | ;; essentially modified the return address, which means you could 895 | ;; theoretically abuse this to jump anywhere in memory. 896 | ;; ...why you would do that over just a plain jmp, i don't know. 897 | var %lower $hex($nes.mem.stack(pop)) 898 | echo -s . lower: %lower 899 | var %upper $hex($nes.mem.stack(pop)) 900 | echo -s . upper: %upper 901 | var %returnAddress $dec($+(%upper,%lower)) 902 | echo -s . RTS return: $hex(%returnAddress) 903 | 904 | hadd nes.cpu programCounter %returnAddress 905 | } 906 | 907 | alias nes.cpu.mnemonic.brk { 908 | 909 | ;; mode is always implicit 910 | 911 | ;; increase program counter by 2(?) 912 | hinc nes.cpu programCounter 2 913 | 914 | ;; get pc in hex padded to 4 digits 915 | var %pc $base($hget(nes.cpu, programCounter), 10, 16, 4) 916 | 917 | ;; split into upper and lower byte 918 | var %upper $dec($left(%pc, 2)) 919 | var %lower $dec($right(%pc, 2)) 920 | 921 | ;; push to stack 922 | nes.mem.stack push %upper 923 | nes.mem.stack push %lower 924 | 925 | ;; push processor status onto the stack... 926 | nes.mem.stack push $dec($nes.cpu.statusFlags).bin 927 | 928 | ;; "the cpu transfers control to the interrupt vector" 929 | ;; what does this mean? 930 | 931 | ; get IRQ interrupt vector at $FFFE-$FFFF 932 | ;var %lower $hex($nes.mem.read($dec(FFFE))) 933 | ;var %upper $hex($nes.mem.read($dec(FFFF))) 934 | ;var %address $+(%upper,%lower) 935 | 936 | ;hadd nes.cpu programCounter $dec(%address) 937 | 938 | ;hadd nes.cpu status.break 1 939 | } 940 | 941 | alias nes.cpu.mnemonic.inc { 942 | 943 | var %mode $2 944 | var %operand $3 945 | 946 | if (%mode == zeropage) { 947 | 948 | var %address %operand 949 | var %result $calc($nes.mem.read(%address) + 1) 950 | 951 | if (%result > 255) { 952 | 953 | var %result 0 954 | } 955 | 956 | nes.mem.write %address %result 957 | } 958 | 959 | setFlag zero %result 960 | setFlag negative %result 961 | 962 | return %result 963 | } 964 | 965 | alias nes.cpu.mnemonic.dec { 966 | 967 | var %mode $2 968 | var %operand $3 969 | 970 | if (%mode == zeropage) { 971 | 972 | var %address %operand 973 | var %result $calc($nes.mem.read(%address) - 1) 974 | 975 | if (%result < 0) { 976 | 977 | var %result 255 978 | } 979 | 980 | nes.mem.write %address %result 981 | } 982 | 983 | setFlag zero %result 984 | setFlag negative %result 985 | 986 | return %result 987 | } 988 | 989 | alias nes.cpu.mnemonic.ora { 990 | 991 | var %mode $2 992 | var %operand $3 993 | 994 | if (%mode == immediate) { 995 | 996 | var %value %operand 997 | } 998 | 999 | ;; binary OR 1000 | 1001 | var %accumulator $bin($hget(nes.cpu, accumulator)) 1002 | var %value $bin(%value) 1003 | 1004 | var %result $or(%accumulator, %value) 1005 | 1006 | ;; store result in accumulator 1007 | hadd nes.cpu accumulator $dec(%result).bin 1008 | 1009 | setFlag zero $hget(nes.cpu, accumulator) 1010 | setFlag negative $hget(nes.cpu, accumulator) 1011 | } 1012 | 1013 | alias nes.cpu.mnemonic.nop { 1014 | 1015 | ;; nothing here matters. 1016 | ;; there are so many types of NOPs, 1017 | ;; most of them illegal, but they don't 1018 | ;; actually affect anything, so... 1019 | } 1020 | 1021 | ;; ----------------------------------------------------------------------------------------------------------------------------------- 1022 | 1023 | ;; sets $1 flag based on $2 input 1024 | alias -l setFlag { 1025 | 1026 | if ($1 == zero) { 1027 | 1028 | ;; sets zero flag is input is 0 1029 | if ($2 == 0) { 1030 | 1031 | hadd nes.cpu status.zero 1 1032 | } 1033 | 1034 | else { 1035 | 1036 | hadd nes.cpu status.zero 0 1037 | } 1038 | } 1039 | 1040 | elseif ($1 == negative) { 1041 | 1042 | ;; sets negative flag if bit 7 of input is 1 1043 | if ($2 > 127) { 1044 | 1045 | hadd nes.cpu status.negative 1 1046 | } 1047 | 1048 | else { 1049 | 1050 | hadd nes.cpu status.negative 0 1051 | } 1052 | } 1053 | } 1054 | 1055 | ;; gets the $2-th bit of the byte in $1 1056 | 1057 | ;; ...talk shit... 1058 | alias -l getBit { 1059 | 1060 | ;; since mIRC starts from 1 with most things, we'll add 1 1061 | ;; to the specified bit, and force it to be a negative value. 1062 | ;; this way we can just call $getBit(byte, 0) to get the 1063 | ;; least significant (rightmost) bit, and 7 will give us the 1064 | ;; most significant (leftmost) bit! 1065 | return $mid($bin($1), $+(-,$calc($2 + 1)), 1) 1066 | } 1067 | 1068 | 1069 | ;; converts input bytes from decimal to hex, 1070 | ;; and fuses them together (2 8-bit -> 1 16-bit value) 1071 | alias -l mergeBytes { 1072 | 1073 | tokenize 32 $1- 1074 | 1075 | ;; for reasons that are beyond my tiny cat brain, 1076 | ;; the byte order is swapped. 1077 | return $dec($+($hex($2),$hex($1))) 1078 | } 1079 | 1080 | ;; inverts a byte. expects binary. returns binary. 1081 | alias -l invert { 1082 | 1083 | return $right($base($not($base($1, 2, 10)), 10, 2), 8) 1084 | } 1085 | 1086 | ;; these things below are getting messy. 1087 | ;; i wanted to use them for readability, since $base() 1088 | ;; everywhere was feeling clunky, but maybe i need to 1089 | ;; reconsider... 1090 | 1091 | ;; explicit hex -> decimal conversion 1092 | ;; .bin for conversaion from binary 1093 | alias -l dec { 1094 | 1095 | return $base($1, $iif($prop == bin, 2, 16), 10) 1096 | } 1097 | 1098 | ;; explicit decimal -> hex conversion. always padded to 2 digits. 1099 | alias -l hex { 1100 | 1101 | return $base($1, 10, 16, 2) 1102 | } 1103 | 1104 | ;; explicit decimal -> binary conversion. 1105 | ;; always returns all 8 bits, even if leading ones are 0. 1106 | alias -l bin { 1107 | 1108 | return $base($1, 10, 2, 8) 1109 | } 1110 | 1111 | -------------------------------------------------------------------------------- /ppu.mrc: -------------------------------------------------------------------------------- 1 | alias nes.ppu.init { 2 | 3 | ;; there are some initial register values the PPU 4 | ;; has on reset. of course, in the real world, 5 | ;; any "random" values are influenced by things like 6 | ;; heat, electro-magnetism, and other things we're not 7 | ;; going to bother emulating. 8 | 9 | ;; there is a fantastic document at 10 | ;; https://www.nesdev.org/wiki/PPU_power_up_state 11 | ;; that we'll go by, mainly just setting bits that 12 | ;; are "often set", and ignoring most of the rest. 13 | 14 | ;; PPU CTRL 15 | nes.mem.write $dec(2000) $dec(00000000).bin 16 | 17 | ;; PPU MASK 18 | nes.mem.write $dec(2001) $dec(00000000).bin 19 | 20 | ;; PPU STATUS 21 | ;; bits 5 and 7 are "often set", 6 is always 0, 22 | ;; the others are irrelevant. hardcoded for now. 23 | nes.mem.write $dec(2002) $dec(10100000).bin 24 | 25 | ;; everything else should just be 0, which 26 | ;; we already do, but we can specifically force 27 | ;; them here in the future if needed. 28 | } 29 | 30 | 31 | 32 | ;; explicit hex -> decimal conversion 33 | ;; .bin for conversaion from binary 34 | alias -l dec { 35 | 36 | return $base($1, $iif($prop == bin, 2, 16), 10) 37 | } -------------------------------------------------------------------------------- /rom.mrc: -------------------------------------------------------------------------------- 1 | 2 | alias nes.rom.load { 3 | 4 | ;; temporary hardcoded ROM path. 5 | var %ROMdir $scriptdir $+ ROMs 6 | ;; just grabs the first .nes file from the path defined above. 7 | var %nes.ROM $qt($findfile(%ROMdir, *.nes, 1)) 8 | 9 | ;; add it to the data table 10 | hadd nes.data rom.file %nes.ROM 11 | 12 | ;; load header as binvar 13 | bread $qt(%nes.ROM) 0 16 &header 14 | 15 | ;; parse header 16 | ;; a lot of this is purely for my own understanding and debugging. 17 | 18 | ;; i should probably move this into it's own function 19 | 20 | echo @nes.debug reading header bytes: 21 | 22 | ;; first 4 bytes should spell NES in ASCII, + DOS "end-of-file" ($4E, $45, $53, $1A) 23 | ;; also, remember, mIRC does 1-indexing. so we gotta offset *everything* 24 | ;; we'll also get decimal values rather than hex. probably not worth converting these, 25 | ;; since it's just a quick check, though I may as well write a function for this. 26 | var %headerValue $nes.baseConvertRange($bvar(&header, 1, 4)) 27 | var %headerConst 4E 45 53 1A 28 | 29 | echo @nes.debug %headerValue 30 | echo @nes.debug %headerConst 31 | 32 | if (%headerValue == %headerConst) { 33 | 34 | echo @nes.debug first 4 bytes match! ^-^ 35 | echo @nes.debug $qt($nopath(%nes.ROM)) is probably a NES ROM file! 36 | 37 | ;; load ROM as binvar, skipping the first 16 bytes of the header. 38 | var %ROMsize $calc($file(%nes.ROM).size - 16) 39 | echo @nes.debug ROM size: $bytes(%ROMsize,k).suf $+ , %ROMsize bytes 40 | bread $qt(%nes.ROM) 16 %ROMsize &ROM 41 | 42 | ;; add the ROM (with header stripped) to the data table. 43 | ;; this is especially important because binary variables are 44 | ;; nuked when a script ends. 45 | hadd -b nes.data rom.data &ROM 46 | 47 | ;; 4th byte (or 5th if starting from 1) contains ROM size in 16k chunks 48 | var %PRGROMsize $bvar(&header, 5, 1) 49 | echo @nes.debug PRG ROM size: %PRGROMsize * 16k chunks = $calc(%PRGROMsize * 16) $+ k, $calc((%PRGROMsize * 16) * 1024) bytes. 50 | 51 | ;; 5th (6th) byte, CHR ROM size in 8k chunks. 0 means the board uses CHR RAM instead 52 | var %CHRROMsize $bvar(&header, 6, 1) 53 | 54 | if (%CHRROMsize > 0) { 55 | 56 | echo @nes.debug CHR ROM size: %CHRROMsize * 8k chunks = $calc(%CHRROMsize * 8) $+ k, $calc((%PRGROMsize * 8) * 1024) bytes. 57 | } 58 | 59 | else { 60 | 61 | ;; what's CHR RAM? I don't know (yet). 62 | echo @nes.debug board uses CHR RAM (not ROM) 63 | } 64 | 65 | ;; ------------------------------------------------------------------ 66 | ;; 6th (7th) byte: oh boy, time to decode individual bits 💀 67 | ;; ------------------------------------------------------------------ 68 | 69 | ;; on 6502, you count bits from right to left. i don't know why. 70 | var %flags6 $base($bvar(&header, 7, 1), 10, 2, 8) 71 | ;echo @nes.debug byte 06: %flags6 72 | 73 | ;; bit 0: mirroring -- 0 = horizontal mirroring, 1 vertical mirroring. 74 | var %mirroring $mid(%flags6, 1, 8) 75 | echo @nes.debug mirroring:11 $iif(%mirroring == 1, vertical, horizontal) 76 | 77 | ;; bit 1: does the cartridge have a battery? 78 | var %battery $mid(%flags6, 1, 7) 79 | echo @nes.debug battery: $iif(%battery == 1, 09true, 04false) 80 | 81 | ;; bit 2: trainer present? what is a "trainer" in this context? 82 | 83 | ;; apparently this is only relevant for modified ROM dumps, something 84 | ;; which is not really relevant in the current year of our lord 2023, 85 | ;; unless we're writing a super compatible emulator (we're not). 86 | var %trainer $mid(%flags6, 1, 6) 87 | echo @nes.debug trainer: $iif(%trainer == 1, 09true, 04false) 88 | 89 | ;; bit 3: ignore mirroring control or previous mirroring bit, and 90 | ;; set 4 screen VRAM (what is this?) 91 | var %ignoreMirror $mid(%flags6, 1, 5) 92 | echo @nes.debug ignore mirroring: $iif(%ignoreMirror == 1, 09true, 04false) 93 | 94 | ;; the next 4 bits are the lower nybble of the mapper number (why????) 95 | ;; just store 'em and combine them with the upper nybble later, i guess 96 | var %mapperLowerNybble $left(%flags6, 4) 97 | echo @nes.debug mapper lower nybble:11 %mapperLowerNybble 98 | 99 | ;; ------------------------------------------------------------------ 100 | ;; 7th (8th) byte: 101 | ;; ------------------------------------------------------------------ 102 | var %flags7 $base($bvar(&header, 8, 1), 10, 2, 8) 103 | 104 | ;; we're going to ignore the first 2 bits here, since they're mainly 105 | ;; relevant for arcade(?) hardware like the VS system and Playchoice-10 106 | 107 | ;; bit 2-3 (3-4): if this is = 2, then flags 8 - 15 are NES 2.0 format. 108 | ;; probably. there's some more detection nuances, we're not gonna worry 109 | ;; about this for the time being. 110 | 111 | ;; the last 4 bits are the upper nybble of the mapper. lol. 112 | var %mapperUpperNybble $left(%flags7, 4) 113 | echo @nes.debug mapper upper nybble:11 %mapperUpperNybble 114 | 115 | ;; combine the upper and lower nybbles. 116 | ;; is upper the first 4 digits, or last? 117 | var %mapperValue $+(%mapperUpperNybble,%mapperLowerNybble) 118 | 119 | echo @nes.debug mapper value: Bin: %mapperValue Hex: $base(%mapperValue, 2, 16, 2) Dec: $base(%mapperValue, 2, 10, 2) 120 | 121 | ;; mappers contain all sorts of weird and wonderful hardware 122 | ;; they also define which address ROM is mapped to, etc 123 | ;; i don't wanna think about that too much yet though... 124 | 125 | if (%mapperValue == 0) { 126 | 127 | ;; mapper 000 maps ROM starting at $8000, 128 | ;; but is mirrored directly after it, at $C000. 129 | hadd nes.data rom.start 8000 130 | hadd nes.data rom.mirror C000 131 | 132 | ;; copy the full ROM into both areas of RAM. 133 | bcopy &RAM $base(8000, 16, 10) &ROM 1 $calc(((%PRGROMsize * 16) * 1024)) 134 | bcopy &RAM $base(C000, 16, 10) &ROM 1 $calc(((%PRGROMsize * 16) * 1024)) 135 | } 136 | 137 | ;; find the area of address space the PRG ROM occupies. 138 | 139 | var %ROMstart $hget(nes.data, rom.start) 140 | 141 | echo @nes.debug PRG ROM is mapped to %ROMstart 142 | echo @nes.debug ------------------------------------- 143 | } 144 | 145 | else { 146 | 147 | echo @nes.debug first 4 bytes do not match! x.x 148 | echo @nes.debug $qt($nopath(%nes.ROM)) is probably not a NES ROM file? 149 | } 150 | 151 | 152 | } 153 | 154 | ;; this entire function might be a bit overkill since it's only used once, I think. 155 | alias nes.baseConvertRange { 156 | 157 | ;; i tried using the $* hack here, but it didn't work. sad 😞 158 | 159 | ;; ok, maybe something was going wrong. it seems like whatever $bvar() 160 | ;; gives us is not delimited with ascii character 32, which is the 161 | ;; assumed default when handling tokens. weird. very weird. 162 | ;; i'd rather not have to re-tokenise the string but I also can't seem 163 | ;; to figure out what the hell the value is for some reason. 164 | 165 | ;; if i look it up online (by copy/pasting from the output), it 166 | ;; comes back up as ascii code 32. so it should work. but it doesn't 167 | ;; what the fuck? 168 | 169 | ;; maybe it's just $0 that is broken. 170 | ;; it should do the same as $numtok($1-, 32), but doesn't. weird. 🙄 171 | 172 | ;; anyway, this converts a range of numbers from decimal to hexadecimal. 173 | 174 | var %i 1 175 | var %t $numtok($1-, 32) 176 | 177 | while (%i <= %t) { 178 | 179 | var %r $instok(%r, $base($gettok($1-, %i, 32), 10, 16), $calc($numtok(%r, 32) + 1), 32) 180 | inc %i 181 | } 182 | 183 | return %r 184 | } --------------------------------------------------------------------------------