├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── assets └── lochnes.png ├── benches └── frames.rs ├── rust-toolchain.toml ├── src ├── gen_utils.rs ├── input.rs ├── lib.rs ├── main.rs ├── nes.rs ├── nes │ ├── cpu.rs │ ├── mapper.rs │ └── ppu.rs ├── rom.rs └── video.rs └── tests ├── fixtures └── egg.nes └── verification_roms.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tests/fixtures/nes-test-roms"] 2 | path = tests/fixtures/nes-test-roms 3 | url = https://github.com/christopherpow/nes-test-roms.git 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 5 | 6 | ## [Unreleased] 7 | 8 | ## [0.1.1] - 2022-07-02 9 | ### Changes 10 | 11 | - Fix generator syntax for newer versions of Rust. 12 | - Pin Lochnes to a specific Rust version using a new [`rust-toolchain.toml`](https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file) file in the project root. 13 | - Upgrade all dependencies. 14 | - Switch from `log` and `stderrlog` to `tracing` and `tracing-subscriber`. 15 | 16 | ### Security 17 | 18 | - Upgrade and tweak dependencies to resolve Dependabot alerts from transitive dependencies. 19 | 20 | ## [0.1.0] - 2019-10-19 21 | ### Added 22 | 23 | - Initial release! 24 | 25 | [Unreleased]: https://github.com/kylewlacy/lochnes/compare/v0.1.1...HEAD 26 | [0.1.1]: https://github.com/kylewlacy/lochnes/compare/v0.1.0...v0.1.1 27 | [0.1.0]: https://github.com/kylewlacy/lochnes/releases/tag/v0.1.0 28 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ansi_term" 7 | version = "0.12.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 10 | dependencies = [ 11 | "winapi", 12 | ] 13 | 14 | [[package]] 15 | name = "atty" 16 | version = "0.2.10" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "2fc4a1aa4c24c0718a250f0681885c1af91419d242f29eb8f2ab28502d80dbd1" 19 | dependencies = [ 20 | "libc", 21 | "termion", 22 | "winapi", 23 | ] 24 | 25 | [[package]] 26 | name = "bitflags" 27 | version = "1.3.2" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 30 | 31 | [[package]] 32 | name = "cfg-if" 33 | version = "1.0.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 36 | 37 | [[package]] 38 | name = "clap" 39 | version = "2.34.0" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 42 | dependencies = [ 43 | "ansi_term", 44 | "atty", 45 | "bitflags", 46 | "strsim", 47 | "textwrap", 48 | "unicode-width", 49 | "vec_map", 50 | ] 51 | 52 | [[package]] 53 | name = "enum-kinds" 54 | version = "0.5.1" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "4e40a16955681d469ab3da85aaa6b42ff656b3c67b52e1d8d3dd36afe97fd462" 57 | dependencies = [ 58 | "proc-macro2", 59 | "quote", 60 | "syn", 61 | ] 62 | 63 | [[package]] 64 | name = "heck" 65 | version = "0.3.1" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 68 | dependencies = [ 69 | "unicode-segmentation", 70 | ] 71 | 72 | [[package]] 73 | name = "lazy_static" 74 | version = "1.4.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 77 | 78 | [[package]] 79 | name = "libc" 80 | version = "0.2.126" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 83 | 84 | [[package]] 85 | name = "lochnes" 86 | version = "0.1.1" 87 | dependencies = [ 88 | "bitflags", 89 | "enum-kinds", 90 | "sdl2", 91 | "structopt", 92 | "tracing", 93 | "tracing-subscriber", 94 | ] 95 | 96 | [[package]] 97 | name = "log" 98 | version = "0.4.17" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 101 | dependencies = [ 102 | "cfg-if", 103 | ] 104 | 105 | [[package]] 106 | name = "once_cell" 107 | version = "1.12.0" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" 110 | 111 | [[package]] 112 | name = "pin-project-lite" 113 | version = "0.2.9" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 116 | 117 | [[package]] 118 | name = "proc-macro-error" 119 | version = "1.0.4" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 122 | dependencies = [ 123 | "proc-macro-error-attr", 124 | "proc-macro2", 125 | "quote", 126 | "syn", 127 | "version_check", 128 | ] 129 | 130 | [[package]] 131 | name = "proc-macro-error-attr" 132 | version = "1.0.4" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 135 | dependencies = [ 136 | "proc-macro2", 137 | "quote", 138 | "version_check", 139 | ] 140 | 141 | [[package]] 142 | name = "proc-macro2" 143 | version = "1.0.40" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" 146 | dependencies = [ 147 | "unicode-ident", 148 | ] 149 | 150 | [[package]] 151 | name = "quote" 152 | version = "1.0.20" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" 155 | dependencies = [ 156 | "proc-macro2", 157 | ] 158 | 159 | [[package]] 160 | name = "redox_syscall" 161 | version = "0.1.40" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" 164 | 165 | [[package]] 166 | name = "redox_termios" 167 | version = "0.1.1" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" 170 | dependencies = [ 171 | "redox_syscall", 172 | ] 173 | 174 | [[package]] 175 | name = "sdl2" 176 | version = "0.35.2" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a" 179 | dependencies = [ 180 | "bitflags", 181 | "lazy_static", 182 | "libc", 183 | "sdl2-sys", 184 | ] 185 | 186 | [[package]] 187 | name = "sdl2-sys" 188 | version = "0.35.2" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0" 191 | dependencies = [ 192 | "cfg-if", 193 | "libc", 194 | "version-compare", 195 | ] 196 | 197 | [[package]] 198 | name = "sharded-slab" 199 | version = "0.1.4" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 202 | dependencies = [ 203 | "lazy_static", 204 | ] 205 | 206 | [[package]] 207 | name = "smallvec" 208 | version = "1.9.0" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" 211 | 212 | [[package]] 213 | name = "strsim" 214 | version = "0.8.0" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 217 | 218 | [[package]] 219 | name = "structopt" 220 | version = "0.3.26" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" 223 | dependencies = [ 224 | "clap", 225 | "lazy_static", 226 | "structopt-derive", 227 | ] 228 | 229 | [[package]] 230 | name = "structopt-derive" 231 | version = "0.4.18" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" 234 | dependencies = [ 235 | "heck", 236 | "proc-macro-error", 237 | "proc-macro2", 238 | "quote", 239 | "syn", 240 | ] 241 | 242 | [[package]] 243 | name = "syn" 244 | version = "1.0.98" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" 247 | dependencies = [ 248 | "proc-macro2", 249 | "quote", 250 | "unicode-ident", 251 | ] 252 | 253 | [[package]] 254 | name = "termion" 255 | version = "1.5.1" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" 258 | dependencies = [ 259 | "libc", 260 | "redox_syscall", 261 | "redox_termios", 262 | ] 263 | 264 | [[package]] 265 | name = "textwrap" 266 | version = "0.11.0" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 269 | dependencies = [ 270 | "unicode-width", 271 | ] 272 | 273 | [[package]] 274 | name = "thread_local" 275 | version = "1.1.4" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" 278 | dependencies = [ 279 | "once_cell", 280 | ] 281 | 282 | [[package]] 283 | name = "tracing" 284 | version = "0.1.35" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" 287 | dependencies = [ 288 | "cfg-if", 289 | "pin-project-lite", 290 | "tracing-attributes", 291 | "tracing-core", 292 | ] 293 | 294 | [[package]] 295 | name = "tracing-attributes" 296 | version = "0.1.22" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" 299 | dependencies = [ 300 | "proc-macro2", 301 | "quote", 302 | "syn", 303 | ] 304 | 305 | [[package]] 306 | name = "tracing-core" 307 | version = "0.1.28" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" 310 | dependencies = [ 311 | "once_cell", 312 | "valuable", 313 | ] 314 | 315 | [[package]] 316 | name = "tracing-log" 317 | version = "0.1.3" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" 320 | dependencies = [ 321 | "lazy_static", 322 | "log", 323 | "tracing-core", 324 | ] 325 | 326 | [[package]] 327 | name = "tracing-subscriber" 328 | version = "0.3.14" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "3a713421342a5a666b7577783721d3117f1b69a393df803ee17bb73b1e122a59" 331 | dependencies = [ 332 | "ansi_term", 333 | "sharded-slab", 334 | "smallvec", 335 | "thread_local", 336 | "tracing-core", 337 | "tracing-log", 338 | ] 339 | 340 | [[package]] 341 | name = "unicode-ident" 342 | version = "1.0.1" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" 345 | 346 | [[package]] 347 | name = "unicode-segmentation" 348 | version = "1.2.1" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "aa6024fc12ddfd1c6dbc14a80fa2324d4568849869b779f6bd37e5e4c03344d1" 351 | 352 | [[package]] 353 | name = "unicode-width" 354 | version = "0.1.5" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" 357 | 358 | [[package]] 359 | name = "valuable" 360 | version = "0.1.0" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 363 | 364 | [[package]] 365 | name = "vec_map" 366 | version = "0.8.1" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" 369 | 370 | [[package]] 371 | name = "version-compare" 372 | version = "0.1.0" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73" 375 | 376 | [[package]] 377 | name = "version_check" 378 | version = "0.9.4" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 381 | 382 | [[package]] 383 | name = "winapi" 384 | version = "0.3.5" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "773ef9dcc5f24b7d850d0ff101e542ff24c3b090a9768e03ff889fdef41f00fd" 387 | dependencies = [ 388 | "winapi-i686-pc-windows-gnu", 389 | "winapi-x86_64-pc-windows-gnu", 390 | ] 391 | 392 | [[package]] 393 | name = "winapi-i686-pc-windows-gnu" 394 | version = "0.4.0" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 397 | 398 | [[package]] 399 | name = "winapi-x86_64-pc-windows-gnu" 400 | version = "0.4.0" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 403 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Kyle Lacy "] 3 | name = "lochnes" 4 | description = "A toy NES emulator in Rust" 5 | repository = "https://github.com/kylewlacy/lochnes" 6 | version = "0.1.1" 7 | edition = "2021" 8 | license = "MIT" 9 | 10 | [dependencies] 11 | bitflags = "1.3.2" 12 | enum-kinds = "0.5.1" 13 | structopt = "0.3.26" 14 | sdl2 = "0.35.2" 15 | tracing = "0.1.35" 16 | tracing-subscriber = "0.3.14" 17 | 18 | [features] 19 | default = ["easter-egg"] 20 | easter-egg = [] 21 | 22 | [profile.dev] 23 | opt-level = 2 24 | 25 | [profile.release] 26 | debug = true 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kyle Lacy 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 | # Lochnes 2 | 3 | ![𝕃𝕆ℂℍℕ𝔼𝕊](./assets/lochnes.png) 4 | 5 | This is a toy NES emulator in Rust, which uses generators for control flow. I wrote a blog post going over some of the implementation details here: https://kyle.space/posts/i-made-a-nes-emulator/ 6 | 7 | **NOTE:** Lochnes uses nightly features! The project is pinned to a specific working nightly version of Rust in `rust-toolchain.toml`. Rustup should ensure that an appropriate version of Rust is installed when building or running the project from the repo root. 8 | 9 | ## Compatibility 10 | 11 | Compatibility is very poor! It doesn't support audio output, scrolling, or most NES ROM mappers. Games that use the NROM or UXROM mappers should be loadable, and games that don't use scrolling should be mostly playable. 12 | 13 | ## Usage 14 | 15 | You'll need to install SDL2 before using Lochnes (see the README for [`rust-sdl2`](https://github.com/Rust-SDL2/rust-sdl2) for different installation options based on on your platform) 16 | 17 | Lochnes can be started with: 18 | 19 | ```sh-session 20 | $ cargo run --release -- rom.nes 21 | ``` 22 | 23 | Or, if you want to use a bundled version of SDL2: 24 | 25 | ```sh-session 26 | $ cargo run --features sdl2/bundled --release -- rom.nes 27 | ``` 28 | 29 | If 256×240 is too small for you, use `--scale` to make the window bigger: 30 | 31 | ```sh-session 32 | $ cargo run --release -- rom.nes --scale=3 33 | ``` 34 | 35 | If you want debug output, pass `-v` multiple times (warning: 5 v's makes everything really slow, don't even bother with 6) 36 | 37 | ```sh-session 38 | $ cargo run --release -- rom.nes -vvvvv 39 | ``` 40 | 41 | ## Controls 42 | 43 | Input bindings is not currently customizable, so here are the current input bindings: 44 | 45 | - **Exit**: \[Esc\] 46 | - **A**: \[Z\], (Xbox: A) 47 | - **B**: \[X\], (Xbox: B or X) 48 | - **Start**: \[Return\], (Xbox: Start) 49 | - **Select**: \[\\\] (Backslash key), (Xbox: Back) 50 | - **Up**/**Down**/**Left**/**Right**: \[⇧\]/\[⇩\]/\[⇦\]/\[⇨\] (Arrow keys), (Xbox: D-pad) 51 | 52 | ## License 53 | 54 | Licensed under the MIT license 55 | -------------------------------------------------------------------------------- /assets/lochnes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylewlacy/lochnes/336d6532c6b6d650ba56360415b064b8de845846/assets/lochnes.png -------------------------------------------------------------------------------- /benches/frames.rs: -------------------------------------------------------------------------------- 1 | #![feature(test, generator_trait, exhaustive_patterns)] 2 | 3 | extern crate test; 4 | 5 | use std::ops::{Generator, GeneratorState}; 6 | use std::pin::Pin; 7 | use std::{env, fs}; 8 | use test::Bencher; 9 | 10 | use lochnes::nes::ppu::PpuStep; 11 | use lochnes::nes::NesStep; 12 | use lochnes::{input, nes, rom, video}; 13 | 14 | #[bench] 15 | fn bench_frames(b: &mut Bencher) { 16 | // TODO: Add a ROM as a fixture for benchmarking 17 | let rom_path = env::var("BENCH_ROM").expect("BENCH_ROM env var must be set for benchmarking"); 18 | let rom_bytes = fs::read(rom_path).expect("Failed to open BENCH_ROM"); 19 | let rom = rom::Rom::from_bytes(rom_bytes.into_iter()) 20 | .expect("Failed to parse BENCH_ROM into a valid ROM"); 21 | 22 | b.iter(|| { 23 | let video = video::NullVideo; 24 | let input = input::NullInput; 25 | let io = nes::NesIoWith { video, input }; 26 | let nes = nes::Nes::new(&io, rom.clone()); 27 | let mut run_nes = nes.run(); 28 | 29 | for _ in 0..10 { 30 | loop { 31 | match Pin::new(&mut run_nes).resume(()) { 32 | GeneratorState::Yielded(NesStep::Ppu(PpuStep::Vblank)) => { 33 | break; 34 | } 35 | GeneratorState::Yielded(_) => {} 36 | } 37 | } 38 | } 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2022-07-02" 3 | -------------------------------------------------------------------------------- /src/gen_utils.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! yield_all { 3 | ($gen: expr) => {{ 4 | use std::ops::{Generator, GeneratorState}; 5 | use std::pin::Pin; 6 | 7 | let mut gen = $gen; 8 | loop { 9 | match Pin::new(&mut gen).resume(()) { 10 | GeneratorState::Yielded(yielded) => { 11 | yield yielded; 12 | } 13 | GeneratorState::Complete(result) => { 14 | break result; 15 | } 16 | } 17 | } 18 | }}; 19 | } 20 | -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | 3 | #[derive(Debug, Clone, Copy, Default)] 4 | pub struct InputState { 5 | pub joypad_1: JoypadState, 6 | pub joypad_2: JoypadState, 7 | } 8 | 9 | #[derive(Debug, Clone, Copy)] 10 | pub struct JoypadState { 11 | pub a: bool, 12 | pub b: bool, 13 | pub start: bool, 14 | pub select: bool, 15 | pub up: bool, 16 | pub down: bool, 17 | pub left: bool, 18 | pub right: bool, 19 | } 20 | 21 | impl Default for JoypadState { 22 | fn default() -> Self { 23 | JoypadState { 24 | a: false, 25 | b: false, 26 | start: false, 27 | select: false, 28 | up: false, 29 | down: false, 30 | left: false, 31 | right: false, 32 | } 33 | } 34 | } 35 | 36 | pub trait Input { 37 | fn input_state(&self) -> InputState; 38 | } 39 | 40 | pub struct NullInput; 41 | 42 | impl Input for NullInput { 43 | fn input_state(&self) -> InputState { 44 | InputState::default() 45 | } 46 | } 47 | 48 | pub struct SampledInput { 49 | state: Cell, 50 | } 51 | 52 | impl SampledInput { 53 | pub fn new(state: InputState) -> Self { 54 | SampledInput { 55 | state: Cell::new(state), 56 | } 57 | } 58 | 59 | pub fn set_state(&self, new_state: InputState) { 60 | self.state.set(new_state) 61 | } 62 | } 63 | 64 | impl Input for SampledInput { 65 | fn input_state(&self) -> InputState { 66 | let state = self.state.take(); 67 | self.state.set(state.clone()); 68 | state 69 | } 70 | } 71 | 72 | impl<'a, I> Input for &'a I 73 | where 74 | I: Input, 75 | { 76 | fn input_state(&self) -> InputState { 77 | (*self).input_state() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature( 2 | cell_update, 3 | never_type, 4 | exhaustive_patterns, 5 | generators, 6 | generator_trait 7 | )] 8 | 9 | #[macro_use] 10 | pub mod gen_utils; 11 | pub mod input; 12 | pub mod nes; 13 | pub mod rom; 14 | pub mod video; 15 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature( 2 | cell_update, 3 | never_type, 4 | exhaustive_patterns, 5 | generators, 6 | generator_trait 7 | )] 8 | 9 | use nes::ppu::PpuStep; 10 | use nes::NesStep; 11 | use sdl2::controller::Button as SdlButton; 12 | use sdl2::event::Event as SdlEvent; 13 | use sdl2::keyboard::Keycode as SdlKeycode; 14 | use std::fs; 15 | use std::io; 16 | use std::ops::{Generator, GeneratorState}; 17 | use std::path::PathBuf; 18 | use std::pin::Pin; 19 | use std::process; 20 | use std::thread; 21 | use std::time::{Duration, Instant}; 22 | use structopt::StructOpt; 23 | 24 | use lochnes::{input, nes, rom, video}; 25 | 26 | fn main() { 27 | let opts = Options::from_args(); 28 | 29 | let log_level = match (opts.quiet, opts.verbose) { 30 | (true, _) => None, 31 | (false, 0) => Some(tracing::Level::WARN), 32 | (false, 1) => Some(tracing::Level::INFO), 33 | (false, 2) => Some(tracing::Level::DEBUG), 34 | (false, 3..) => Some(tracing::Level::TRACE), 35 | }; 36 | 37 | tracing_subscriber::fmt() 38 | .with_max_level(log_level) 39 | .without_time() 40 | .init(); 41 | 42 | let run_result = run(opts); 43 | 44 | match run_result { 45 | Ok(_) => {} 46 | Err(err) => { 47 | eprintln!("{:?}", err); 48 | process::exit(1); 49 | } 50 | } 51 | } 52 | 53 | #[derive(StructOpt, Debug)] 54 | #[structopt(name = "lochnes")] 55 | struct Options { 56 | #[structopt(name = "ROM", parse(from_os_str))] 57 | rom: PathBuf, 58 | 59 | #[structopt(long = "scale")] 60 | scale: Option, 61 | 62 | #[structopt(short = "q", long = "quiet")] 63 | quiet: bool, 64 | 65 | #[structopt(short = "v", parse(from_occurrences))] 66 | verbose: u8, 67 | } 68 | 69 | fn run(opts: Options) -> Result<(), LochnesError> { 70 | tracing::debug!("Options: {:#?}", opts); 71 | 72 | #[cfg(feature = "easter-egg")] 73 | { 74 | if opts.verbose == 6 { 75 | let bytes = include_bytes!("../tests/fixtures/egg.nes"); 76 | let mut bytes: Vec = bytes.to_vec(); 77 | let nmi = &mut bytes[0x400C]; 78 | *nmi = nmi.wrapping_add(opts.verbose / 2); 79 | let rom = rom::Rom::from_bytes(bytes.into_iter())?; 80 | run_rom(opts, rom)?; 81 | return Ok(()); 82 | } 83 | } 84 | 85 | let bytes = fs::read(&opts.rom)?; 86 | let rom = rom::Rom::from_bytes(bytes.into_iter())?; 87 | 88 | tracing::debug!("ROM header: {:#04X?}", rom.header); 89 | 90 | run_rom(opts, rom)?; 91 | 92 | Ok(()) 93 | } 94 | 95 | fn run_rom(opts: Options, rom: rom::Rom) -> Result<(), LochnesError> { 96 | const NES_REFRESH_RATE: Duration = Duration::from_nanos(1_000_000_000 / 60); 97 | const NES_WIDTH: u32 = 256; 98 | const NES_HEIGHT: u32 = 240; 99 | 100 | let scale = opts.scale.unwrap_or(1); 101 | 102 | let window_width = NES_WIDTH * scale; 103 | let window_height = NES_HEIGHT * scale; 104 | 105 | let sdl = sdl2::init().map_err(LochnesError::Sdl2Error)?; 106 | let sdl_video = sdl.video().map_err(LochnesError::Sdl2Error)?; 107 | let sdl_window = sdl_video 108 | .window("Lochnes", window_width, window_height) 109 | .opengl() 110 | .build()?; 111 | let mut sdl_canvas = sdl_window.into_canvas().build()?; 112 | let sdl_texture_creator = sdl_canvas.texture_creator(); 113 | let sdl_controllers = sdl.game_controller().map_err(LochnesError::Sdl2Error)?; 114 | 115 | let num_sdl_controllers = sdl_controllers 116 | .num_joysticks() 117 | .map_err(LochnesError::Sdl2Error)?; 118 | 119 | let sdl_controller_index = (0..num_sdl_controllers).find_map(|n| { 120 | if sdl_controllers.is_game_controller(n) { 121 | Some(n) 122 | } else { 123 | None 124 | } 125 | }); 126 | let _sdl_controller = sdl_controller_index 127 | .map(|index| sdl_controllers.open(index)) 128 | .transpose()?; 129 | 130 | let mut sdl_event_pump = sdl.event_pump().map_err(LochnesError::Sdl2Error)?; 131 | 132 | let video = &video::TextureBufferedVideo::new(&sdl_texture_creator, NES_WIDTH, NES_HEIGHT)?; 133 | let mut input_state = input::InputState::default(); 134 | let input = &input::SampledInput::new(input_state); 135 | let io = nes::NesIoWith { video, input }; 136 | let nes = nes::Nes::new(&io, rom); 137 | let mut run_nes = nes.run(); 138 | 139 | 'running: loop { 140 | let frame_start = Instant::now(); 141 | for event in sdl_event_pump.poll_iter() { 142 | match event { 143 | SdlEvent::Quit { .. } 144 | | SdlEvent::KeyUp { 145 | keycode: Some(SdlKeycode::Escape), 146 | .. 147 | } => { 148 | break 'running; 149 | } 150 | SdlEvent::KeyDown { 151 | keycode: Some(SdlKeycode::Z), 152 | .. 153 | } 154 | | SdlEvent::ControllerButtonDown { 155 | button: SdlButton::A, 156 | .. 157 | } => { 158 | input_state.joypad_1.a = true; 159 | } 160 | SdlEvent::KeyUp { 161 | keycode: Some(SdlKeycode::Z), 162 | .. 163 | } 164 | | SdlEvent::ControllerButtonUp { 165 | button: SdlButton::A, 166 | .. 167 | } => { 168 | input_state.joypad_1.a = false; 169 | } 170 | SdlEvent::KeyDown { 171 | keycode: Some(SdlKeycode::X), 172 | .. 173 | } 174 | | SdlEvent::ControllerButtonDown { 175 | button: SdlButton::B, 176 | .. 177 | } 178 | | SdlEvent::ControllerButtonDown { 179 | button: SdlButton::X, 180 | .. 181 | } => { 182 | input_state.joypad_1.b = true; 183 | } 184 | SdlEvent::KeyUp { 185 | keycode: Some(SdlKeycode::X), 186 | .. 187 | } 188 | | SdlEvent::ControllerButtonUp { 189 | button: SdlButton::B, 190 | .. 191 | } 192 | | SdlEvent::ControllerButtonUp { 193 | button: SdlButton::X, 194 | .. 195 | } => { 196 | input_state.joypad_1.b = false; 197 | } 198 | SdlEvent::KeyDown { 199 | keycode: Some(SdlKeycode::Return), 200 | .. 201 | } 202 | | SdlEvent::ControllerButtonDown { 203 | button: SdlButton::Start, 204 | .. 205 | } => { 206 | input_state.joypad_1.start = true; 207 | } 208 | SdlEvent::KeyUp { 209 | keycode: Some(SdlKeycode::Return), 210 | .. 211 | } 212 | | SdlEvent::ControllerButtonUp { 213 | button: SdlButton::Start, 214 | .. 215 | } => { 216 | input_state.joypad_1.start = false; 217 | } 218 | SdlEvent::KeyDown { 219 | keycode: Some(SdlKeycode::Backslash), 220 | .. 221 | } 222 | | SdlEvent::ControllerButtonDown { 223 | button: SdlButton::Back, 224 | .. 225 | } => { 226 | input_state.joypad_1.select = true; 227 | } 228 | SdlEvent::KeyUp { 229 | keycode: Some(SdlKeycode::Backslash), 230 | .. 231 | } 232 | | SdlEvent::ControllerButtonUp { 233 | button: SdlButton::Back, 234 | .. 235 | } => { 236 | input_state.joypad_1.select = false; 237 | } 238 | SdlEvent::KeyDown { 239 | keycode: Some(SdlKeycode::Up), 240 | .. 241 | } 242 | | SdlEvent::ControllerButtonDown { 243 | button: SdlButton::DPadUp, 244 | .. 245 | } => { 246 | input_state.joypad_1.up = true; 247 | } 248 | SdlEvent::KeyUp { 249 | keycode: Some(SdlKeycode::Up), 250 | .. 251 | } 252 | | SdlEvent::ControllerButtonUp { 253 | button: SdlButton::DPadUp, 254 | .. 255 | } => { 256 | input_state.joypad_1.up = false; 257 | } 258 | SdlEvent::KeyDown { 259 | keycode: Some(SdlKeycode::Down), 260 | .. 261 | } 262 | | SdlEvent::ControllerButtonDown { 263 | button: SdlButton::DPadDown, 264 | .. 265 | } => { 266 | input_state.joypad_1.down = true; 267 | } 268 | SdlEvent::KeyUp { 269 | keycode: Some(SdlKeycode::Down), 270 | .. 271 | } 272 | | SdlEvent::ControllerButtonUp { 273 | button: SdlButton::DPadDown, 274 | .. 275 | } => { 276 | input_state.joypad_1.down = false; 277 | } 278 | SdlEvent::KeyDown { 279 | keycode: Some(SdlKeycode::Left), 280 | .. 281 | } 282 | | SdlEvent::ControllerButtonDown { 283 | button: SdlButton::DPadLeft, 284 | .. 285 | } => { 286 | input_state.joypad_1.left = true; 287 | } 288 | SdlEvent::KeyUp { 289 | keycode: Some(SdlKeycode::Left), 290 | .. 291 | } 292 | | SdlEvent::ControllerButtonUp { 293 | button: SdlButton::DPadLeft, 294 | .. 295 | } => { 296 | input_state.joypad_1.left = false; 297 | } 298 | SdlEvent::KeyDown { 299 | keycode: Some(SdlKeycode::Right), 300 | .. 301 | } 302 | | SdlEvent::ControllerButtonDown { 303 | button: SdlButton::DPadRight, 304 | .. 305 | } => { 306 | input_state.joypad_1.right = true; 307 | } 308 | SdlEvent::KeyUp { 309 | keycode: Some(SdlKeycode::Right), 310 | .. 311 | } 312 | | SdlEvent::ControllerButtonUp { 313 | button: SdlButton::DPadRight, 314 | .. 315 | } => { 316 | input_state.joypad_1.right = false; 317 | } 318 | _ => {} 319 | } 320 | } 321 | 322 | input.set_state(input_state); 323 | tracing::debug!("Input: {:?}", input_state); 324 | 325 | loop { 326 | match Pin::new(&mut run_nes).resume(()) { 327 | GeneratorState::Yielded(NesStep::Ppu(PpuStep::Vblank)) => { 328 | break; 329 | } 330 | GeneratorState::Yielded(NesStep::Cpu(nes::cpu::CpuStep::Op(op))) => { 331 | tracing::trace!("{:X?}", nes.cpu); 332 | tracing::trace!("${:04X}: {}", op.pc, op.op); 333 | tracing::trace!("----------"); 334 | } 335 | GeneratorState::Yielded(_) => {} 336 | } 337 | } 338 | 339 | video 340 | .copy_to(&mut sdl_canvas) 341 | .map_err(LochnesError::Sdl2Error)?; 342 | sdl_canvas.present(); 343 | 344 | let elapsed = frame_start.elapsed(); 345 | tracing::info!("frame time: {:5.2}ms", elapsed.as_micros() as f64 / 1_000.0); 346 | let duration_until_refresh = NES_REFRESH_RATE.checked_sub(elapsed); 347 | let sleep_duration = duration_until_refresh.unwrap_or_else(|| Duration::from_secs(0)); 348 | thread::sleep(sleep_duration); 349 | } 350 | 351 | Ok(()) 352 | } 353 | 354 | #[derive(Debug)] 355 | enum LochnesError { 356 | IoError(io::Error), 357 | RomError(rom::RomError), 358 | Sdl2Error(String), 359 | } 360 | 361 | impl From for LochnesError { 362 | fn from(err: io::Error) -> Self { 363 | LochnesError::IoError(err) 364 | } 365 | } 366 | 367 | impl From for LochnesError { 368 | fn from(err: rom::RomError) -> Self { 369 | LochnesError::RomError(err) 370 | } 371 | } 372 | 373 | impl From for LochnesError { 374 | fn from(err: sdl2::video::WindowBuildError) -> Self { 375 | LochnesError::Sdl2Error(err.to_string()) 376 | } 377 | } 378 | 379 | impl From for LochnesError { 380 | fn from(err: sdl2::IntegerOrSdlError) -> Self { 381 | LochnesError::Sdl2Error(err.to_string()) 382 | } 383 | } 384 | 385 | impl From for LochnesError { 386 | fn from(err: sdl2::render::TextureValueError) -> Self { 387 | LochnesError::Sdl2Error(err.to_string()) 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /src/nes.rs: -------------------------------------------------------------------------------- 1 | use crate::input::{Input, InputState}; 2 | use crate::rom::Rom; 3 | use crate::video::Video; 4 | use cpu::{Cpu, CpuStep}; 5 | use mapper::Mapper; 6 | use ppu::{Ppu, PpuStep}; 7 | use std::cell::Cell; 8 | use std::ops::{Generator, GeneratorState}; 9 | use std::pin::Pin; 10 | use std::u8; 11 | 12 | pub mod cpu; 13 | pub mod mapper; 14 | pub mod ppu; 15 | 16 | #[derive(Clone)] 17 | pub struct Nes<'a, I> 18 | where 19 | I: NesIo, 20 | { 21 | pub io: &'a I, 22 | input_reader: InputReader<&'a I::Input>, 23 | pub mapper: Mapper, 24 | pub ram: Cell<[u8; 0x0800]>, 25 | pub cpu: Cpu, 26 | pub ppu: Ppu, 27 | } 28 | 29 | impl<'a, I> Nes<'a, I> 30 | where 31 | I: NesIo, 32 | { 33 | pub fn new(io: &'a I, rom: Rom) -> Self { 34 | let ram = Cell::new([0; 0x0800]); 35 | let cpu = Cpu::new(); 36 | let ppu = Ppu::new(); 37 | let mapper = Mapper::from_rom(rom); 38 | let input_reader = InputReader::new(io.input()); 39 | 40 | let nes = Nes { 41 | io, 42 | input_reader, 43 | mapper, 44 | ram, 45 | cpu, 46 | ppu, 47 | }; 48 | 49 | let reset_addr = nes.read_u16(0xFFFC); 50 | 51 | nes.cpu.pc.set(reset_addr); 52 | 53 | nes 54 | } 55 | 56 | fn ram(&self) -> &[Cell] { 57 | let ram: &Cell<[u8]> = &self.ram; 58 | ram.as_slice_of_cells() 59 | } 60 | 61 | pub fn read_u8(&self, addr: u16) -> u8 { 62 | let ram = self.ram(); 63 | 64 | match addr { 65 | 0x0000..=0x07FF => ram[addr as usize].get(), 66 | 0x2002 => self.ppu.ppustatus(), 67 | 0x2007 => self.ppu.read_ppudata(self), 68 | 0x4000..=0x4007 => { 69 | // TODO: Return APU pulse 70 | 0x00 71 | } 72 | 0x4008..=0x400B => { 73 | // TODO: Return APU triangle 74 | 0x00 75 | } 76 | 0x400C..=0x400F => { 77 | // TODO: Return APU noise 78 | 0x00 79 | } 80 | 0x4010..=0x4013 => { 81 | // TODO: Return APU DMC 82 | 0x00 83 | } 84 | 0x4015 => { 85 | // TODO: Return APU status 86 | 0x00 87 | } 88 | 0x4016 => { 89 | // TODO: Handle open bus behavior! 90 | match self.input_reader.read_port_1_data() { 91 | true => 0b_0000_0001, 92 | false => 0b_0000_0000, 93 | } 94 | } 95 | 0x4017 => { 96 | // TODO: Return joystick state 97 | 0x40 98 | } 99 | 0x6000..=0xFFFF => self.mapper.read_u8(addr), 100 | _ => { 101 | unimplemented!("Unhandled read from address: 0x{:X}", addr); 102 | } 103 | } 104 | } 105 | 106 | pub fn read_u16(&self, addr: u16) -> u16 { 107 | let lo = self.read_u8(addr); 108 | let hi = self.read_u8(addr.wrapping_add(1)); 109 | 110 | lo as u16 | ((hi as u16) << 8) 111 | } 112 | 113 | pub fn write_u8(&self, addr: u16, value: u8) { 114 | let ram = self.ram(); 115 | 116 | match addr { 117 | 0x0000..=0x07FF => { 118 | ram[addr as usize].set(value); 119 | } 120 | 0x2000 => { 121 | self.ppu.set_ppuctrl(value); 122 | } 123 | 0x2001 => { 124 | self.ppu.set_ppumask(value); 125 | } 126 | 0x2003 => { 127 | self.ppu.write_oamaddr(value); 128 | } 129 | 0x2004 => { 130 | self.ppu.write_oamdata(value); 131 | } 132 | 0x2005 => { 133 | self.ppu.write_ppuscroll(value); 134 | } 135 | 0x2006 => { 136 | self.ppu.write_ppuaddr(value); 137 | } 138 | 0x2007 => { 139 | self.ppu.write_ppudata(self, value); 140 | } 141 | 0x4000..=0x4007 => { 142 | // TODO: APU pulse 143 | } 144 | 0x4008..=0x400B => { 145 | // TODO: APU triangle 146 | } 147 | 0x400C..=0x400F => { 148 | // TODO: APU noise 149 | } 150 | 0x4010..=0x4013 => { 151 | // TODO: APU DMC 152 | } 153 | 0x4014 => { 154 | self.copy_oam_dma(value); 155 | } 156 | 0x4015 => { 157 | // TODO: APU sound channel control 158 | } 159 | 0x4016 => { 160 | let strobe = (value & 0b_0000_0001) != 0; 161 | if strobe { 162 | self.input_reader.start_strobe(); 163 | } else { 164 | self.input_reader.stop_strobe(); 165 | } 166 | } 167 | 0x4017 => { 168 | // TODO: Implement APU frame counter 169 | } 170 | 0x6000..=0xFFFF => { 171 | self.mapper.write_u8(addr, value); 172 | } 173 | _ => { 174 | unimplemented!("Unhandled write to address: 0x{:X}", addr); 175 | } 176 | } 177 | } 178 | 179 | fn push_u8(&self, value: u8) { 180 | let s = self.cpu.s.get(); 181 | let stack_addr = 0x0100 | s as u16; 182 | 183 | self.write_u8(stack_addr, value); 184 | 185 | self.cpu.s.set(s.wrapping_sub(1)); 186 | } 187 | 188 | fn push_u16(&self, value: u16) { 189 | let value_hi = ((0xFF00 & value) >> 8) as u8; 190 | let value_lo = (0x00FF & value) as u8; 191 | 192 | self.push_u8(value_hi); 193 | self.push_u8(value_lo); 194 | } 195 | 196 | pub fn read_ppu_u8(&self, addr: u16) -> u8 { 197 | let palette_ram = self.ppu.palette_ram(); 198 | 199 | match addr { 200 | 0x0000..=0x3EFF => self.mapper.read_ppu_u8(self, addr), 201 | 0x3F10 => self.read_ppu_u8(0x3F00), 202 | 0x3F14 => self.read_ppu_u8(0x3F04), 203 | 0x3F18 => self.read_ppu_u8(0x3F08), 204 | 0x3F1C => self.read_ppu_u8(0x3F0C), 205 | 0x3F00..=0x3FFF => { 206 | let offset = (addr - 0x3F00) as usize % palette_ram.len(); 207 | palette_ram[offset].get() 208 | } 209 | 0x4000..=0xFFFF => { 210 | unimplemented!("Tried to read from PPU address ${:04X}", addr); 211 | } 212 | } 213 | } 214 | 215 | pub fn write_ppu_u8(&self, addr: u16, value: u8) { 216 | let palette_ram = self.ppu.palette_ram(); 217 | 218 | match addr { 219 | 0x0000..=0x3EFF => { 220 | self.mapper.write_ppu_u8(self, addr, value); 221 | } 222 | 0x3F10 => { 223 | self.write_ppu_u8(0x3F00, value); 224 | } 225 | 0x3F14 => { 226 | self.write_ppu_u8(0x3F04, value); 227 | } 228 | 0x3F18 => { 229 | self.write_ppu_u8(0x3F08, value); 230 | } 231 | 0x3F1C => { 232 | self.write_ppu_u8(0x3F0C, value); 233 | } 234 | 0x3F00..=0x3FFF => { 235 | let offset = (addr - 0x3F00) as usize % palette_ram.len(); 236 | palette_ram[offset].set(value); 237 | } 238 | 0x4000..=0xFFFF => { 239 | unimplemented!("Tried to write to PPU address ${:04X}", addr); 240 | } 241 | } 242 | } 243 | 244 | fn copy_oam_dma(&self, page: u8) { 245 | let target_addr_start = self.ppu.oam_addr.get() as u16; 246 | let mut oam = self.ppu.oam.get(); 247 | for index in 0x00..=0xFF { 248 | let source_addr = ((page as u16) << 8) | index; 249 | let byte = self.read_u8(source_addr); 250 | 251 | let target_addr = (target_addr_start + index) as usize % oam.len(); 252 | oam[target_addr] = byte; 253 | } 254 | 255 | self.ppu.oam.set(oam); 256 | } 257 | 258 | pub fn run(&'a self) -> impl Generator + 'a { 259 | let mut run_cpu = Cpu::run(&self); 260 | 261 | let mut run_ppu = Ppu::run(&self); 262 | 263 | move || loop { 264 | // TODO: Clean this up 265 | loop { 266 | match Pin::new(&mut run_cpu).resume(()) { 267 | GeneratorState::Yielded(cpu_step @ CpuStep::Cycle) => { 268 | yield NesStep::Cpu(cpu_step); 269 | break; 270 | } 271 | GeneratorState::Yielded(cpu_step) => { 272 | yield NesStep::Cpu(cpu_step); 273 | } 274 | } 275 | } 276 | 277 | for _ in 0u8..3 { 278 | loop { 279 | match Pin::new(&mut run_ppu).resume(()) { 280 | GeneratorState::Yielded(ppu_step @ PpuStep::Cycle) => { 281 | yield NesStep::Ppu(ppu_step); 282 | break; 283 | } 284 | GeneratorState::Yielded(ppu_step) => { 285 | yield NesStep::Ppu(ppu_step); 286 | } 287 | } 288 | } 289 | } 290 | } 291 | } 292 | } 293 | 294 | pub enum NesStep { 295 | Cpu(CpuStep), 296 | Ppu(PpuStep), 297 | } 298 | 299 | // A trait that encapsulates NES I/O traits (`Video` and `Input`), allowing 300 | // code that uses `Nes` to only take or return a single generic parameter. 301 | pub trait NesIo { 302 | type Video: Video; 303 | type Input: Input; 304 | 305 | fn video(&self) -> &Self::Video; 306 | fn input(&self) -> &Self::Input; 307 | } 308 | 309 | pub struct NesIoWith 310 | where 311 | V: Video, 312 | I: Input, 313 | { 314 | pub video: V, 315 | pub input: I, 316 | } 317 | 318 | impl NesIo for NesIoWith 319 | where 320 | V: Video, 321 | I: Input, 322 | { 323 | type Video = V; 324 | type Input = I; 325 | 326 | fn video(&self) -> &Self::Video { 327 | &self.video 328 | } 329 | 330 | fn input(&self) -> &Self::Input { 331 | &self.input 332 | } 333 | } 334 | 335 | impl<'a, I> NesIo for &'a I 336 | where 337 | I: NesIo, 338 | { 339 | type Video = I::Video; 340 | type Input = I::Input; 341 | 342 | fn video(&self) -> &Self::Video { 343 | (*self).video() 344 | } 345 | 346 | fn input(&self) -> &Self::Input { 347 | (*self).input() 348 | } 349 | } 350 | 351 | #[derive(Clone)] 352 | struct InputReader 353 | where 354 | I: Input, 355 | { 356 | input: I, 357 | strobe: Cell, 358 | } 359 | 360 | impl InputReader 361 | where 362 | I: Input, 363 | { 364 | fn new(input: I) -> Self { 365 | let strobe = Cell::new(InputStrobe::Live); 366 | InputReader { input, strobe } 367 | } 368 | 369 | fn start_strobe(&self) { 370 | self.strobe.set(InputStrobe::Live); 371 | } 372 | 373 | fn stop_strobe(&self) { 374 | let state = self.input.input_state(); 375 | self.strobe.set(InputStrobe::Strobed { 376 | state, 377 | read_port_1: 0, 378 | read_port_2: 0, 379 | }); 380 | } 381 | 382 | fn read_port_1_data(&self) -> bool { 383 | match self.strobe.get() { 384 | InputStrobe::Live => { 385 | let current_state = self.input.input_state(); 386 | current_state.joypad_1.a 387 | } 388 | InputStrobe::Strobed { 389 | state, 390 | read_port_1, 391 | read_port_2, 392 | } => { 393 | let data = match read_port_1 { 394 | 0 => state.joypad_1.a, 395 | 1 => state.joypad_1.b, 396 | 2 => state.joypad_1.select, 397 | 3 => state.joypad_1.start, 398 | 4 => state.joypad_1.up, 399 | 5 => state.joypad_1.down, 400 | 6 => state.joypad_1.left, 401 | 7 => state.joypad_1.right, 402 | _ => true, 403 | }; 404 | 405 | self.strobe.set(InputStrobe::Strobed { 406 | state, 407 | read_port_1: read_port_1.saturating_add(1), 408 | read_port_2, 409 | }); 410 | 411 | data 412 | } 413 | } 414 | } 415 | } 416 | 417 | #[derive(Clone, Copy)] 418 | enum InputStrobe { 419 | Live, 420 | Strobed { 421 | state: InputState, 422 | read_port_1: u8, 423 | read_port_2: u8, 424 | }, 425 | } 426 | -------------------------------------------------------------------------------- /src/nes/mapper.rs: -------------------------------------------------------------------------------- 1 | use crate::nes::{Nes, NesIo}; 2 | use crate::rom::Rom; 3 | use std::cell::Cell; 4 | 5 | #[derive(Clone)] 6 | pub enum Mapper { 7 | Nrom(NromMapper), 8 | Uxrom(UxromMapper), 9 | } 10 | 11 | impl Mapper { 12 | pub fn from_rom(rom: Rom) -> Self { 13 | match rom.header.mapper { 14 | 0 => Mapper::Nrom(NromMapper::from_rom(rom)), 15 | 2 => Mapper::Uxrom(UxromMapper::from_rom(rom)), 16 | mapper => { 17 | unimplemented!("Mapper number {}", mapper); 18 | } 19 | } 20 | } 21 | 22 | pub fn read_u8(&self, addr: u16) -> u8 { 23 | match self { 24 | Mapper::Nrom(mapper) => mapper.read_u8(addr), 25 | Mapper::Uxrom(mapper) => mapper.read_u8(addr), 26 | } 27 | } 28 | 29 | pub fn write_u8(&self, addr: u16, value: u8) { 30 | match self { 31 | Mapper::Nrom(mapper) => mapper.write_u8(addr, value), 32 | Mapper::Uxrom(mapper) => mapper.write_u8(addr, value), 33 | } 34 | } 35 | 36 | pub fn read_ppu_u8(&self, nes: &Nes, addr: u16) -> u8 { 37 | match self { 38 | Mapper::Nrom(mapper) => mapper.read_ppu_u8(nes, addr), 39 | Mapper::Uxrom(mapper) => mapper.read_ppu_u8(nes, addr), 40 | } 41 | } 42 | 43 | pub fn write_ppu_u8(&self, nes: &Nes, addr: u16, value: u8) { 44 | match self { 45 | Mapper::Nrom(mapper) => mapper.write_ppu_u8(nes, addr, value), 46 | Mapper::Uxrom(mapper) => mapper.write_ppu_u8(nes, addr, value), 47 | } 48 | } 49 | } 50 | 51 | #[derive(Clone)] 52 | pub struct NromMapper { 53 | rom: Rom, 54 | work_ram: Cell<[u8; 0x2000]>, 55 | chr_ram: Vec>, 56 | } 57 | 58 | impl NromMapper { 59 | pub fn from_rom(rom: Rom) -> Self { 60 | let work_ram = Cell::new([0; 0x2000]); 61 | let chr_ram = vec![Cell::new(0); rom.header.chr_ram_size_bytes]; 62 | 63 | NromMapper { 64 | rom, 65 | work_ram, 66 | chr_ram, 67 | } 68 | } 69 | 70 | pub fn read_u8(&self, addr: u16) -> u8 { 71 | let work_ram = self.work_ram(); 72 | let prg_rom = &self.rom.prg_rom; 73 | 74 | match addr { 75 | 0x0000..=0x5FFF => { 76 | panic!("Tried to read from mapper at address ${:04X}", addr); 77 | } 78 | 0x6000..=0x7FFF => { 79 | let offset = ((addr - 0x6000) as usize) % work_ram.len(); 80 | work_ram[offset].get() 81 | } 82 | 0x8000..=0xFFFF => { 83 | let offset = ((addr - 0x8000) as usize) % prg_rom.len(); 84 | prg_rom[offset] 85 | } 86 | } 87 | } 88 | 89 | pub fn write_u8(&self, addr: u16, value: u8) { 90 | let work_ram = self.work_ram(); 91 | 92 | match addr { 93 | 0x0000..=0x5FFF => { 94 | panic!("Tried to write to mapper at address ${:04X}", addr); 95 | } 96 | 0x6000..=0x7FFF => { 97 | let offset = ((addr - 0x6000) as usize) % work_ram.len(); 98 | work_ram[offset].set(value); 99 | } 100 | 0x8000..=0xFFFF => {} 101 | } 102 | } 103 | 104 | pub fn read_ppu_u8(&self, nes: &Nes, addr: u16) -> u8 { 105 | let chr_rom = &self.rom.chr_rom; 106 | let chr_ram = &self.chr_ram; 107 | let ppu_ram = nes.ppu.ppu_ram(); 108 | match addr { 109 | 0x0000..=0x1FFF => { 110 | if chr_rom.is_empty() { 111 | let offset = (addr as usize) % chr_ram.len(); 112 | chr_ram[offset].get() 113 | } else { 114 | let offset = (addr as usize) % chr_rom.len(); 115 | chr_rom[offset] 116 | } 117 | } 118 | 0x2000..=0x2FFF => { 119 | let offset = ((addr - 0x2000) as usize) % ppu_ram.len(); 120 | ppu_ram[offset].get() 121 | } 122 | 0x3000..=0x3EFF => { 123 | let offset = (addr - 0x3000) % 0x0FFF; 124 | self.read_ppu_u8(nes, offset + 0x2000) 125 | } 126 | 0x3F00..=0xFFFF => { 127 | unreachable!(); 128 | } 129 | } 130 | } 131 | 132 | pub fn write_ppu_u8(&self, nes: &Nes, addr: u16, value: u8) { 133 | let ppu_ram = nes.ppu.ppu_ram(); 134 | let chr_rom = &self.rom.chr_rom; 135 | let chr_ram = &self.chr_ram; 136 | match addr { 137 | 0x0000..=0x1FFF => { 138 | if chr_rom.is_empty() { 139 | let offset = (addr as usize) % chr_ram.len(); 140 | chr_ram[offset].set(value); 141 | } else { 142 | // Do nothing-- tried to write to read-only CHR ROM 143 | } 144 | } 145 | 0x2000..=0x2FFF => { 146 | let offset = ((addr - 0x2000) as usize) % ppu_ram.len(); 147 | ppu_ram[offset].set(value) 148 | } 149 | 0x3000..=0x3EFF => { 150 | let offset = (addr - 0x3000) % 0x0FFF; 151 | self.write_ppu_u8(nes, offset + 0x2000, value) 152 | } 153 | 0x3F00..=0xFFFF => { 154 | unreachable!(); 155 | } 156 | } 157 | } 158 | 159 | fn work_ram(&self) -> &[Cell] { 160 | let work_ram: &Cell<[u8]> = &self.work_ram; 161 | work_ram.as_slice_of_cells() 162 | } 163 | } 164 | 165 | #[derive(Clone)] 166 | pub struct UxromMapper { 167 | rom: Rom, 168 | bank: Cell, 169 | work_ram: Cell<[u8; 0x2000]>, 170 | chr_ram: Vec>, 171 | } 172 | 173 | impl UxromMapper { 174 | pub fn from_rom(rom: Rom) -> Self { 175 | let work_ram = Cell::new([0; 0x2000]); 176 | let chr_ram = vec![Cell::new(0); rom.header.chr_ram_size_bytes]; 177 | let bank = Cell::new(5); 178 | 179 | UxromMapper { 180 | rom, 181 | bank, 182 | work_ram, 183 | chr_ram, 184 | } 185 | } 186 | 187 | pub fn banks<'a>(&'a self) -> impl ExactSizeIterator + 'a { 188 | self.rom.prg_rom.chunks(16_384) 189 | } 190 | 191 | pub fn read_u8(&self, addr: u16) -> u8 { 192 | let work_ram = self.work_ram(); 193 | let mut banks = self.banks(); 194 | 195 | match addr { 196 | 0x0000..=0x5FFF => { 197 | panic!("Tried to read from mapper at address ${:04X}", addr); 198 | } 199 | 0x6000..=0x7FFF => { 200 | let offset = ((addr - 0x6000) as usize) % work_ram.len(); 201 | work_ram[offset].get() 202 | } 203 | 0x8000..=0xBFFF => { 204 | let n = self.bank.get(); 205 | let bank = banks.nth(n).unwrap(); 206 | let offset = ((addr - 0x8000) as usize) % bank.len(); 207 | bank[offset] 208 | } 209 | 0xC000..=0xFFFF => { 210 | let bank = banks.last().unwrap(); 211 | let offset = ((addr - 0xC000) as usize) % bank.len(); 212 | bank[offset] 213 | } 214 | } 215 | } 216 | 217 | pub fn write_u8(&self, addr: u16, value: u8) { 218 | let work_ram = self.work_ram(); 219 | let banks = self.banks(); 220 | 221 | match addr { 222 | 0x0000..=0x5FFF => { 223 | panic!("Tried to write to mapper at address ${:04X}", addr); 224 | } 225 | 0x6000..=0x7FFF => { 226 | let offset = ((addr - 0x6000) as usize) % work_ram.len(); 227 | work_ram[offset].set(value); 228 | } 229 | 0x8000..=0xFFFF => { 230 | let new_bank = (value & 0b_0000_1111) as usize; 231 | if new_bank > banks.len() { 232 | unimplemented!("UxROM tried to select bank by writing byte 0x{:02X}, but ROM only has {} bank(s)", value, banks.len()); 233 | } 234 | 235 | self.bank.set(new_bank); 236 | } 237 | } 238 | } 239 | 240 | pub fn read_ppu_u8(&self, nes: &Nes, addr: u16) -> u8 { 241 | let chr_rom = &self.rom.chr_rom; 242 | let chr_ram = &self.chr_ram; 243 | let ppu_ram = nes.ppu.ppu_ram(); 244 | match addr { 245 | 0x0000..=0x1FFF => { 246 | if chr_rom.is_empty() { 247 | let offset = (addr as usize) % chr_ram.len(); 248 | chr_ram[offset].get() 249 | } else { 250 | let offset = (addr as usize) % chr_rom.len(); 251 | chr_rom[offset] 252 | } 253 | } 254 | 0x2000..=0x2FFF => { 255 | let offset = ((addr - 0x2000) as usize) % ppu_ram.len(); 256 | ppu_ram[offset].get() 257 | } 258 | 0x3000..=0x3EFF => { 259 | let offset = (addr - 0x3000) % 0x0FFF; 260 | self.read_ppu_u8(nes, offset + 0x2000) 261 | } 262 | 0x3F00..=0xFFFF => { 263 | unreachable!(); 264 | } 265 | } 266 | } 267 | 268 | pub fn write_ppu_u8(&self, nes: &Nes, addr: u16, value: u8) { 269 | let ppu_ram = nes.ppu.ppu_ram(); 270 | let chr_rom = &self.rom.chr_rom; 271 | let chr_ram = &self.chr_ram; 272 | match addr { 273 | 0x0000..=0x1FFF => { 274 | if chr_rom.is_empty() { 275 | let offset = (addr as usize) % chr_ram.len(); 276 | chr_ram[offset].set(value); 277 | } else { 278 | // Do nothing-- tried to write to read-only CHR ROM 279 | } 280 | } 281 | 0x2000..=0x2FFF => { 282 | let offset = ((addr - 0x2000) as usize) % ppu_ram.len(); 283 | ppu_ram[offset].set(value) 284 | } 285 | 0x3000..=0x3EFF => { 286 | let offset = (addr - 0x3000) % 0x0FFF; 287 | self.write_ppu_u8(nes, offset + 0x2000, value) 288 | } 289 | 0x3F00..=0xFFFF => { 290 | unreachable!(); 291 | } 292 | } 293 | } 294 | 295 | fn work_ram(&self) -> &[Cell] { 296 | let work_ram: &Cell<[u8]> = &self.work_ram; 297 | work_ram.as_slice_of_cells() 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/nes/ppu.rs: -------------------------------------------------------------------------------- 1 | use crate::nes::{Nes, NesIo}; 2 | use crate::video::{Color, Point, Video}; 3 | use bitflags::bitflags; 4 | use std::cell::Cell; 5 | use std::ops::{Generator, GeneratorState}; 6 | use std::pin::Pin; 7 | use std::u8; 8 | 9 | #[derive(Clone)] 10 | pub struct Ppu { 11 | pub ctrl: Cell, 12 | pub mask: Cell, 13 | pub status: Cell, 14 | pub oam_addr: Cell, 15 | pub scroll: Cell, 16 | pub addr: Cell, 17 | 18 | // Latch used for writing to PPUSCROLL and PPUADDR (toggles after a write 19 | // to each, used to determine if the high bit or low bit is being written). 20 | pub scroll_addr_latch: Cell, 21 | 22 | pub ppu_ram: Cell<[u8; 0x0800]>, 23 | pub oam: Cell<[u8; 0x0100]>, 24 | pub palette_ram: Cell<[u8; 0x20]>, 25 | 26 | // Internal "cache" indexed by an X coordinate, returning a bitfield 27 | // that represents the sprites that should be rendered for that 28 | // X coordinate. For example, a value of 0b_0100_0001 at index 5 means that 29 | // the sprites at index 0 and 6 should be rendered at pixel 5 of 30 | // the scanline (since bits 0 and 6 are set) 31 | scanline_sprite_indices: Cell<[u64; 256]>, 32 | } 33 | 34 | impl Ppu { 35 | pub fn new() -> Self { 36 | Ppu { 37 | ctrl: Cell::new(PpuCtrlFlags::from_bits_truncate(0x00)), 38 | mask: Cell::new(PpuMaskFlags::from_bits_truncate(0x00)), 39 | status: Cell::new(PpuStatusFlags::from_bits_truncate(0x00)), 40 | oam_addr: Cell::new(0x00), 41 | scroll: Cell::new(0x0000), 42 | addr: Cell::new(0x0000), 43 | scroll_addr_latch: Cell::new(false), 44 | ppu_ram: Cell::new([0; 0x0800]), 45 | oam: Cell::new([0; 0x0100]), 46 | palette_ram: Cell::new([0; 0x20]), 47 | scanline_sprite_indices: Cell::new([0; 256]), 48 | } 49 | } 50 | 51 | pub fn ppu_ram(&self) -> &[Cell] { 52 | let ppu_ram: &Cell<[u8]> = &self.ppu_ram; 53 | ppu_ram.as_slice_of_cells() 54 | } 55 | 56 | fn oam(&self) -> &[Cell] { 57 | let oam: &Cell<[u8]> = &self.oam; 58 | oam.as_slice_of_cells() 59 | } 60 | 61 | pub fn palette_ram(&self) -> &[Cell] { 62 | let palette_ram: &Cell<[u8]> = &self.palette_ram; 63 | palette_ram.as_slice_of_cells() 64 | } 65 | 66 | pub fn set_ppuctrl(&self, value: u8) { 67 | self.ctrl.set(PpuCtrlFlags::from_bits_truncate(value)); 68 | } 69 | 70 | pub fn set_ppumask(&self, value: u8) { 71 | self.mask.set(PpuMaskFlags::from_bits_truncate(value)); 72 | } 73 | 74 | pub fn write_oamaddr(&self, value: u8) { 75 | self.oam_addr.set(value); 76 | } 77 | 78 | pub fn write_oamdata(&self, value: u8) { 79 | let oam_addr = self.oam_addr.get(); 80 | let oam = self.oam(); 81 | 82 | oam[oam_addr as usize].set(value); 83 | 84 | let next_oam_addr = oam_addr.wrapping_add(1) as usize % oam.len(); 85 | self.oam_addr.set(next_oam_addr as u8); 86 | } 87 | 88 | pub fn write_ppuscroll(&self, value: u8) { 89 | let latch = self.scroll_addr_latch.get(); 90 | 91 | if latch { 92 | let scroll_lo = self.scroll.get() & 0x00FF; 93 | let scroll_hi = (value as u16) << 8; 94 | self.scroll.set(scroll_lo | scroll_hi); 95 | } else { 96 | let scroll_lo = value as u16; 97 | let scroll_hi = self.scroll.get() & 0xFF00; 98 | self.scroll.set(scroll_lo | scroll_hi); 99 | } 100 | 101 | self.scroll_addr_latch.set(!latch); 102 | } 103 | 104 | pub fn write_ppuaddr(&self, value: u8) { 105 | let latch = self.scroll_addr_latch.get(); 106 | 107 | if latch { 108 | let addr_lo = value as u16; 109 | let addr_hi = self.addr.get() & 0xFF00; 110 | self.addr.set(addr_lo | addr_hi); 111 | } else { 112 | let addr_lo = self.addr.get() & 0x00FF; 113 | let addr_hi = (value as u16) << 8; 114 | self.addr.set(addr_lo | addr_hi); 115 | } 116 | 117 | self.scroll_addr_latch.set(!latch); 118 | } 119 | 120 | pub fn read_ppudata(&self, nes: &Nes) -> u8 { 121 | let addr = self.addr.get(); 122 | let ctrl = self.ctrl.get(); 123 | let stride = 124 | // Add 1 to the PPU address if the I flag is clear, add 32 if 125 | // it is set 126 | match ctrl.contains(PpuCtrlFlags::VRAM_ADDR_INCREMENT) { 127 | false => 1, 128 | true => 32 129 | }; 130 | 131 | let value = nes.read_ppu_u8(addr); 132 | self.addr.update(|addr| addr.wrapping_add(stride)); 133 | 134 | value 135 | } 136 | 137 | pub fn write_ppudata(&self, nes: &Nes, value: u8) { 138 | let addr = self.addr.get(); 139 | let ctrl = self.ctrl.get(); 140 | let stride = 141 | // Add 1 to the PPU address if the I flag is clear, add 32 if 142 | // it is set 143 | match ctrl.contains(PpuCtrlFlags::VRAM_ADDR_INCREMENT) { 144 | false => 1, 145 | true => 32 146 | }; 147 | 148 | nes.write_ppu_u8(addr, value); 149 | self.addr.update(|addr| addr.wrapping_add(stride)); 150 | } 151 | 152 | pub fn ppustatus(&self) -> u8 { 153 | self.status.get().bits() 154 | } 155 | 156 | pub fn run<'a>(nes: &'a Nes) -> impl Generator + 'a { 157 | let mut run_sprite_evaluation = Ppu::run_sprite_evaluation(nes); 158 | let mut run_renderer = Ppu::run_renderer(nes); 159 | 160 | move || loop { 161 | loop { 162 | match Pin::new(&mut run_sprite_evaluation).resume(()) { 163 | GeneratorState::Yielded(PpuStep::Cycle) => { 164 | break; 165 | } 166 | GeneratorState::Yielded(step) => { 167 | yield step; 168 | } 169 | } 170 | } 171 | loop { 172 | match Pin::new(&mut run_renderer).resume(()) { 173 | GeneratorState::Yielded(PpuStep::Cycle) => { 174 | break; 175 | } 176 | GeneratorState::Yielded(step) => { 177 | yield step; 178 | } 179 | } 180 | } 181 | 182 | yield PpuStep::Cycle; 183 | } 184 | } 185 | 186 | fn run_sprite_evaluation<'a>( 187 | nes: &'a Nes, 188 | ) -> impl Generator + 'a { 189 | move || loop { 190 | for frame in 0_u64.. { 191 | let frame_is_odd = frame % 2 != 0; 192 | for scanline in 0_u16..=261 { 193 | let y = scanline; 194 | let should_skip_first_cycle = frame_is_odd && scanline == 0; 195 | if !should_skip_first_cycle { 196 | // The first cycle of each scanline is idle (except 197 | // for the first cycle of the pre-render scanline 198 | // for odd frames, which is skipped) 199 | yield PpuStep::Cycle; 200 | } 201 | 202 | // TODO: Implement sprite evaluation with secondary OAM! 203 | 204 | let oam = nes.ppu.oam(); 205 | for _ in 0_u16..340 { 206 | // Here, secondary OAM would be filled with sprite data 207 | yield PpuStep::Cycle; 208 | } 209 | 210 | if scanline < 240 { 211 | let mut new_sprite_indices = [0; 256]; 212 | for sprite_index in 0_u8..64 { 213 | let oam_index = sprite_index as usize * 4; 214 | let sprite_y = oam[oam_index].get() as u16; 215 | if sprite_y <= y && y < sprite_y + 8 { 216 | let sprite_x = oam[oam_index + 3].get() as u16; 217 | 218 | for sprite_x_offset in 0_u16..8 { 219 | let x = (sprite_x + sprite_x_offset) as usize; 220 | if x < 256 { 221 | let sprite_bitmask = 1 << sprite_index; 222 | new_sprite_indices[x] |= sprite_bitmask; 223 | } 224 | } 225 | } 226 | } 227 | 228 | nes.ppu.scanline_sprite_indices.set(new_sprite_indices); 229 | } else { 230 | let new_sprite_indices = [0; 256]; 231 | nes.ppu.scanline_sprite_indices.set(new_sprite_indices); 232 | } 233 | } 234 | } 235 | } 236 | } 237 | 238 | fn run_renderer<'a>( 239 | nes: &'a Nes, 240 | ) -> impl Generator + 'a { 241 | move || loop { 242 | for frame in 0_u64.. { 243 | let frame_is_odd = frame % 2 != 0; 244 | for scanline in 0_u16..=261 { 245 | let tile_y = scanline / 8; 246 | let tile_y_pixel = scanline % 8; 247 | let y = scanline; 248 | 249 | let sprite_indices = nes.ppu.scanline_sprite_indices.get(); 250 | let oam = nes.ppu.oam(); 251 | 252 | let should_skip_first_cycle = frame_is_odd && scanline == 0; 253 | if !should_skip_first_cycle { 254 | // The first cycle of each scanline is idle (except 255 | // for the first cycle of the pre-render scanline 256 | // for odd frames, which is skipped) 257 | yield PpuStep::Cycle; 258 | } 259 | 260 | if scanline == 240 { 261 | let _ = nes.ppu.status.update(|mut status| { 262 | status.set(PpuStatusFlags::VBLANK_STARTED, true); 263 | status 264 | }); 265 | let ctrl = nes.ppu.ctrl.get(); 266 | if ctrl.contains(PpuCtrlFlags::VBLANK_INTERRUPT) { 267 | // TODO: Generate NMI immediately if 268 | // VBLANK_INTERRUPT is toggled during vblank 269 | nes.cpu.nmi.set(true); 270 | } 271 | nes.io.video().present(); 272 | yield PpuStep::Vblank; 273 | } else if scanline == 0 { 274 | let _ = nes.ppu.status.update(|mut status| { 275 | status.set(PpuStatusFlags::VBLANK_STARTED, false); 276 | status 277 | }); 278 | nes.io.video().clear(); 279 | } 280 | 281 | for tile_x in 0_u16..42 { 282 | if tile_x >= 32 || scanline >= 240 { 283 | // TODO: Implement sprite tile fetching here 284 | 285 | for _cycle in 0..8 { 286 | yield PpuStep::Cycle; 287 | } 288 | 289 | continue; 290 | } 291 | 292 | let scroll = nes.ppu.scroll.get(); 293 | let scroll_x = scroll & 0x00FF; 294 | let tile_offset = scroll_x / 8; 295 | let tile_x_pixel_offset = scroll_x % 8; 296 | let scroll_tile_x = tile_x + tile_offset; 297 | 298 | yield PpuStep::Cycle; 299 | yield PpuStep::Cycle; 300 | let nametable_index = tile_y * 32 + scroll_tile_x; 301 | let nametable_byte = nes.read_ppu_u8(0x2000 + nametable_index); 302 | 303 | yield PpuStep::Cycle; 304 | yield PpuStep::Cycle; 305 | let attr_x = scroll_tile_x / 4; 306 | let attr_y = tile_y / 4; 307 | let attr_is_left = ((scroll_tile_x / 2) % 2) == 0; 308 | let attr_is_top = ((tile_y / 2) % 2) == 0; 309 | 310 | let attr_index = attr_y * 8 + attr_x; 311 | let attr = nes.read_ppu_u8(0x23C0 + attr_index); 312 | let background_palette_index = match (attr_is_top, attr_is_left) { 313 | (true, true) => attr & 0b_0000_0011, 314 | (true, false) => (attr & 0b_0000_1100) >> 2, 315 | (false, true) => (attr & 0b_0011_0000) >> 4, 316 | (false, false) => (attr & 0b_1100_0000) >> 6, 317 | }; 318 | 319 | let pattern_table_offset = if nes 320 | .ppu 321 | .ctrl 322 | .get() 323 | .contains(PpuCtrlFlags::BACKGROUND_PATTERN_TABLE_ADDR) 324 | { 325 | 0x1000 326 | } else { 327 | 0x0000 328 | }; 329 | let bitmap_offset = pattern_table_offset + nametable_byte as u16 * 16; 330 | let bitmap_lo_byte = nes.read_ppu_u8(bitmap_offset + tile_y_pixel); 331 | 332 | yield PpuStep::Cycle; 333 | yield PpuStep::Cycle; 334 | let bitmap_hi_byte = nes.read_ppu_u8(bitmap_offset + tile_y_pixel + 8); 335 | 336 | yield PpuStep::Cycle; 337 | yield PpuStep::Cycle; 338 | for tile_x_pixel in 0..8 { 339 | let tile_x_pixel_scroll = tile_x_pixel + tile_x_pixel_offset; 340 | let x = (tile_x * 8) + tile_x_pixel; 341 | 342 | let background_color_index = { 343 | let bitmap_bitmask = 0b_1000_0000 >> (tile_x_pixel_scroll % 8); 344 | let bitmap_lo_bit = (bitmap_lo_byte & bitmap_bitmask) != 0; 345 | let bitmap_hi_bit = (bitmap_hi_byte & bitmap_bitmask) != 0; 346 | 347 | let color_index = match (bitmap_hi_bit, bitmap_lo_bit) { 348 | (false, false) => 0, 349 | (false, true) => 1, 350 | (true, false) => 2, 351 | (true, true) => 3, 352 | }; 353 | color_index 354 | }; 355 | 356 | let sprite_palette_and_color_index = { 357 | let sprite_index_bitmask = sprite_indices[x as usize]; 358 | let included_sprites = (0_u64..64).filter(|sprite_index| { 359 | (sprite_index_bitmask & (1_u64 << sprite_index)) != 0 360 | }); 361 | 362 | let mut palette_and_color_indices = 363 | included_sprites.map(|sprite_index| { 364 | let oam_index = (sprite_index * 4) as usize; 365 | let sprite_y = oam[oam_index].get() as u16; 366 | let tile_index = oam[oam_index + 1].get(); 367 | let attrs = oam[oam_index + 2].get(); 368 | let sprite_x = oam[oam_index + 3].get() as u16; 369 | 370 | let flip_horizontal = (attrs & 0b_0100_0000) != 0; 371 | let flip_vertical = (attrs & 0b_1000_0000) != 0; 372 | 373 | let sprite_x_pixel = x.wrapping_sub(sprite_x) % 256; 374 | let sprite_y_pixel = 375 | y.wrapping_sub(1).wrapping_sub(sprite_y) % 256; 376 | 377 | let sprite_x_pixel = if flip_horizontal { 378 | 7 - sprite_x_pixel 379 | } else { 380 | sprite_x_pixel 381 | }; 382 | let sprite_y_pixel = if flip_vertical { 383 | 7 - sprite_y_pixel 384 | } else { 385 | sprite_y_pixel 386 | }; 387 | 388 | let pattern_bitmask = 0b_1000_0000 >> sprite_x_pixel; 389 | let pattern_table_offset = if nes 390 | .ppu 391 | .ctrl 392 | .get() 393 | .contains(PpuCtrlFlags::SPRITE_PATTERN_TABLE_ADDR) 394 | { 395 | 0x1000 396 | } else { 397 | 0x0000 398 | }; 399 | let pattern_offset = 400 | pattern_table_offset as u16 + tile_index as u16 * 16; 401 | let pattern_lo_byte = 402 | nes.read_ppu_u8(pattern_offset + sprite_y_pixel); 403 | let pattern_hi_byte = 404 | nes.read_ppu_u8(pattern_offset + sprite_y_pixel + 8); 405 | let pattern_lo_bit = 406 | (pattern_lo_byte & pattern_bitmask) != 0; 407 | let pattern_hi_bit = 408 | (pattern_hi_byte & pattern_bitmask) != 0; 409 | 410 | let palette_lo_bit = (attrs & 0b_0000_0001) != 0; 411 | let palette_hi_bit = (attrs & 0b_0000_0010) != 0; 412 | 413 | let palette_index = match (palette_hi_bit, palette_lo_bit) { 414 | (false, false) => 4, 415 | (false, true) => 5, 416 | (true, false) => 6, 417 | (true, true) => 7, 418 | }; 419 | 420 | let color_index = match (pattern_hi_bit, pattern_lo_bit) { 421 | (false, false) => 0, 422 | (false, true) => 1, 423 | (true, false) => 2, 424 | (true, true) => 3, 425 | }; 426 | 427 | (palette_index, color_index) 428 | }); 429 | 430 | let palette_and_color_index = palette_and_color_indices 431 | .find(|&(_, color_index)| color_index != 0); 432 | 433 | palette_and_color_index 434 | }; 435 | 436 | let color_code = match sprite_palette_and_color_index { 437 | None | Some((_, 0)) => nes.ppu.palette_index_to_nes_color_code( 438 | background_palette_index, 439 | background_color_index, 440 | ), 441 | Some((sprite_palette_index, sprite_color_index)) => { 442 | nes.ppu.palette_index_to_nes_color_code( 443 | sprite_palette_index, 444 | sprite_color_index, 445 | ) 446 | } 447 | }; 448 | let color = nes_color_code_to_rgb(color_code); 449 | let point = Point { x, y }; 450 | nes.io.video().draw_point(point, color); 451 | } 452 | } 453 | 454 | for _ in 0..4 { 455 | // TODO: Implement PPU garbage reads 456 | yield PpuStep::Cycle; 457 | } 458 | } 459 | } 460 | } 461 | } 462 | 463 | fn palette_index_to_nes_color_code(&self, palette_index: u8, color_index: u8) -> u8 { 464 | let palette_ram = self.palette_ram(); 465 | let palette_ram_indices = [ 466 | [0x00, 0x01, 0x02, 0x03], 467 | [0x00, 0x05, 0x06, 0x07], 468 | [0x00, 0x09, 0x0A, 0x0B], 469 | [0x00, 0x0D, 0x0E, 0x0F], 470 | [0x00, 0x11, 0x12, 0x13], 471 | [0x00, 0x15, 0x16, 0x17], 472 | [0x00, 0x19, 0x1A, 0x1B], 473 | [0x00, 0x1D, 0x1E, 0x1F], 474 | ]; 475 | let palettes = [ 476 | [ 477 | palette_ram[palette_ram_indices[0][0]].get(), 478 | palette_ram[palette_ram_indices[0][1]].get(), 479 | palette_ram[palette_ram_indices[0][2]].get(), 480 | palette_ram[palette_ram_indices[0][3]].get(), 481 | ], 482 | [ 483 | palette_ram[palette_ram_indices[1][0]].get(), 484 | palette_ram[palette_ram_indices[1][1]].get(), 485 | palette_ram[palette_ram_indices[1][2]].get(), 486 | palette_ram[palette_ram_indices[1][3]].get(), 487 | ], 488 | [ 489 | palette_ram[palette_ram_indices[2][0]].get(), 490 | palette_ram[palette_ram_indices[2][1]].get(), 491 | palette_ram[palette_ram_indices[2][2]].get(), 492 | palette_ram[palette_ram_indices[2][3]].get(), 493 | ], 494 | [ 495 | palette_ram[palette_ram_indices[3][0]].get(), 496 | palette_ram[palette_ram_indices[3][1]].get(), 497 | palette_ram[palette_ram_indices[3][2]].get(), 498 | palette_ram[palette_ram_indices[3][3]].get(), 499 | ], 500 | [ 501 | palette_ram[palette_ram_indices[4][0]].get(), 502 | palette_ram[palette_ram_indices[4][1]].get(), 503 | palette_ram[palette_ram_indices[4][2]].get(), 504 | palette_ram[palette_ram_indices[4][3]].get(), 505 | ], 506 | [ 507 | palette_ram[palette_ram_indices[5][0]].get(), 508 | palette_ram[palette_ram_indices[5][1]].get(), 509 | palette_ram[palette_ram_indices[5][2]].get(), 510 | palette_ram[palette_ram_indices[5][3]].get(), 511 | ], 512 | [ 513 | palette_ram[palette_ram_indices[6][0]].get(), 514 | palette_ram[palette_ram_indices[6][1]].get(), 515 | palette_ram[palette_ram_indices[6][2]].get(), 516 | palette_ram[palette_ram_indices[6][3]].get(), 517 | ], 518 | [ 519 | palette_ram[palette_ram_indices[7][0]].get(), 520 | palette_ram[palette_ram_indices[7][1]].get(), 521 | palette_ram[palette_ram_indices[7][2]].get(), 522 | palette_ram[palette_ram_indices[7][3]].get(), 523 | ], 524 | ]; 525 | let palette = palettes[palette_index as usize]; 526 | palette[color_index as usize] 527 | } 528 | } 529 | 530 | pub enum PpuStep { 531 | Cycle, 532 | Vblank, 533 | } 534 | 535 | fn nes_color_code_to_rgb(color_code: u8) -> Color { 536 | // Based on the palette provided on the NesDev wiki: 537 | // - https://wiki.nesdev.com/w/index.php/PPU_palettes 538 | // - https://wiki.nesdev.com/w/index.php/File:Savtool-swatches.png 539 | match color_code & 0x3F { 540 | 0x00 => Color { 541 | r: 0x54, 542 | g: 0x54, 543 | b: 0x54, 544 | }, 545 | 0x01 => Color { 546 | r: 0x00, 547 | g: 0x1E, 548 | b: 0x74, 549 | }, 550 | 0x02 => Color { 551 | r: 0x08, 552 | g: 0x10, 553 | b: 0x90, 554 | }, 555 | 0x03 => Color { 556 | r: 0x30, 557 | g: 0x00, 558 | b: 0x88, 559 | }, 560 | 0x04 => Color { 561 | r: 0x44, 562 | g: 0x00, 563 | b: 0x64, 564 | }, 565 | 0x05 => Color { 566 | r: 0x5C, 567 | g: 0x00, 568 | b: 0x30, 569 | }, 570 | 0x06 => Color { 571 | r: 0x54, 572 | g: 0x04, 573 | b: 0x00, 574 | }, 575 | 0x07 => Color { 576 | r: 0x3C, 577 | g: 0x18, 578 | b: 0x00, 579 | }, 580 | 0x08 => Color { 581 | r: 0x20, 582 | g: 0x2A, 583 | b: 0x00, 584 | }, 585 | 0x09 => Color { 586 | r: 0x08, 587 | g: 0x3A, 588 | b: 0x00, 589 | }, 590 | 0x0A => Color { 591 | r: 0x00, 592 | g: 0x40, 593 | b: 0x00, 594 | }, 595 | 0x0B => Color { 596 | r: 0x00, 597 | g: 0x3C, 598 | b: 0x00, 599 | }, 600 | 0x0C => Color { 601 | r: 0x00, 602 | g: 0x32, 603 | b: 0x3C, 604 | }, 605 | 0x0D => Color { 606 | r: 0x00, 607 | g: 0x00, 608 | b: 0x00, 609 | }, 610 | 0x0E => Color { 611 | r: 0x00, 612 | g: 0x00, 613 | b: 0x00, 614 | }, 615 | 0x0F => Color { 616 | r: 0x00, 617 | g: 0x00, 618 | b: 0x00, 619 | }, 620 | 0x10 => Color { 621 | r: 0x98, 622 | g: 0x96, 623 | b: 0x98, 624 | }, 625 | 0x11 => Color { 626 | r: 0x08, 627 | g: 0x4C, 628 | b: 0xC4, 629 | }, 630 | 0x12 => Color { 631 | r: 0x30, 632 | g: 0x32, 633 | b: 0xEC, 634 | }, 635 | 0x13 => Color { 636 | r: 0x5C, 637 | g: 0x1E, 638 | b: 0xE4, 639 | }, 640 | 0x14 => Color { 641 | r: 0x88, 642 | g: 0x14, 643 | b: 0xB0, 644 | }, 645 | 0x15 => Color { 646 | r: 0xA0, 647 | g: 0x14, 648 | b: 0x64, 649 | }, 650 | 0x16 => Color { 651 | r: 0x98, 652 | g: 0x22, 653 | b: 0x20, 654 | }, 655 | 0x17 => Color { 656 | r: 0x78, 657 | g: 0x3C, 658 | b: 0x00, 659 | }, 660 | 0x18 => Color { 661 | r: 0x54, 662 | g: 0x5A, 663 | b: 0x00, 664 | }, 665 | 0x19 => Color { 666 | r: 0x28, 667 | g: 0x72, 668 | b: 0x00, 669 | }, 670 | 0x1A => Color { 671 | r: 0x08, 672 | g: 0x7C, 673 | b: 0x00, 674 | }, 675 | 0x1B => Color { 676 | r: 0x00, 677 | g: 0x76, 678 | b: 0x28, 679 | }, 680 | 0x1C => Color { 681 | r: 0x00, 682 | g: 0x66, 683 | b: 0x78, 684 | }, 685 | 0x1D => Color { 686 | r: 0x00, 687 | g: 0x00, 688 | b: 0x00, 689 | }, 690 | 0x1E => Color { 691 | r: 0x00, 692 | g: 0x00, 693 | b: 0x00, 694 | }, 695 | 0x1F => Color { 696 | r: 0x00, 697 | g: 0x00, 698 | b: 0x00, 699 | }, 700 | 0x20 => Color { 701 | r: 0xEC, 702 | g: 0xEE, 703 | b: 0xEC, 704 | }, 705 | 0x21 => Color { 706 | r: 0x4C, 707 | g: 0x9A, 708 | b: 0xEC, 709 | }, 710 | 0x22 => Color { 711 | r: 0x78, 712 | g: 0x7C, 713 | b: 0xEC, 714 | }, 715 | 0x23 => Color { 716 | r: 0xB0, 717 | g: 0x62, 718 | b: 0xEC, 719 | }, 720 | 0x24 => Color { 721 | r: 0xE4, 722 | g: 0x54, 723 | b: 0xEC, 724 | }, 725 | 0x25 => Color { 726 | r: 0xEC, 727 | g: 0x58, 728 | b: 0xB4, 729 | }, 730 | 0x26 => Color { 731 | r: 0xEC, 732 | g: 0x6A, 733 | b: 0x64, 734 | }, 735 | 0x27 => Color { 736 | r: 0xD4, 737 | g: 0x88, 738 | b: 0x20, 739 | }, 740 | 0x28 => Color { 741 | r: 0xA0, 742 | g: 0xAA, 743 | b: 0x00, 744 | }, 745 | 0x29 => Color { 746 | r: 0x74, 747 | g: 0xC4, 748 | b: 0x00, 749 | }, 750 | 0x2A => Color { 751 | r: 0x4C, 752 | g: 0xD0, 753 | b: 0x20, 754 | }, 755 | 0x2B => Color { 756 | r: 0x38, 757 | g: 0xCC, 758 | b: 0x6C, 759 | }, 760 | 0x2C => Color { 761 | r: 0x38, 762 | g: 0xB4, 763 | b: 0xCC, 764 | }, 765 | 0x2D => Color { 766 | r: 0x3C, 767 | g: 0x3C, 768 | b: 0x3C, 769 | }, 770 | 0x2E => Color { 771 | r: 0x00, 772 | g: 0x00, 773 | b: 0x00, 774 | }, 775 | 0x2F => Color { 776 | r: 0x00, 777 | g: 0x00, 778 | b: 0x00, 779 | }, 780 | 0x30 => Color { 781 | r: 0xEC, 782 | g: 0xEE, 783 | b: 0xEC, 784 | }, 785 | 0x31 => Color { 786 | r: 0xA8, 787 | g: 0xCC, 788 | b: 0xEC, 789 | }, 790 | 0x32 => Color { 791 | r: 0xBC, 792 | g: 0xBC, 793 | b: 0xEC, 794 | }, 795 | 0x33 => Color { 796 | r: 0xD4, 797 | g: 0xB2, 798 | b: 0xEC, 799 | }, 800 | 0x34 => Color { 801 | r: 0xEC, 802 | g: 0xAE, 803 | b: 0xEC, 804 | }, 805 | 0x35 => Color { 806 | r: 0xEC, 807 | g: 0xAE, 808 | b: 0xD4, 809 | }, 810 | 0x36 => Color { 811 | r: 0xEC, 812 | g: 0xB4, 813 | b: 0xB0, 814 | }, 815 | 0x37 => Color { 816 | r: 0xE4, 817 | g: 0xC4, 818 | b: 0x90, 819 | }, 820 | 0x38 => Color { 821 | r: 0xCC, 822 | g: 0xD2, 823 | b: 0x78, 824 | }, 825 | 0x39 => Color { 826 | r: 0xB4, 827 | g: 0xDE, 828 | b: 0x78, 829 | }, 830 | 0x3A => Color { 831 | r: 0xA8, 832 | g: 0xE2, 833 | b: 0x90, 834 | }, 835 | 0x3B => Color { 836 | r: 0x98, 837 | g: 0xE2, 838 | b: 0xB4, 839 | }, 840 | 0x3C => Color { 841 | r: 0xA0, 842 | g: 0xD6, 843 | b: 0xE4, 844 | }, 845 | 0x3D => Color { 846 | r: 0xA0, 847 | g: 0xA2, 848 | b: 0xA0, 849 | }, 850 | 0x3E => Color { 851 | r: 0x00, 852 | g: 0x00, 853 | b: 0x00, 854 | }, 855 | 0x3F => Color { 856 | r: 0x00, 857 | g: 0x00, 858 | b: 0x00, 859 | }, 860 | _ => { 861 | unreachable!(); 862 | } 863 | } 864 | } 865 | 866 | bitflags! { 867 | pub struct PpuCtrlFlags: u8 { 868 | const NAMETABLE_LO = 1 << 0; 869 | const NAMETABLE_HI = 1 << 1; 870 | const VRAM_ADDR_INCREMENT = 1 << 2; 871 | const SPRITE_PATTERN_TABLE_ADDR = 1 << 3; 872 | const BACKGROUND_PATTERN_TABLE_ADDR = 1 << 4; 873 | const SPRITE_SIZE = 1 << 5; 874 | const PPU_MASTER_SLAVE_SELECT = 1 << 6; 875 | const VBLANK_INTERRUPT = 1 << 7; 876 | } 877 | } 878 | 879 | bitflags! { 880 | pub struct PpuMaskFlags: u8 { 881 | const GREYSCALE = 1 << 0; 882 | const SHOW_BACKGROUND_IN_LEFT_MARGIN = 1 << 1; 883 | const SHOW_SPRITES_IN_LEFT_MARGIN = 1 << 2; 884 | const SHOW_BACKGROUND = 1 << 3; 885 | const SHOW_SPRITES = 1 << 4; 886 | const EMPHASIZE_RED = 1 << 5; 887 | const EMPHASIZE_GREEN = 1 << 6; 888 | const EMPHASIZE_BLUE = 1 << 7; 889 | } 890 | } 891 | 892 | bitflags! { 893 | pub struct PpuStatusFlags: u8 { 894 | // NOTE: Bits 0-4 are unused (but result in bits read from 895 | // the PPU's latch) 896 | const SPRITE_OVERFLOW = 1 << 5; 897 | const SPRITE_ZERO_HIT = 1 << 6; 898 | const VBLANK_STARTED = 1 << 7; 899 | } 900 | } 901 | -------------------------------------------------------------------------------- /src/rom.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone)] 2 | pub struct Rom { 3 | pub header: RomHeader, 4 | pub prg_rom: Vec, 5 | pub chr_rom: Vec, 6 | pub title: String, 7 | } 8 | 9 | impl Rom { 10 | pub fn from_bytes(mut bytes: impl Iterator) -> Result { 11 | let mut bytes = &mut bytes; 12 | 13 | let header = RomHeader::from_bytes(&mut bytes)?; 14 | match header { 15 | RomHeader { 16 | prg_rom_size_bytes: _, 17 | chr_rom_size_bytes: _, 18 | chr_ram_size_bytes: _, 19 | mirror_mode: _, 20 | mapper: _, 21 | prg_ram_size_bytes: 0, 22 | has_persistence: false, 23 | has_trainer: false, 24 | is_vs_unisystem: false, 25 | is_playchoice_10: false, 26 | tv_system: TvSystem::Ntsc, 27 | has_bus_conflicts: false, 28 | } => {} 29 | header => { 30 | unimplemented!("ROM with header: {:#?}", header); 31 | } 32 | } 33 | 34 | let prg_rom: Vec<_> = bytes.take(header.prg_rom_size_bytes).collect(); 35 | if prg_rom.len() != header.prg_rom_size_bytes { 36 | return Err(RomError::UnexpectedEof); 37 | } 38 | 39 | let chr_rom: Vec<_> = bytes.take(header.chr_rom_size_bytes).collect(); 40 | if chr_rom.len() != header.chr_rom_size_bytes { 41 | return Err(RomError::UnexpectedEof); 42 | } 43 | 44 | let title: Vec<_> = bytes.take(128).collect(); 45 | let title = String::from_utf8_lossy(&title).into_owned(); 46 | 47 | let eof = bytes.next(); 48 | if eof != None { 49 | return Err(RomError::ExpectedEof); 50 | } 51 | 52 | Ok(Rom { 53 | header, 54 | prg_rom, 55 | chr_rom, 56 | title, 57 | }) 58 | } 59 | } 60 | 61 | #[derive(Debug)] 62 | pub enum RomError { 63 | InvalidHeader, 64 | UnexpectedEof, 65 | ExpectedEof, 66 | } 67 | 68 | #[derive(Debug, Clone, Copy)] 69 | pub struct RomHeader { 70 | prg_rom_size_bytes: usize, 71 | chr_rom_size_bytes: usize, 72 | prg_ram_size_bytes: usize, 73 | pub chr_ram_size_bytes: usize, 74 | pub mirror_mode: MirrorMode, 75 | pub has_persistence: bool, 76 | has_trainer: bool, 77 | pub mapper: u8, 78 | pub is_vs_unisystem: bool, 79 | pub is_playchoice_10: bool, 80 | pub tv_system: TvSystem, 81 | pub has_bus_conflicts: bool, 82 | } 83 | 84 | impl RomHeader { 85 | fn from_bytes(mut bytes: impl Iterator) -> Result { 86 | let expected_magic_str = b"NES\x1A".iter().cloned(); 87 | let actual_magic_str = (&mut bytes).take(4); 88 | if expected_magic_str.ne(actual_magic_str) { 89 | return Err(RomError::InvalidHeader); 90 | } 91 | 92 | let prg_rom_size_field = bytes.next().ok_or_else(|| RomError::UnexpectedEof)?; 93 | let chr_rom_size_field = bytes.next().ok_or_else(|| RomError::UnexpectedEof)?; 94 | let flags_6 = bytes.next().ok_or_else(|| RomError::UnexpectedEof)?; 95 | let flags_7 = bytes.next().ok_or_else(|| RomError::UnexpectedEof)?; 96 | let prg_ram_size_field = bytes.next().ok_or_else(|| RomError::UnexpectedEof)?; 97 | let flags_9 = bytes.next().ok_or_else(|| RomError::UnexpectedEof)?; 98 | let flags_10 = bytes.next().ok_or_else(|| RomError::UnexpectedEof)?; 99 | 100 | let expected_zeros = [0, 0, 0, 0, 0].iter().cloned(); 101 | let actual_zeros = (&mut bytes).take(5); 102 | if expected_zeros.ne(actual_zeros) { 103 | return Err(RomError::InvalidHeader)?; 104 | } 105 | 106 | let flag_mirror_bit = flags_6 & 0b_0000_0001 != 0; 107 | let flag_persistent_bit = flags_6 & 0b_0000_0010 != 0; 108 | let flag_trainer_bit = flags_6 & 0b_0000_0100 != 0; 109 | let flag_four_screen_vram_bit = flags_6 & 0b_0000_1000 != 0; 110 | let flag_mapper_lo = (flags_6 & 0b_1111_0000) >> 4; 111 | 112 | let flag_vs_unisystem = flags_7 & 0b_0000_0001 != 0; 113 | let flag_playchoice_10 = flags_7 & 0b_0000_0010 != 0; 114 | let flag_rom_format = (flags_7 & 0b_0000_1100) >> 2; 115 | let flag_mapper_hi = (flags_7 & 0b_1111_0000) >> 4; 116 | 117 | let _flag_tv_system_ignored = flags_9 & 0b_0000_0001; 118 | let _flag_reserved = flags_9 & 0b_1111_1110; 119 | 120 | let flag_tv_system = flags_10 & 0b_0000_0011; 121 | let _flag_unused = flags_10 & 0b_1100_1100; 122 | let flag_prg_ram_bit = (flags_10 & 0b_0001_0000) != 0; 123 | let flag_bus_conflicts_bit = (flags_10 & 0b_0010_0000) != 0; 124 | 125 | let prg_rom_size_bytes = prg_rom_size_field as usize * 16_384; 126 | 127 | let chr_rom_size_bytes = chr_rom_size_field as usize * 8_192; 128 | let chr_ram_size_bytes = match chr_rom_size_field { 129 | 0 => { 130 | // We assume a ROM only has CHR RAM if it has no CHR ROM. 131 | // Because the iNES 1.0 format doesn't include the size 132 | // of the CHR RAM, we always assume it has 8KiB 133 | 8_192 134 | } 135 | _ => 0, 136 | }; 137 | 138 | let prg_ram_size_bytes = match (prg_ram_size_field, flag_prg_ram_bit) { 139 | (_, false) => 0, 140 | (0, true) => { 141 | // When a ROM has the PRG RAM bit set but has a PRG RAM 142 | // size of 0, then a fallback size of 8KB is used 143 | 8_192 144 | } 145 | (prg_ram_size_field, true) => prg_ram_size_field as usize * 8_192, 146 | }; 147 | 148 | let mirror_mode = match (flag_mirror_bit, flag_four_screen_vram_bit) { 149 | (false, false) => MirrorMode::Horizontal, 150 | (true, false) => MirrorMode::Vertical, 151 | (_, true) => MirrorMode::FourScreenVram, 152 | }; 153 | let has_persistence = flag_persistent_bit; 154 | let has_trainer = flag_trainer_bit; 155 | let mapper = flag_mapper_lo | (flag_mapper_hi << 4); 156 | 157 | let is_vs_unisystem = flag_vs_unisystem; 158 | let is_playchoice_10 = flag_playchoice_10; 159 | match flag_rom_format { 160 | 2 => { 161 | unimplemented!("ROM uses NES 2.0 ROM format!"); 162 | } 163 | _ => {} 164 | }; 165 | 166 | let tv_system = match flag_tv_system { 167 | 0 => TvSystem::Ntsc, 168 | 2 => TvSystem::Pal, 169 | 1 | 3 => TvSystem::Dual, 170 | _ => { 171 | unreachable!(); 172 | } 173 | }; 174 | 175 | let has_bus_conflicts = flag_bus_conflicts_bit; 176 | 177 | Ok(RomHeader { 178 | prg_rom_size_bytes, 179 | chr_rom_size_bytes, 180 | prg_ram_size_bytes, 181 | chr_ram_size_bytes, 182 | mirror_mode, 183 | has_persistence, 184 | has_trainer, 185 | mapper, 186 | is_vs_unisystem, 187 | is_playchoice_10, 188 | tv_system, 189 | has_bus_conflicts, 190 | }) 191 | } 192 | } 193 | 194 | #[derive(Debug, Clone, Copy)] 195 | pub enum MirrorMode { 196 | Horizontal, 197 | Vertical, 198 | FourScreenVram, 199 | } 200 | 201 | #[derive(Debug, Clone, Copy)] 202 | pub enum TvSystem { 203 | Ntsc, 204 | Pal, 205 | Dual, 206 | } 207 | -------------------------------------------------------------------------------- /src/video.rs: -------------------------------------------------------------------------------- 1 | use sdl2::pixels::PixelFormatEnum::RGB24; 2 | use sdl2::render::{Canvas, RenderTarget, Texture, TextureCreator}; 3 | use std::sync::RwLock; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct Point { 7 | pub x: u16, 8 | pub y: u16, 9 | } 10 | 11 | #[derive(Debug, Clone, Copy)] 12 | pub struct Color { 13 | pub r: u8, 14 | pub g: u8, 15 | pub b: u8, 16 | } 17 | 18 | pub trait Video { 19 | fn draw_point(&self, point: Point, color: Color); 20 | fn present(&self); 21 | fn clear(&self); 22 | } 23 | 24 | pub struct NullVideo; 25 | 26 | impl Video for NullVideo { 27 | fn draw_point(&self, _point: Point, _color: Color) {} 28 | fn present(&self) {} 29 | fn clear(&self) {} 30 | } 31 | 32 | pub struct TextureBufferedVideo<'a> { 33 | buffer: RwLock>, 34 | width: u32, 35 | height: u32, 36 | frame: RwLock>, 37 | } 38 | 39 | impl<'a> TextureBufferedVideo<'a> { 40 | pub fn new( 41 | texture_creator: &'a TextureCreator, 42 | width: u32, 43 | height: u32, 44 | ) -> Result { 45 | let black = Color { r: 0, g: 0, b: 0 }; 46 | let buffer = vec![black; width as usize * height as usize]; 47 | let buffer = RwLock::new(buffer); 48 | let frame = texture_creator.create_texture_streaming(RGB24, width, height)?; 49 | let frame = RwLock::new(frame); 50 | 51 | Ok(TextureBufferedVideo { 52 | buffer, 53 | frame, 54 | width, 55 | height, 56 | }) 57 | } 58 | 59 | pub fn copy_to(&self, canvas: &mut Canvas) -> Result<(), String> { 60 | let current_frame = self.frame.read().unwrap(); 61 | canvas.copy(¤t_frame, None, None)?; 62 | 63 | Ok(()) 64 | } 65 | } 66 | 67 | impl<'a, 'b> Video for TextureBufferedVideo<'b> { 68 | fn draw_point(&self, point: Point, color: Color) { 69 | let mut buffer = self.buffer.write().unwrap(); 70 | let offset = point.y as usize * self.width as usize + point.x as usize; 71 | buffer[offset] = color; 72 | } 73 | 74 | fn present(&self) { 75 | let buffer = self.buffer.read().unwrap(); 76 | let mut frame = self.frame.write().unwrap(); 77 | frame 78 | .with_lock(None, |frame_buffer, pitch| { 79 | for y in 0..self.height { 80 | for x in 0..self.width { 81 | let offset = y as usize * self.width as usize + x as usize; 82 | let color = buffer[offset]; 83 | 84 | let frame_offset = y as usize * pitch + (x as usize * 3); 85 | frame_buffer[frame_offset] = color.r; 86 | frame_buffer[frame_offset + 1] = color.g; 87 | frame_buffer[frame_offset + 2] = color.b; 88 | } 89 | } 90 | }) 91 | .unwrap(); 92 | } 93 | 94 | fn clear(&self) {} 95 | } 96 | 97 | impl<'a, V> Video for &'a V 98 | where 99 | V: Video, 100 | { 101 | fn draw_point(&self, point: Point, color: Color) { 102 | (*self).draw_point(point, color); 103 | } 104 | 105 | fn present(&self) { 106 | (*self).present(); 107 | } 108 | 109 | fn clear(&self) { 110 | (*self).clear(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/fixtures/egg.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylewlacy/lochnes/336d6532c6b6d650ba56360415b064b8de845846/tests/fixtures/egg.nes -------------------------------------------------------------------------------- /tests/verification_roms.rs: -------------------------------------------------------------------------------- 1 | #![feature(generator_trait, exhaustive_patterns)] 2 | 3 | use std::ops::{Generator, GeneratorState}; 4 | use std::pin::Pin; 5 | 6 | use lochnes::nes::ppu::PpuStep; 7 | use lochnes::nes::NesStep; 8 | use lochnes::{input, nes, rom, video}; 9 | 10 | fn run_blargg_instr_test_with_expected_result( 11 | test_name: &str, 12 | rom_bytes: &[u8], 13 | expected_status: u8, 14 | expected_output: &str, 15 | ) { 16 | let rom = rom::Rom::from_bytes(rom_bytes.into_iter().cloned()) 17 | .expect(&format!("Failed to parse test ROM {:?}", test_name)); 18 | 19 | let video = video::NullVideo; 20 | let input = input::NullInput; 21 | let io = nes::NesIoWith { video, input }; 22 | let nes = nes::Nes::new(&io, rom.clone()); 23 | let mut run_nes = nes.run(); 24 | 25 | // Run for a max of 240 frames, just in case the test ROM never completes 26 | for frame in 0..240 { 27 | loop { 28 | match Pin::new(&mut run_nes).resume(()) { 29 | GeneratorState::Yielded(NesStep::Ppu(PpuStep::Vblank)) => { 30 | break; 31 | } 32 | GeneratorState::Yielded(_) => {} 33 | } 34 | } 35 | 36 | const STATUS_TEST_IS_RUNNING: u8 = 0x80; 37 | const STATUS_TEST_NEEDS_RESET: u8 = 0x81; 38 | 39 | let status = nes.read_u8(0x6000); 40 | match (status, frame) { 41 | (_, 0) => {} // Ignore status on first frame 42 | (STATUS_TEST_IS_RUNNING, _) => {} 43 | (STATUS_TEST_NEEDS_RESET, _) => { 44 | unimplemented!("Verification ROM requested a reset!"); 45 | } 46 | _ => { 47 | break; 48 | } 49 | } 50 | } 51 | 52 | let status = nes.read_u8(0x6000); 53 | 54 | let mut test_output = vec![]; 55 | for i in 0x6004_u16.. { 56 | let byte = nes.read_u8(i); 57 | match byte { 58 | 0 => { 59 | break; 60 | } 61 | byte => { 62 | test_output.push(byte); 63 | } 64 | } 65 | } 66 | 67 | let test_output = String::from_utf8_lossy(&test_output); 68 | println!("{}", test_output); 69 | assert_eq!(status, expected_status); 70 | 71 | assert_eq!(test_output, expected_output); 72 | } 73 | 74 | fn run_blargg_instr_test(test_name: &str, rom_bytes: &[u8]) { 75 | let expected_output = format!("\n{}\n\nPassed\n", test_name); 76 | run_blargg_instr_test_with_expected_result(test_name, rom_bytes, 0, &expected_output); 77 | } 78 | 79 | #[test] 80 | fn rom_blargg_instr_test_implied() { 81 | run_blargg_instr_test( 82 | "01-implied", 83 | include_bytes!("./fixtures/nes-test-roms/nes_instr_test/rom_singles/01-implied.nes"), 84 | ); 85 | } 86 | 87 | #[test] 88 | fn rom_blargg_instr_test_immediate() { 89 | run_blargg_instr_test( 90 | "02-immediate", 91 | include_bytes!("./fixtures/nes-test-roms/nes_instr_test/rom_singles/02-immediate.nes"), 92 | ); 93 | } 94 | 95 | #[test] 96 | fn rom_blargg_instr_test_zero_page() { 97 | run_blargg_instr_test( 98 | "03-zero_page", 99 | include_bytes!("./fixtures/nes-test-roms/nes_instr_test/rom_singles/03-zero_page.nes"), 100 | ); 101 | } 102 | 103 | #[test] 104 | fn rom_blargg_instr_test_zero_page_indexed() { 105 | run_blargg_instr_test( 106 | "04-zp_xy", 107 | include_bytes!("./fixtures/nes-test-roms/nes_instr_test/rom_singles/04-zp_xy.nes"), 108 | ); 109 | } 110 | 111 | #[test] 112 | fn rom_blargg_instr_test_abs() { 113 | run_blargg_instr_test( 114 | "05-absolute", 115 | include_bytes!("./fixtures/nes-test-roms/nes_instr_test/rom_singles/05-absolute.nes"), 116 | ); 117 | } 118 | 119 | // NOTE: The absolute-indexed test ROM fails with the below message in other 120 | // emulators. For now, we assume that the test is wrong, and verify that we 121 | // also see the below message. 122 | const ABS_XY_EXPECTED_FAILURE: &str = r##"9C SYA abs,X 123 | 9E SXA abs,Y 124 | 125 | 06-abs_xy 126 | 127 | Failed 128 | "##; 129 | 130 | #[test] 131 | fn rom_blargg_instr_test_abs_indexed() { 132 | run_blargg_instr_test_with_expected_result( 133 | "06-abs_xy", 134 | include_bytes!("./fixtures/nes-test-roms/nes_instr_test/rom_singles/06-abs_xy.nes"), 135 | 1, 136 | &ABS_XY_EXPECTED_FAILURE, 137 | ); 138 | } 139 | 140 | #[test] 141 | fn rom_blargg_instr_test_indexed_indirect() { 142 | run_blargg_instr_test( 143 | "07-ind_x", 144 | include_bytes!("./fixtures/nes-test-roms/nes_instr_test/rom_singles/07-ind_x.nes"), 145 | ); 146 | } 147 | 148 | #[test] 149 | fn rom_blargg_instr_test_indirect_indexed() { 150 | run_blargg_instr_test( 151 | "08-ind_y", 152 | include_bytes!("./fixtures/nes-test-roms/nes_instr_test/rom_singles/08-ind_y.nes"), 153 | ); 154 | } 155 | 156 | #[test] 157 | fn rom_blargg_instr_test_branching() { 158 | run_blargg_instr_test( 159 | "09-branches", 160 | include_bytes!("./fixtures/nes-test-roms/nes_instr_test/rom_singles/09-branches.nes"), 161 | ); 162 | } 163 | 164 | #[test] 165 | fn rom_blargg_instr_test_stack() { 166 | run_blargg_instr_test( 167 | "10-stack", 168 | include_bytes!("./fixtures/nes-test-roms/nes_instr_test/rom_singles/10-stack.nes"), 169 | ); 170 | } 171 | 172 | #[test] 173 | fn rom_blargg_instr_test_special() { 174 | run_blargg_instr_test( 175 | "11-special", 176 | include_bytes!("./fixtures/nes-test-roms/nes_instr_test/rom_singles/11-special.nes"), 177 | ); 178 | } 179 | --------------------------------------------------------------------------------