Repository: lukexor/tetanes Branch: main Commit: 900e840134f4 Files: 534 Total size: 16.1 MB Directory structure: gitextract_bip8yc8i/ ├── .cargo/ │ └── config.toml ├── .config/ │ └── nextest.toml ├── .git-blame-ignore-revs ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── defect-report.md │ │ └── feature_request.md │ ├── dependabot.yml │ └── workflows/ │ ├── cd.yml │ ├── ci.yml │ ├── outdated.yml │ ├── release-pr.yml │ ├── security.yml │ └── triage.yml ├── .gitignore ├── .gitmodules ├── .prettierignore ├── .rgignore ├── Cargo.toml ├── Cross.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── Makefile.toml ├── README.md ├── ROADMAP.md ├── assets/ │ ├── linux/ │ │ └── tetanes.desktop │ └── macos/ │ ├── .DS_Store │ ├── Icon.icns │ └── Info.plist ├── cliff.toml ├── deny.toml ├── docs/ │ ├── apu/ │ │ ├── apu_ref.txt │ │ ├── audio_psuedo_code.txt │ │ ├── blargg_tests_readme.txt │ │ ├── mixer_readme.txt │ │ ├── test_readme.txt │ │ └── volume_readme.txt │ ├── cartridge_board_list.txt │ ├── cpu/ │ │ ├── branch_timing_readme.txt │ │ ├── dummy_writes_readme.txt │ │ ├── exec_space_readme.txt │ │ ├── instr_misc_readme.txt │ │ ├── instr_test_readme.txt │ │ ├── instr_timing_readme.txt │ │ ├── interrupts_readme.txt │ │ ├── opcode_list.txt │ │ └── reset_readme.txt │ ├── genie_codes │ ├── mapper/ │ │ ├── 000.txt │ │ ├── 001.txt │ │ ├── 002.txt │ │ ├── 003.txt │ │ ├── 004.txt │ │ ├── 005.txt │ │ ├── 007.txt │ │ ├── 009.txt │ │ ├── 010.txt │ │ ├── 011.txt │ │ ├── 013.txt │ │ ├── 015.txt │ │ ├── 016.txt │ │ ├── 018.txt │ │ ├── 019.txt │ │ ├── 021.txt │ │ ├── 022.txt │ │ ├── 023.txt │ │ ├── 024.txt │ │ ├── 025.txt │ │ ├── 026.txt │ │ ├── 032.txt │ │ ├── 033.txt │ │ ├── 034.txt │ │ ├── 044.txt │ │ ├── 045.txt │ │ ├── 046.txt │ │ ├── 047.txt │ │ ├── 048.txt │ │ ├── 049.txt │ │ ├── 050.txt │ │ ├── 052.txt │ │ ├── 057.txt │ │ ├── 058.txt │ │ ├── 060.txt │ │ ├── 061.txt │ │ ├── 062.txt │ │ ├── 064.txt │ │ ├── 065.txt │ │ ├── 066.txt │ │ ├── 067.txt │ │ ├── 068.txt │ │ ├── 069.txt │ │ ├── 070.txt │ │ ├── 071.txt │ │ ├── 072.txt │ │ ├── 073.txt │ │ ├── 074.txt │ │ ├── 075.txt │ │ ├── 076.txt │ │ ├── 077.txt │ │ ├── 078.txt │ │ ├── 079.txt │ │ ├── 080.txt │ │ ├── 082.txt │ │ ├── 085.txt │ │ ├── 086.txt │ │ ├── 087.txt │ │ ├── 088.txt │ │ ├── 089.txt │ │ ├── 090.txt │ │ ├── 091.txt │ │ ├── 092.txt │ │ ├── 093.txt │ │ ├── 094.txt │ │ ├── 095.txt │ │ ├── 096.txt │ │ ├── 097.txt │ │ ├── 105.txt │ │ ├── 107.txt │ │ ├── 112.txt │ │ ├── 113.txt │ │ ├── 115.txt │ │ ├── 118.txt │ │ ├── 119.txt │ │ ├── 140.txt │ │ ├── 152.txt │ │ ├── 154.txt │ │ ├── 159.txt │ │ ├── 164.txt │ │ ├── 165.txt │ │ ├── 180.txt │ │ ├── 182.txt │ │ ├── 184.txt │ │ ├── 185.txt │ │ ├── 189.txt │ │ ├── 191.txt │ │ ├── 192.txt │ │ ├── 193.txt │ │ ├── 194.txt │ │ ├── 200.txt │ │ ├── 201.txt │ │ ├── 203.txt │ │ ├── 205.txt │ │ ├── 207.txt │ │ ├── 209.txt │ │ ├── 210.txt │ │ ├── 225.txt │ │ ├── 226.txt │ │ ├── 227.txt │ │ ├── 228.txt │ │ ├── 230.txt │ │ ├── 231.txt │ │ ├── 232.txt │ │ ├── 233.txt │ │ ├── 234.txt │ │ ├── 240.txt │ │ ├── 242.txt │ │ ├── 243.txt │ │ ├── 245.txt │ │ ├── 246.txt │ │ ├── __ READ THIS FIRST __.txt │ │ ├── changes.txt │ │ ├── mmc3_irq_tests_readme.txt │ │ └── mmc3_test_readme.txt │ ├── memory_mapping.txt │ ├── nes_arch.txt │ ├── nes_graphics.txt │ ├── nes_tech.txt │ └── ppu/ │ ├── blargg_tests_readme.txt │ ├── nmi_sync_ntsc_readme.txt │ ├── oam_read_readme.txt │ ├── oam_stress_readme.txt │ ├── open_bus_readme.txt │ ├── ppu_2c02_ref.txt │ ├── ppu_scrolling.txt │ ├── read_buffer_test_readme.txt │ ├── sprite_hit_readme.txt │ ├── sprite_overflow_readme.txt │ ├── tv_readme.txt │ ├── vbl_nmi_readme.txt │ └── vbl_nmi_timing_readme.txt ├── release-plz.toml ├── rust-toolchain.toml ├── static/ │ └── tetanes.xcf ├── tetanes/ │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── assets/ │ │ ├── main.css │ │ ├── pixeloid-license.txt │ │ └── roms/ │ │ ├── alter_ego.nes │ │ ├── alter_ego.txt │ │ ├── ao_demo.nes │ │ ├── ao_demo.txt │ │ ├── assimilate.nes │ │ ├── assimilate.txt │ │ ├── blade_buster.nes │ │ ├── blade_buster.txt │ │ ├── cheril_the_goddess.nes │ │ ├── cheril_the_goddess.txt │ │ ├── data_man_demo.nes │ │ ├── dushlan.nes │ │ ├── dushlan.txt │ │ ├── from_below.nes │ │ ├── from_below.txt │ │ ├── lan_master.nes │ │ ├── lan_master.txt │ │ ├── lawn_mower.nes │ │ ├── lawn_mower.txt │ │ ├── mad_wizard.nes │ │ ├── mad_wizard.txt │ │ ├── micro_knight.nes │ │ ├── micro_knight.txt │ │ ├── nebs_n_debs.txt │ │ ├── nebs_n_debs_demo.nes │ │ ├── owlia.nes │ │ ├── owlia.txt │ │ ├── streemerz.nes │ │ ├── streemerz.txt │ │ ├── super_painter.nes │ │ ├── super_painter.txt │ │ ├── tiger_jenny.nes │ │ ├── tiger_jenny.txt │ │ ├── yun.nes │ │ └── yun.txt │ ├── build.rs │ ├── index.html │ ├── initializer.js │ ├── shaders/ │ │ ├── crt-easymode.wgsl │ │ └── gui.wgsl │ ├── src/ │ │ ├── bin/ │ │ │ └── build_artifacts.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── logging.rs │ │ ├── main.rs │ │ ├── nes/ │ │ │ ├── action.rs │ │ │ ├── audio.rs │ │ │ ├── config.rs │ │ │ ├── emulation/ │ │ │ │ ├── replay.rs │ │ │ │ └── rewind.rs │ │ │ ├── emulation.rs │ │ │ ├── event.rs │ │ │ ├── input.rs │ │ │ ├── renderer/ │ │ │ │ ├── clipboard.rs │ │ │ │ ├── event.rs │ │ │ │ ├── gui/ │ │ │ │ │ ├── keybinds.rs │ │ │ │ │ ├── lib.rs │ │ │ │ │ ├── ppu_viewer.rs │ │ │ │ │ └── preferences.rs │ │ │ │ ├── gui.rs │ │ │ │ ├── painter.rs │ │ │ │ ├── shader.rs │ │ │ │ └── texture.rs │ │ │ ├── renderer.rs │ │ │ ├── rom.rs │ │ │ └── version.rs │ │ ├── nes.rs │ │ ├── opts.rs │ │ ├── platform.rs │ │ ├── sys/ │ │ │ ├── info/ │ │ │ │ ├── os.rs │ │ │ │ └── wasm.rs │ │ │ ├── info.rs │ │ │ ├── logging/ │ │ │ │ ├── os.rs │ │ │ │ └── wasm.rs │ │ │ ├── logging.rs │ │ │ ├── platform/ │ │ │ │ ├── os.rs │ │ │ │ └── wasm.rs │ │ │ ├── platform.rs │ │ │ ├── thread/ │ │ │ │ ├── os.rs │ │ │ │ └── wasm.rs │ │ │ └── thread.rs │ │ ├── sys.rs │ │ └── thread.rs │ └── wix/ │ └── main.wxs ├── tetanes-core/ │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── README.md │ ├── benches/ │ │ └── clock_frame.rs │ ├── game_database.txt │ ├── ntscpalette.pal │ ├── src/ │ │ ├── action.rs │ │ ├── apu/ │ │ │ ├── dmc.rs │ │ │ ├── envelope.rs │ │ │ ├── filter.rs │ │ │ ├── frame_counter.rs │ │ │ ├── length_counter.rs │ │ │ ├── noise.rs │ │ │ ├── pulse.rs │ │ │ ├── timer.rs │ │ │ └── triangle.rs │ │ ├── apu.rs │ │ ├── bus.rs │ │ ├── cart.rs │ │ ├── common.rs │ │ ├── control_deck.rs │ │ ├── cpu/ │ │ │ └── instr.rs │ │ ├── cpu.rs │ │ ├── debug.rs │ │ ├── error.rs │ │ ├── fs.rs │ │ ├── genie.rs │ │ ├── input.rs │ │ ├── lib.rs │ │ ├── mapper/ │ │ │ ├── bandai_fcg.rs │ │ │ ├── m000_nrom.rs │ │ │ ├── m001_sxrom.rs │ │ │ ├── m002_uxrom.rs │ │ │ ├── m003_cnrom.rs │ │ │ ├── m004_txrom.rs │ │ │ ├── m005_exrom.rs │ │ │ ├── m007_axrom.rs │ │ │ ├── m009_pxrom.rs │ │ │ ├── m010_fxrom.rs │ │ │ ├── m011_color_dreams.rs │ │ │ ├── m018_jalecoss88006.rs │ │ │ ├── m019_namco163.rs │ │ │ ├── m024_m026_vrc6.rs │ │ │ ├── m034_bnrom.rs │ │ │ ├── m034_nina001.rs │ │ │ ├── m066_gxrom.rs │ │ │ ├── m069_sunsoft_fme7.rs │ │ │ ├── m071_bf909x.rs │ │ │ ├── m079_nina003_006.rs │ │ │ └── vrc_irq.rs │ │ ├── mapper.rs │ │ ├── mem.rs │ │ ├── ppu/ │ │ │ ├── ctrl.rs │ │ │ ├── frame.rs │ │ │ ├── mask.rs │ │ │ ├── scroll.rs │ │ │ ├── sprite.rs │ │ │ └── status.rs │ │ ├── ppu.rs │ │ ├── sys/ │ │ │ ├── fs/ │ │ │ │ ├── os.rs │ │ │ │ └── wasm.rs │ │ │ ├── fs.rs │ │ │ └── time.rs │ │ ├── sys.rs │ │ ├── time.rs │ │ └── video.rs │ └── test_roms/ │ ├── apu/ │ │ ├── apu_env.nes │ │ ├── blargg_readme.txt │ │ ├── clock_jitter.nes │ │ ├── dmc.nes │ │ ├── dmc_basics.nes │ │ ├── dmc_buffer_retained.nes │ │ ├── dmc_dma_2007_read.nes │ │ ├── dmc_dma_2007_write.nes │ │ ├── dmc_dma_4016_read.nes │ │ ├── dmc_dma_double_2007_read.nes │ │ ├── dmc_dma_read_write_2007.nes │ │ ├── dmc_latency.nes │ │ ├── dmc_pitch.nes │ │ ├── dmc_rates.nes │ │ ├── dmc_status.nes │ │ ├── dmc_status_irq.nes │ │ ├── dpcmletterbox.nes │ │ ├── dpcmletterbox.txt │ │ ├── irq_flag.nes │ │ ├── irq_flag_timing.nes │ │ ├── irq_timing.nes │ │ ├── len_ctr.nes │ │ ├── len_halt_timing.nes │ │ ├── len_reload_timing.nes │ │ ├── len_table.nes │ │ ├── len_timing.nes │ │ ├── len_timing_mode0.nes │ │ ├── len_timing_mode1.nes │ │ ├── lin_ctr.nes │ │ ├── mixer.txt │ │ ├── noise.nes │ │ ├── noise_pitch.nes │ │ ├── pal_clock_jitter.nes │ │ ├── pal_irq_flag.nes │ │ ├── pal_irq_flag_timing.nes │ │ ├── pal_irq_timing.nes │ │ ├── pal_len_ctr.nes │ │ ├── pal_len_halt_timing.nes │ │ ├── pal_len_reload_timing.nes │ │ ├── pal_len_table.nes │ │ ├── pal_len_timing_mode0.nes │ │ ├── pal_len_timing_mode1.nes │ │ ├── pal_readme.txt │ │ ├── phase_reset.nes │ │ ├── readme.txt │ │ ├── reset.txt │ │ ├── reset_4015_cleared.nes │ │ ├── reset_4017_timing.nes │ │ ├── reset_4017_written.nes │ │ ├── reset_irq_flag_cleared.nes │ │ ├── reset_len_ctrs_enabled.nes │ │ ├── reset_timing.nes │ │ ├── reset_works_immediately.nes │ │ ├── square.nes │ │ ├── square_pitch.nes │ │ ├── sweep_cutoff.nes │ │ ├── sweep_sub.nes │ │ ├── test_1.nes │ │ ├── test_10.nes │ │ ├── test_2.nes │ │ ├── test_3.nes │ │ ├── test_4.nes │ │ ├── test_5.nes │ │ ├── test_6.nes │ │ ├── test_7.nes │ │ ├── test_8.nes │ │ ├── test_9.nes │ │ ├── tests.json │ │ ├── triangle.nes │ │ ├── triangle_pitch.nes │ │ ├── volumes.nes │ │ └── volumes.txt │ ├── cpu/ │ │ ├── branch.txt │ │ ├── branch_backward.nes │ │ ├── branch_basics.nes │ │ ├── branch_forward.nes │ │ ├── dummy_reads.nes │ │ ├── dummy_writes.txt │ │ ├── dummy_writes_oam.nes │ │ ├── dummy_writes_ppumem.nes │ │ ├── exec_space.txt │ │ ├── exec_space_apu.nes │ │ ├── exec_space_ppuio.nes │ │ ├── flag_concurrency.nes │ │ ├── instr.txt │ │ ├── instr_abs.nes │ │ ├── instr_abs_xy.nes │ │ ├── instr_basics.nes │ │ ├── instr_branches.nes │ │ ├── instr_brk.nes │ │ ├── instr_imm.nes │ │ ├── instr_imp.nes │ │ ├── instr_ind_x.nes │ │ ├── instr_ind_y.nes │ │ ├── instr_jmp_jsr.nes │ │ ├── instr_misc.nes │ │ ├── instr_misc.txt │ │ ├── instr_rti.nes │ │ ├── instr_rts.nes │ │ ├── instr_special.nes │ │ ├── instr_stack.nes │ │ ├── instr_timing.nes │ │ ├── instr_timing.txt │ │ ├── instr_zp.nes │ │ ├── instr_zp_xy.nes │ │ ├── int_branch_delays_irq.nes │ │ ├── int_cli_latency.nes │ │ ├── int_irq_and_dma.nes │ │ ├── int_nmi_and_brk.nes │ │ ├── int_nmi_and_irq.nes │ │ ├── interrupts.txt │ │ ├── nestest.nes │ │ ├── nestest.txt │ │ ├── overclock.nes │ │ ├── ram_after_reset.nes │ │ ├── regs_after_reset.nes │ │ ├── reset.txt │ │ ├── sprdma_and_dmc_dma.nes │ │ ├── sprdma_and_dmc_dma_512.nes │ │ ├── tests.json │ │ ├── timing.txt │ │ └── timing_test.nes │ ├── input/ │ │ ├── tests.json │ │ ├── zapper_flip.nes │ │ ├── zapper_light.nes │ │ ├── zapper_stream.nes │ │ └── zapper_trigger.nes │ ├── mapper/ │ │ ├── m004_txrom/ │ │ │ ├── a12_clocking.nes │ │ │ ├── big_chr_ram.nes │ │ │ ├── clocking.nes │ │ │ ├── details.nes │ │ │ ├── irq.txt │ │ │ ├── rev_a.nes │ │ │ ├── rev_b.nes │ │ │ ├── scanline_timing.nes │ │ │ └── tests.json │ │ └── m005_exrom/ │ │ ├── basics.nes │ │ ├── exram.nes │ │ └── tests.json │ ├── ppu/ │ │ ├── _240pee.nes │ │ ├── blargg_readme.txt │ │ ├── color.nes │ │ ├── ntsc_torture.nes │ │ ├── oam_read.nes │ │ ├── oam_read.txt │ │ ├── oam_stress.nes │ │ ├── oam_stress.txt │ │ ├── open_bus.nes │ │ ├── open_bus.txt │ │ ├── palette.nes │ │ ├── palette_ram.nes │ │ ├── read_buffer.nes │ │ ├── read_buffer.txt │ │ ├── scanline.nes │ │ ├── spr_hit.txt │ │ ├── spr_hit_alignment.nes │ │ ├── spr_hit_basics.nes │ │ ├── spr_hit_corners.nes │ │ ├── spr_hit_double_height.nes │ │ ├── spr_hit_edge_timing.nes │ │ ├── spr_hit_flip.nes │ │ ├── spr_hit_left_clip.nes │ │ ├── spr_hit_right_edge.nes │ │ ├── spr_hit_screen_bottom.nes │ │ ├── spr_hit_timing_basics.nes │ │ ├── spr_hit_timing_order.nes │ │ ├── spr_overflow.txt │ │ ├── spr_overflow_basics.nes │ │ ├── spr_overflow_details.nes │ │ ├── spr_overflow_emulator.nes │ │ ├── spr_overflow_obscure.nes │ │ ├── spr_overflow_timing.nes │ │ ├── sprite_ram.nes │ │ ├── tests.json │ │ ├── tv.nes │ │ ├── tv.txt │ │ ├── vbl_nmi.txt │ │ ├── vbl_nmi_basics.nes │ │ ├── vbl_nmi_clear_timing.nes │ │ ├── vbl_nmi_control.nes │ │ ├── vbl_nmi_disable.nes │ │ ├── vbl_nmi_even_odd_frames.nes │ │ ├── vbl_nmi_even_odd_timing.nes │ │ ├── vbl_nmi_frame_basics.nes │ │ ├── vbl_nmi_off_timing.nes │ │ ├── vbl_nmi_on_timing.nes │ │ ├── vbl_nmi_set_time.nes │ │ ├── vbl_nmi_suppression.nes │ │ ├── vbl_nmi_timing.nes │ │ ├── vbl_nmi_timing.txt │ │ ├── vbl_timing.nes │ │ └── vram_access.nes │ └── spritecans.nes ├── tetanes-utils/ │ ├── Cargo.toml │ └── src/ │ └── bin/ │ ├── generate_db.rs │ └── list_boards.rs └── vendored/ ├── linuxdeploy-aarch64.AppImage └── linuxdeploy-x86_64.AppImage ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cargo/config.toml ================================================ [build] rustflags = ["-Z", "threads=8"] [target.'cfg(target_arch = "wasm32")'] rustflags = [ "-Zthreads=8", "-Zwasm-c-abi=spec", "--cfg=web_sys_unstable_apis", "--cfg=getrandom_backend=\"wasm_js\"", ] ================================================ FILE: .config/nextest.toml ================================================ [profile.ci] fail-fast = false slow-timeout = { period = "30s", terminate-after = 4 } test-threads = 1 ================================================ FILE: .git-blame-ignore-revs ================================================ ================================================ FILE: .gitattributes ================================================ *.rs linguist-detectable=true *.js linguist-detectable=false *.html linguist-detectable=false ================================================ FILE: .github/ISSUE_TEMPLATE/defect-report.md ================================================ --- name: Defect Report about: Report issues to improve TetaNES title: '' labels: needs-triage bug assignees: lukexor --- ## Describe the issue A clear and concise description of what the defect is. Be sure to try the suggestions in the [Troubleshooting](https://github.com/lukexor/tetanes#troubleshooting) section of the README first. ## To Reproduce Steps to reproduce the behavior: 1. Load '...' 1. Press '....' 1. See error ## Expected behavior A clear and concise description of what you expected to happen. ## Screenshots, Logs or Artifacts If applicable, attach screenshots, logs, configuration files, or save states to help explain or reproduce the issue. See the [Directories](https://github.com/lukexor/tetanes#directories) section for file locations. ## Environment - ROM title - Please don't attach any download links or ROMs due to copyright laws. - Operating System and Version - e.g. Windows 7, macOS Mojave 10.14.6, Ubuntu 22.04 - Browser Vendor and Version (if using TetaNES Web) - e.g. Chrome 77.0.3865 - TetaNES Version - Can be found in the `About` menu, e.g. 0.12.1 ## Additional context Add any other context about the issue here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature Request about: Suggest an idea title: '' labels: needs-triage enhancement assignees: lukexor --- ## Is your feature request related to a problem? A clear and concise description of what the problem is. e.g. I'm always frustrated when [...]. ## Describe the solution you'd like A clear and concise description of what you want to happen. ## Describe alternatives you've considered A clear and concise description of any alternative solutions or features you've considered. ## Additional context Add any other context or screenshots about the feature request here. ================================================ FILE: .github/dependabot.yml ================================================ --- version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" assignees: - "lukexor" open-pull-requests-limit: 1 groups: ci-dependencies: patterns: - "*" ================================================ FILE: .github/workflows/cd.yml ================================================ --- name: CD # yamllint disable-line rule:truthy on: release: types: [published] workflow_dispatch: inputs: tag: description: "Release tag" required: true type: string os: description: "Target platform" required: true type: choice options: - all - linux - macos - windows - web permissions: contents: write env: # Unnecessary for CI and just pollutes cache CARGO_INCREMENTAL: 0 # Remove debug symbols, substantially reduces cache size CARGO_PROFILE_DEV_DEBUG: 0 CARGO_PROFILE_TEST_DEBUG: 0 CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 jobs: build-linux: name: Build Linux Artifacts (${{ matrix.target }}) if: > ((startsWith(github.event.release.name, 'tetanes') && !startsWith(github.event.release.name, 'tetanes-core')) || (inputs.tag && (inputs.os == 'all' || inputs.os == 'linux'))) runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - target: x86_64-unknown-linux-gnu # TODO: aarch64 linux having trouble with docker in CI # - target: aarch64-unknown-linux-gnu defaults: run: shell: bash steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@master with: toolchain: nightly - uses: Swatinem/rust-cache@v2 - uses: baptiste0928/cargo-install@v3 with: crate: cross git: https://github.com/cross-rs/cross commit: 19bc73f - uses: taiki-e/install-action@v2 with: tool: cargo-make,cargo-deb,cargo-pgo - run: | sudo apt update sudo apt install -y libudev-dev libasound2-dev libssl-dev libfuse2 - if: startsWith(matrix.target, 'x86_64') run: | time cargo make build-artifacts -- --target ${{ matrix.target }} # aarch64 requires cross building - if: startsWith(matrix.target, 'aarch64') run: | export CROSS_CONTAINER_IN_CONTAINER=true time cargo make build-artifacts -- --target ${{ matrix.target }} --cross - if: success() uses: actions/upload-artifact@v7 with: name: ${{ matrix.target }}-artifacts path: tetanes/dist/ build-macos: name: Build macOS Artifacts (${{ matrix.target }}) if: > ((startsWith(github.event.release.name, 'tetanes') && !startsWith(github.event.release.name, 'tetanes-core')) || (inputs.tag && (inputs.os == 'all' || inputs.os == 'macos'))) runs-on: macos-latest strategy: fail-fast: false matrix: include: - target: x86_64-apple-darwin - target: aarch64-apple-darwin defaults: run: shell: bash steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@master with: toolchain: nightly - uses: Swatinem/rust-cache@v2 - uses: taiki-e/setup-cross-toolchain-action@v1 with: target: ${{ matrix.target }} - uses: taiki-e/install-action@v2 with: tool: cargo-make,cargo-pgo - run: | time cargo make build-artifacts -- --target ${{ matrix.target }} - if: success() uses: actions/upload-artifact@v7 with: name: ${{ matrix.target }}-artifacts path: tetanes/dist/ build-windows: name: Build Windows Artifacts (${{ matrix.target }}) if: > ((startsWith(github.event.release.name, 'tetanes') && !startsWith(github.event.release.name, 'tetanes-core')) || (inputs.tag && (inputs.os == 'all' || inputs.os == 'windows'))) runs-on: windows-latest strategy: fail-fast: false matrix: include: - target: x86_64-pc-windows-msvc # TODO: windows aarch64 # - target: aarch64-pc-windows-msvc defaults: run: shell: bash steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@master with: toolchain: nightly - uses: Swatinem/rust-cache@v2 - uses: taiki-e/setup-cross-toolchain-action@v1 with: target: ${{ matrix.target }} - uses: taiki-e/install-action@v2 with: tool: cargo-make,cargo-wix,cargo-pgo - if: startsWith(matrix.target, 'x86_64') run: | time cargo make build-artifacts -- --target ${{ matrix.target }} - if: success() uses: actions/upload-artifact@v7 with: name: ${{ matrix.target }}-artifacts path: tetanes/dist/ build-web: name: Build Web Artifacts (wasm32-unknown-unknown) if: > ((startsWith(github.event.release.name, 'tetanes') && !startsWith(github.event.release.name, 'tetanes-core')) || (inputs.tag && (inputs.os == 'all' || inputs.os == 'web'))) runs-on: ubuntu-latest defaults: run: shell: bash steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@master with: toolchain: nightly targets: wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 - uses: taiki-e/install-action@v2 with: tool: cargo-make,trunk - run: | sudo apt update sudo apt install -y libudev-dev libasound2-dev libssl-dev libfuse2 - run: | time cargo make build-artifacts -- --target wasm32-unknown-unknown - if: success() uses: actions/upload-artifact@v7 with: name: wasm32-unknown-unknown-artifacts path: tetanes/dist/ upload-artifacts: name: Attach Release Artifacts runs-on: ubuntu-latest needs: [build-linux, build-macos, build-windows, build-web] if: | always() && contains(needs.*.result, 'success') steps: - uses: actions/download-artifact@v8 with: path: artifacts merge-multiple: true - env: GH_TOKEN: ${{ github.token }} run: | gh release upload ${{ github.event.release.tag_name || inputs.tag }} \ artifacts/* --clobber --repo "${{ github.repository }}" update-tetanes-web: name: Update TetaNES Web needs: build-web runs-on: ubuntu-latest env: RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }} steps: - uses: actions/checkout@v6 with: repository: "lukexor/lukeworks" token: ${{ secrets.REPOS }} - uses: actions/download-artifact@v8 with: path: artifacts pattern: "wasm32-unknown-unknown-artifacts" - id: commit run: | rm -f web/public/tetanes-web/* tar -xzf artifacts/*.tar.gz -C web/public/tetanes-web/ VERSION=${RELEASE_TAG#"tetanes-v"} echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - uses: stefanzweifel/git-auto-commit-action@v7 with: file_pattern: "web/public/tetanes-web/*" commit_message: Updated TetaNES Web v${{ steps.commit.outputs.version }} update-homebrew-formula: name: Update Homebrew Formula needs: build-macos runs-on: ubuntu-latest env: RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }} steps: - uses: actions/checkout@v6 with: repository: "lukexor/homebrew-formulae" token: ${{ secrets.REPOS }} - uses: actions/download-artifact@v8 with: path: artifacts pattern: "*-apple-darwin-artifacts" merge-multiple: true - id: commit run: | x86_64_SHA=$(cat artifacts/*-x86_64-apple-darwin.tar.gz-sha256.txt | awk '{ print $1 }') aarch64_SHA=$(cat artifacts/*-aarch64-apple-darwin.tar.gz-sha256.txt | awk '{ print $1 }') VERSION=${RELEASE_TAG#"tetanes-v"} cat tetanes.rb.tmpl | \ sed "s/%VERSION%/${VERSION}/g" | \ sed "s/%x86_64_SHA%/${x86_64_SHA}/g" | \ sed "s/%aarch64_SHA%/${aarch64_SHA}/g" \ > Casks/tetanes.rb echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - uses: stefanzweifel/git-auto-commit-action@v7 with: file_pattern: "*.rb" commit_message: Version Bump v${{ steps.commit.outputs.version }} ================================================ FILE: .github/workflows/ci.yml ================================================ --- name: CI # yamllint disable-line rule:truthy on: push: branches: [main] paths-ignore: - "**.md" pull_request: paths-ignore: - "**.md" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true permissions: contents: read env: # Unnecessary for CI and just pollutes cache CARGO_INCREMENTAL: 0 # Remove debug symbols, substantially reduces cache size CARGO_PROFILE_DEV_DEBUG: 0 CARGO_PROFILE_TEST_DEBUG: 0 CARGO_TERM_COLOR: always RUST_LOG: debug RUST_BACKTRACE: 1 jobs: format: name: Check format runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: dtolnay/rust-toolchain@master with: toolchain: nightly targets: wasm32-unknown-unknown components: clippy - uses: Swatinem/rust-cache@v2 - run: | time cargo fmt --all --check lint-web: name: Lint Web runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: dtolnay/rust-toolchain@master with: toolchain: nightly targets: wasm32-unknown-unknown components: clippy - uses: Swatinem/rust-cache@v2 - run: | time cargo clippy --locked --lib --bin tetanes --target wasm32-unknown-unknown --all-features --keep-going -- -D warnings lint-tetanes: name: Lint TetaNES (${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [macos-latest, ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: dtolnay/rust-toolchain@master with: toolchain: nightly components: clippy - uses: Swatinem/rust-cache@v2 - if: startsWith(matrix.os, 'ubuntu') run: | sudo apt update sudo apt install -y libudev-dev libasound2-dev - run: | cargo clippy --locked -p tetanes --all-features --keep-going -- -D warnings lint-tetanes-core: name: Lint TetaNES Core (${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [macos-latest, ubuntu-latest, windows-latest] toolchain: [nightly, stable, 1.85] steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} components: clippy - uses: Swatinem/rust-cache@v2 - env: # Unset nightly RUSTFLAGS so we can lint with non-nightly toolchains CARGO_ENCODED_RUSTFLAGS: "" run: | cargo +${{ matrix.toolchain }} clippy --locked -p tetanes-core --all-features --keep-going -- -D warnings # No tests currently # test-tetanes: # name: Test TetaNES # runs-on: ubuntu-latest # steps: # - uses: actions/checkout@v6 # with: # fetch-depth: 0 # - uses: dtolnay/rust-toolchain@master # with: # toolchain: nightly # - uses: taiki-e/install-action@v2 # with: # tool: cargo-nextest # - uses: Swatinem/rust-cache@v2 # - run: | # sudo apt update # sudo apt install -y libudev-dev libasound2-dev # - run: | # cargo nextest run --locked -p tetanes --all-features --profile ci test-tetanes-core: name: Test TetaNES Core runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: dtolnay/rust-toolchain@master with: toolchain: nightly - uses: taiki-e/install-action@v2 with: tool: cargo-nextest - uses: Swatinem/rust-cache@v2 - run: | cargo nextest run --locked -p tetanes-core --all-features --profile ci docs-web: name: Docs Web runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@master with: toolchain: nightly targets: wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 - env: RUSTDOCFLAGS: -D warnings run: | time cargo doc --locked --no-deps --document-private-items --lib --target wasm32-unknown-unknown --all-features --keep-going docs-tetanes: name: Docs TetaNES runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [macos-latest, ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@master with: toolchain: nightly - uses: Swatinem/rust-cache@v2 - if: startsWith(matrix.os, 'ubuntu') run: | sudo apt update sudo apt install -y libudev-dev libasound2-dev - env: RUSTDOCFLAGS: -D warnings run: | cargo doc --locked --no-deps --document-private-items --all-features --workspace --keep-going ================================================ FILE: .github/workflows/outdated.yml ================================================ --- name: Check Outdated # yamllint disable-line rule:truthy on: schedule: # At 06:00 on day-of-month 2 and 16 - cron: "0 6 2,16 * *" permissions: contents: read env: # Unnecessary for CI and just pollutes cache CARGO_INCREMENTAL: 0 # Remove debug symbols, substantially reduces cache size CARGO_PROFILE_DEV_DEBUG: 0 CARGO_PROFILE_TEST_DEBUG: 0 CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 jobs: outdated: name: Check Outdated runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: dtolnay/install@cargo-outdated - run: | # gilrs and sysinfo currently conflict over objc2-core-foundation # accesskit is blocked by egui upgrading it # criterion is blocked by pprof upgrading it cargo outdated -e -d 1 --exit-code 1 \ --ignore gilrs \ --ignore sysinfo \ --ignore accesskit \ --ignore accesskit_winit \ --ignore criterion ================================================ FILE: .github/workflows/release-pr.yml ================================================ --- name: Release PR # yamllint disable-line rule:truthy on: push: branches: [main] permissions: pull-requests: write contents: write id-token: write env: # Unnecessary for CI and just pollutes cache CARGO_INCREMENTAL: 0 # Remove debug symbols, substantially reduces cache size CARGO_PROFILE_DEV_DEBUG: 0 CARGO_PROFILE_TEST_DEBUG: 0 CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 jobs: release-pr: name: Release PR runs-on: ubuntu-latest if: ${{ github.repository_owner == 'lukexor' }} steps: - uses: actions/checkout@v6 with: fetch-depth: 0 # Required to trigger post-release workflows token: ${{ secrets.RELEASE_PLZ_TOKEN }} - uses: dtolnay/rust-toolchain@master with: toolchain: nightly - uses: Swatinem/rust-cache@v2 - run: | sudo apt update sudo apt install -y libudev-dev libasound2-dev - uses: taiki-e/install-action@v2 with: tool: cargo-nextest - uses: Swatinem/rust-cache@v2 - run: | cargo nextest run --locked -p tetanes-core --all-features --profile ci - name: Run release-plz uses: MarcoIeni/release-plz-action@v0.5 env: # Required to trigger post-release workflows GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} ================================================ FILE: .github/workflows/security.yml ================================================ --- name: Security Audit # yamllint disable-line rule:truthy on: schedule: # At 06:00 once a week on Sunday - cron: "0 6 * * 0" push: branches: [main] pull_request: permissions: contents: read jobs: audit: name: Security Audit runs-on: ubuntu-22.04 steps: - name: Checkout repository uses: actions/checkout@v6 - uses: EmbarkStudios/cargo-deny-action@v2 ================================================ FILE: .github/workflows/triage.yml ================================================ --- name: Triage Issues # yamllint disable-line rule:truthy on: issues: types: [opened, reopened] permissions: issues: write jobs: triage: name: Triage Issue runs-on: ubuntu-latest steps: - name: add needs-triage label uses: andymckay/labeler@master with: add-labels: "needs-triage" ignore-if-labeled: true ================================================ FILE: .gitignore ================================================ tmp/ test_results* target/ .DS_Store !assets/macos/.DS_Store logs/ dist/ .direnv perf.* ================================================ FILE: .gitmodules ================================================ ================================================ FILE: .prettierignore ================================================ *.wxs ================================================ FILE: .rgignore ================================================ roms test_roms docs ================================================ FILE: Cargo.toml ================================================ # Disabled for now since it was ICEing # cargo-features = ["codegen-backend"] [workspace] resolver = "2" members = ["tetanes", "tetanes-core", "tetanes-utils"] [workspace.package] version = "0.14.1" edition = "2024" license = "MIT OR Apache-2.0" authors = ["Luke Petherbridge "] readme = "README.md" repository = "https://github.com/lukexor/tetanes.git" homepage = "https://lukeworks.tech/tetanes" [workspace.lints.clippy] all = { level = "warn", priority = -1 } missing_const_for_fn = "warn" print_literal = "warn" [workspace.lints.rust] future_incompatible = "warn" nonstandard_style = "warn" rust_2018_compatibility = "warn" rust_2018_idioms = "warn" rust_2021_compatibility = "warn" unused = "warn" [workspace.dependencies] anyhow = "1.0" bincode = { version = "2.0", default-features = false, features = [ "std", "serde", ] } cfg-if = "1.0" clap = { version = "4.5", default-features = false, features = [ "std", "help", "usage", "suggestions", "derive", ] } dirs = "6.0" image = { version = "0.25", default-features = false, features = ["png"] } serde = { version = "1.0", features = ["derive"] } tetanes-core = { version = "0.14", path = "tetanes-core" } thiserror = "2.0" tracing = { version = "0.1", default-features = false, features = [ "std", "release_max_level_info", ] } tracing-subscriber = { version = "0.3", default-features = false, features = [ "ansi", "std", "registry", "parking_lot", ] } serde_json = "1.0" web-time = "1.0" web-sys = "0.3.95" [profile.dev] # `debug = false` Saves both compile time and WASM download times debug = false # Playable framerates in development opt-level = 1 # Higher opt-level for deps speeds up incremental compile times and can improve # runtime performance [profile.dev.package."*"] opt-level = 3 [profile.dev.build-override] opt-level = 3 # TODO: Would be nice to move lto to `dist` but Trunk doesn't support profiles yet # See: https://github.com/trunk-rs/trunk/issues/605 # https://github.com/trunk-rs/trunk/issues/933 [profile.release] codegen-units = 1 lto = true # See: https://smallcultfollowing.com/babysteps/blog/2024/05/02/unwind-considered-harmful/ panic = "abort" strip = true # Profile to run performance profiling with debug symbols [profile.perf] inherits = "release" lto = "off" debug = true strip = false [workspace.metadata.wix] upgrade-guid = "DB76CEB0-15B8-4727-9C3E-55819AB5E7B9" path-guid = "5731AE63-80DE-4CD7-ADFA-9E79BEDCE08B" license = false eula = false ================================================ FILE: Cross.toml ================================================ [build] pre-build = [ "dpkg --add-architecture $CROSS_DEB_ARCH", """apt update && apt install -y \ libudev-dev:$CROSS_DEB_ARCH \ libssl-dev:$CROSS_DEB_ARCH \ libasound2-dev:$CROSS_DEB_ARCH """, ] ================================================ FILE: LICENSE-APACHE ================================================ Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: LICENSE-MIT ================================================ MIT License Copyright (c) 2021 Luke Petherbridge Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile.toml ================================================ [env] CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true [config] reduce_output = false skip_core_tasks = true default_to_workspace = false [tasks.default] alias = "run" [tasks.version] description = "Print the crate version" category = "Tools" script = ["echo Version: ${CARGO_MAKE_PROJECT_VERSION}"] [tasks.clean] description = "Clean up build artifacts" category = "Development" command = "cargo" args = ["clean"] [tasks.check-fmt] description = "Check format" category = "Development" command = "cargo" args = ["fmt", "--all", "--check"] [tasks.lint-web] description = "Lint TetaNES Web" category = "Development" command = "cargo" args = ["clippy", "--locked", "--lib", "--bin", "tetanes", "--target", "wasm32-unknown-unknown", "--all-features", "--keep-going"] dependencies = ["add-wasm-target"] [tasks.lint] description = "Lint TetaNES" category = "Development" command = "cargo" args = ["clippy", "--locked", "--all-features", "--keep-going"] dependencies = ["lint-web"] [tasks.pgo-profile] description = "Run benchmark to generate PGO profile" category = "Build" command = "cargo" args = ["pgo", "bench", "--", "--bench", "clock_frame"] [tasks.pgo-build] description = "Optimize TetaNES with PGO" category = "Build" command = "cargo" args = ["pgo", "optimize", "build", "--", "-p", "tetanes", "${@}"] [tasks.build] description = "Build TetaNES with PGO" category = "Build" dependencies = ["pgo-profile", "pgo-build"] [tasks.build-artifacts] description = "Build TetaNES Artifacts for a given target_arch" category = "Build" command = "cargo" args = ["run", "--bin", "build_artifacts", "${@}"] [tasks.build-cross] description = "Cross-Build TetaNES for a given target_arch" category = "Build" command = "cross" args = ["build", "-p", "tetanes", "${@}"] [tasks.bench] description = "Benchmark TetaNES" category = "Development" command = "perf" args = [ "stat", "-e", "cycles,instructions,cache-misses,cache-references,branch-misses,branches,L1-dcache-load-misses,L1-dcache-loads", "taskset", "-c", "0", "cargo", "bench", "--profile", "perf", "--bench", "clock_frame", "${@}" ] [tasks.test] description = "Test TetaNES" category = "Development" command = "cargo" args = ["nextest", "run", "--locked", "--all-features", "--no-fail-fast", "${@}"] [tasks.run] description = "Run TetaNES in release mode" category = "Development" command = "cargo" args = ["run", "-p", "tetanes", "--release", "${@}"] [tasks.profile] description = "Run TetaNES in release mode w/profiling" category = "Development" command = "cargo" args = ["run", "-p", "tetanes", "--release", "--features", "profiling", "${@}"] [tasks.dev] description = "Run TetaNES in development mode" category = "Development" command = "cargo" args = ["run", "-p", "tetanes", "${@}"] [tasks.add-wasm-target] description = "Add wasm target" category = "Development" command = "rustup" args = ["target", "add", "wasm32-unknown-unknown"] [tasks.build-web] description = "Build TetaNES Web" category = "Build" command = "trunk" args = ["build", "--config", "tetanes/Cargo.toml", "--release", "--dist", "dist/web", "--public-url", "./"] dependencies = ["add-wasm-target"] [tasks.docs-web] description = "Document TetaNES Web" category = "Documentation" command = "cargo" args = ["doc", "--locked", "--no-deps", "--document-private-items", "--lib", "--target", "wasm32-unknown-unknown", "--all-features", "--keep-going"] dependencies = ["add-wasm-target"] [tasks.docs] description = "Document TetaNES" category = "Documentation" command = "cargo" args = ["doc", "--locked", "--no-deps", "--document-private-items", "--all-features", "--workspace", "--keep-going"] dependencies = ["docs-web"] [tasks.run-web] description = "Run TetaNES Web in release mode" category = "Development" command = "trunk" args = ["serve", "--release", "--config", "tetanes/Cargo.toml", "--address", "0.0.0.0"] dependencies = ["add-wasm-target"] [tasks.profile-web] description = "Run TetaNES Web in release mode w/profiling" category = "Development" command = "trunk" args = ["serve", "--release", "--features", "profiling", "--config", "tetanes/Cargo.toml", "--address", "0.0.0.0"] dependencies = ["add-wasm-target"] [tasks.dev-web] description = "Run TetaNES Web in development mode" category = "Development" command = "trunk" args = ["serve", "--config", "tetanes/Cargo.toml", "--address", "0.0.0.0"] dependencies = ["add-wasm-target"] ================================================ FILE: README.md ================================================ # TetaNES [![Build Status]][build] [![Doc Status]][docs] [![Latest Version]][crates.io] [![Downloads]][crates.io] [![License]][gnu] [build status]: https://img.shields.io/github/actions/workflow/status/lukexor/tetanes/ci.yml?branch=main [build]: https://github.com/lukexor/tetanes/actions/workflows/ci.yml [doc status]: https://img.shields.io/docsrs/tetanes?style=plastic [docs]: https://docs.rs/tetanes/ [latest version]: https://img.shields.io/crates/v/tetanes?style=plastic [crates.io]: https://crates.io/crates/tetanes [downloads]: https://img.shields.io/crates/d/tetanes?style=plastic [license]: https://img.shields.io/crates/l/tetanes?style=plastic [gnu]: https://github.com/lukexor/tetanes/blob/main/LICENSE-MIT 📖 [Summary](#summary) - ✨ [Features](#features) - 🌆 [Screenshots](#screenshots) - 🚀 [Getting Started](#getting-started) - 🛠️ [Roadmap](#roadmap) - ⚠️ [Known Issues](#known-issues) - 💬 [Contact](#contact) ## Summary TetaNES > photo credit for background: [Zsolt Palatinus](https://unsplash.com/@sunitalap) > on [unsplash](https://unsplash.com/photos/pEK3AbP8wa4) `TetaNES` is a cross-platform emulator for the Nintendo Entertainment System (NES) released in Japan in 1983 and North America in 1986, written in [Rust][] using [wgpu][]. It runs on Linux, macOS, Windows, and in a web browser with [Web Assembly][]. It started as a personal curiosity that turned into a passion project. It is still being actively developed with new features and improvements constantly being added. It is a fairly accurate emulator that can play most NES titles. `TetaNES` is also meant to showcase using Rust's performance, memory safety, and fearless concurrency features in a large project. Features used in this project include complex enums, traits, generics, matching, iterators, channels, and threads. Try it out in your [browser](https://lukeworks.tech/tetanes-web)! ## Features - Runs on Linux, macOS, Windows, and Web. - Standalone emulation core in `tetanes-core`. - NTSC, PAL and Dendy emulation. - Headless Mode when using `tetanes-core`. - Pixellate and NTSC filters. - CRT shader for that retro feel. - Up to 4 players with gamepad support. - Zapper (Light Gun) support using the mouse. - iNES and NES 2.0 ROM header formats supported. - Over 30 supported mappers covering >90% of licensed games. - Game Genie Codes. - PPU Debugger - Runtime performance stats - Preference snd keybonding menus using [egui](https://egui.rs). - Increase/Decrease speed & Fast Forward - Visual & Instant Rewind - Save & Load States - Battery-backed RAM saves - Screenshots - Gameplay recording and playback - Audio recording ## Screenshots Donkey Kong  Super Mario Bros. The Legend of Zelda  Metroid ## TetaNES Core TetaNES is split into two crates. This is the primary crate, which provides the cross-platform emulator UI binary. [tetanes-core](https://crates.io/crates/tetanes_core) is the emulation library that emulator developers can use to develop custom emulator applications with. `tetanes-core` is aimed to have stronger stability guarantees, but it's still not `1.0` yet and there are several large features on the roadmap that may result in breaking changes. ## Stability Preferences and save file formats are fairly stable at this point, but since TetaNES is not yet `1.0` and there are several large features on the roadmap that may result in breaking changes which may result in being unable to restore your preferences or save files. Once some of these larger features are completed, and `1.0` is released, more effort will be dedicatged to versioning these files for backward compatibility in the event of future breaking changes. ## Getting Started `TetaNES` runs on all major operating systems (Linux, macOS, Windows, and the web). ### Install There are multiple options for installation, depending on your operating system, preference and existing tooling. #### Linux ##### Ubuntu/Debian A `.deb` package is provided under `Assets` on the latest [Release][]. Once downloaded, you can install it and the required dependencies. e.g. ```sh sudo apt install ./tetanes-0.10.0-1-amd64.deb ``` ##### Other Distros An [AppImage](https://appimage.org/) is provided under `Assets` on the latest [Release][]. Simply download it and put it wherever you want. A `.tar.gz` package is also provided under `Assets` on the latest [Release][]. You can place the `tetanes` binary anywhere in your `PATH`. The following dependencies are required to be installed: - ALSA Shared Library - GTK3 e.g. `apt install libasound2 libgtk-3-0` `dnf install alsa-lib gtk3` `pacman -Sy alsa-lib gtk3` #### MacOS ##### App Bundle The easiest is to download the correct app bundle for your processor. The `.dmg` downloads can be found under the `Assets` section of the latest [Release][]. ##### Homebrew `TetaNES` can also be installed through [Homebrew](https://brew.sh/). ```sh brew install lukexor/formulae/tetanes ``` #### Windows A windows installer is provided under `Assets` on the latest [Release][]. Note: You will need the latest ["Microsoft Visual C++ 2015 - 2022 Redistributable"](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#latest-microsoft-visual-c-redistributable-version) installed, otherwise you'll get an error about `vcruntime140.dll` not being found. #### Cargo Install You can also build and install with `cargo` which comes with [rustup](https://www.rust-lang.org/tools/install). ```sh cargo install tetanes ``` This will install the latest released version of the `TetaNES` binary to your `cargo` bin directory located at either `$HOME/.cargo/bin/` on a Unix-like platform or `%USERPROFILE%\.cargo\bin` on Windows. Alternatively, if you have [`cargo binstall`](https://crates.io/crates/cargo-binstall/) installed: ```sh cargo binstall tetanes ``` This will try to find the target binary for your platform from the latest [Release][] or install from source, similar to above. #### Web You can also play directly in your web browser without installing by visiting . ### Usage ```text Usage: tetanes [OPTIONS] [PATH] Arguments: [PATH] The NES ROM to load or a directory containing `.nes` ROM files. [default: current directory] Options: --rewind Enable rewinding -s, --silent Silence audio -f, --fullscreen Start fullscreen -4, --four-player Set four player adapter. [default: 'disabled'] [possible values: disabled, four-score, satellite] -z, --zapper Enable zapper gun --no-threaded Disable multi-threaded -m, --ram-state Choose power-up RAM state. [default: "all-zeros"] [possible values: all-zeros, all-ones, random] -w, --emulate-ppu-warmup Whether to emulate PPU warmup where writes to certain registers are ignored. Can result in some games not working correctly -r, --region Choose default NES region. [default: "ntsc"] [possible values: ntsc, pal, dendy] -i, --save-slot Save slot. [default: 1] --no-load Don't load save state on start --no-save Don't auto save state or save on exit -x, --speed Emulation speed. [default: 1.0] -g, --genie-code Add Game Genie Code(s). e.g. `AATOZE` (Start Super Mario Bros. with 9 lives) --config Custom Config path -c, --clean "Default Config" (skip user config and previous save states) -d, --debug Start with debugger open -h, --help Print help -V, --version Print version ``` [iNES][] and [NES 2.0][] formatted ROMS are supported, though some advanced `NES 2.0` features may not be implemented. [ines]: https://wiki.nesdev.org/w/index.php/INES [nes 2.0]: https://wiki.nesdev.org/w/index.php/NES_2.0 ### Supported Mappers Support for the following mappers is currently implemented or in development: | # | Name | Example Games | # of Games1 | % of Games1 | | --- | --------------------- | ------------------------------------------ | ---------------------- | ---------------------- | | 000 | NROM | Bomberman, Donkey Kong, Super Mario Bros. | ~247 | ~10% | | 001 | SxROM/MMC1B/C | Metroid, Legend of Zelda, Tetris | ~680 | ~28% | | 002 | UxROM | Castlevania, Contra, Mega Man | ~270 | ~11% | | 003 | CNROM | Arkanoid, Paperboy, Pipe Dream | ~155 | ~6% | | 004 | TxROM/MMC3/MMC6 | Kirby's Adventure, Super Mario Bros. 2/3 | ~599 | ~24% | | 005 | ExROM/MMC5 | Castlevania 3, Laser Invasion | ~24 | <0.01% | | 007 | AxROM | Battletoads, Marble Madness | ~75 | ~3% | | 009 | PxROM/MMC2 | Punch Out!! | 1 | <0.01% | | 010 | FxROM/MMC4 | Fire Emblem Gaiden | 3 | <0.01% | | 011 | Color Dreams | Crystal Mines, Metal Fighter | 15 | ~1% | | 016 | Bandai FCG | Dragon Ball: Daimaou Fukkatsu | 14 | ~1% | | 018 | Jaleco SS 88006 | Magic John | 15 | ~1% | | 019 | Namco163 | Battle Fleet, Dragon Ninja | 20 | ~1% | | 024 | VRC6a | Akumajou Densetsu | 1 | <0.01% | | 026 | VRC6b | Madara, Esper Dream 2 | 2 | <0.01% | | 034 | BNROM/NINA-001 | Deadly Towers, Impossible Mission II | 3 | <0.01% | | 066 | GxROM/MxROM | Super Mario Bros. + Duck Hunt | ~17 | <0.01% | | 069 | Sunsoft/FME-7 | Batman: Return of the Joker, Gimmick! | ~15 | <0.01% | | 071 | Camerica/Codemasters | Firehawk, Bee 52, MiG 29 - Soviet Fighter | ~15 | <0.01% | | 076 | DxROM/Namco 108 | Megami Tensei: Digital Devil Story | 1 | <0.01% | | 079 | NINA-003/006 | Black Jack, Double Strike | 16 | <0.01% | | 088 | DxROM/Namco 108 | Quinty, Dragon Spirit - Aratanaru Densetsu | 3 | <0.01% | | 095 | DxROM/Namco 108 | Dragon Buster | 1 | <0.01% | | 113 | NINA-003/006 | HES 6-in-1, Total Funpak | ~3 | <0.01% | | 144 | Color Dreams | Death Race | 1 | <0.01% | | 146 | NINA-003/006 | Galactic Crusader | 1 | <0.01% | | 153 | Bandai FCG | Famicom Jump II: Saikyou no 7-nin | 1 | <0.01% | | 154 | DxROM/Namco 108 | Devil Man | 1 | <0.01% | | 157 | Bandai FCG/Datach | SD Gundam Wars | 7 | <0.01% | | 155 | SxROM/MMC1A | Tatakae!! Ramen Man: Sakuretsu Choujin | 2 | <0.01% | | 159 | Bandai FCG | Dragon Ball Z: Kyoushuu! Saiya-jin | 4 | <0.01% | | 206 | DxROM/Namco 108 | Fantasy Zone, Gauntlet | 45 | ~2% | | 210 | Namco175/340 | Dream Master, Family Circuit '91 | 4 | <0.01% | | | | | ~2256 / 2447 | ~92.2% | 1. [Source](http://bootgod.dyndns.org:7777/stats.php?page=6) [Mirror](https://nescartdb.com/) ### Controls Keybindings can be customized in the keybindings menu. Below are the defaults. NES joypad: | Button | Keyboard (Player 1) | Controller | | --------- | ------------------- | ---------------- | | A | Z | East | | B | X | South | | A (Turbo) | A | North | | B (Turbo) | S | West | | Select | Q | Select | | Start | W | Start | | D-Pad | Arrow Keys | D-Pad | Controller Layout: SDL-compatible mappings are used: but can be overriden by setting `SDL_GAMECONTROLLERCONFIG`. ```text Left Triggers Right Triggers _=====_ _=====_ / _____ \ / _____ \ +.-'_____'-.---------------------------.-' '-.+ / | | '. .' \ / ___| /|\ |___ \ / (N) \ / | | | ; _ _ ; ; Action Pad D-Pad | | <--- ---> | | <:_| |_:> | (W) (E) | (South, East, | |___ | ___| ; Select Start ; ; North, West) |\ | \|/ | / _ _ \ (S) /| | \ |_____| .','" "', ,'" "', '. .' | | '-.______.-' / Left \------/ Right \ '-._____.-' | | /\ Stick / \ Stick /\ | | / '.___.' '.___.' \ | | / \ | \ / \ / \________/ \_________/ ``` Emulator shortcuts: | Action | Keyboard | Controller | | ----------------------------- | ------------ | -------------- | | Pause | Escape | Guide Button | | About TetaNES | F1 | | | Preferences Menu | Ctrl-P or F2 | | | Load/Open ROM | Ctrl-O or F3 | | | Quit | Ctrl-Q | | | Reset | Ctrl-R | | | Power Cycle | Ctrl-H | | | Increase Speed by 25% | = | Right Shoulder | | Decrease Speed by 25% | - | Left Shoulder | | Increase Emulation Scale | Shift-= | | | Decrease Emulation Scale | Shift-- | | | Increase UI Scale | Ctrl-= | | | Decrease UI Scale | Ctrl-- | | | Fast-Forward 2x | Space (Hold) | | | Set Save State Slot (1-4) | Ctrl-(1-4) | | | Save State | Ctrl-S | | | Load State | Ctrl-L | | | Instant Rewind | R (Tap) | | | Visual Rewind | R (Hold) | | | Take Screenshot | F10 | | | Toggle Gameplay Recording | Shift-V | | | Toggle Audio Recording | Shift-R | | | Toggle Audio | Ctrl-M | | | Toggle Pulse Channel 1 | Shift-1 | | | Toggle Pulse Channel 2 | Shift-2 | | | Toggle Triangle Channel | Shift-3 | | | Toggle Noise Channel | Shift-4 | | | Toggle DMC Channel | Shift-5 | | | Toggle Mapper Channel | Shift-6 | | | Toggle Fullscreen | Ctrl-Enter | | | Toggle NTSC Filter | Ctrl-N | | | Toggle CRT Shader | Ctrl-T | | | Toggle CPU Debugger | Shift-D | | | Toggle PPU Debugger | Shift-P | | | Toggle APU Debugger | Shift-A | | While the CPU Debugger is open: | Action | Keyboard | | ----------------------------- | -------- | | Step a single CPU instruction | C | | Step over a function | O | | Step out of a function | Shift-O | | Step a single scanline | Shift-L | | Step an entire frame | Shift-F | While the PPU Debugger is open: | Action | Keyboard | | ------------------------------ | --------------- | | Move debug scanline up by 1 | Ctrl-Up | | Move debug scanline up by 10 | Ctrl-Shift-Up | | Move debug scanline down by 1 | Ctrl-Down | | Move debug scanline down by 10 | Ctrl-Shift-Down | Other mappings can be found and modified in the `Config -> Keybinds` menu. ### Directories `TetaNES` saves files to disk to support a number of features and, depending on the file type, varies based on operating system. #### Preferences - Linux: `$HOME/.config` - macOS: `$HOME/Library/Application Support` - Windows: `%LOCALAPPDATA%\tetanes` - Web: localStorage (e.g. `config/config.json`) #### Screenshots - Linux, macOS, & Windows: `$HOME/Pictures` - Web: Does not currently support saving screenshots. #### Replay Recordings - Linux, macOS, & Windows: `$HOME/Documents` - Web: Does not currently support saving recordings. #### Audio Recordings - Linux, macOS, & Windows: `$HOME/Music` - Web: Does not currently support saving recordings. #### Battery-backed RAM, save states, and logs - Linux: `$HOME/.local/share/tetanes` - macOS: `$HOME/Library/Application Support/tetanes` - Windows: `%LOCALAPPDATA%\tetanes` - Web: localStorage (e.g. `data/save/AO Demo/slot-1.sav`) ### Powerup State The original NES hardware had semi-random contents located in RAM upon power-up and several games made use of this to seed their Random Number Generators (RNGs). By default, `TetaNES` honors the original hardware and emulates randomized powerup RAM state. This shows up in several games such as `Final Fantasy`, `River City Ransom`, and `Impossible Mission II`, amongst others. Not emulating this would make these games seem deterministic when they weren't intended to be. If you would like `TetaNES` to provide fully deterministic emulated power-up state, you'll need to change the `RAM State` setting in the configuration menu and trigger a power-cycle or use the `-m`/`--ram_state` flag from the command line. ### Building/Running To build/run `TetaNES`, you'll need a nightly version of the compiler and run `cargo build` or `cargo build --release` (if you want better framerates). To run the web version, you'll also need the `wasm32-unknown-unknown` target and [trunk](https://trunkrs.dev/) installed: ```sh rustup target add wasm32-unknown-unknown trunk serve --release ``` Unit and integration tests can be run with `cargo test`. There are also several test roms that can be run to test various capabilities of the emulator. They are all located in the `tetanes-core/tests_roms/` directory. Run them in a similar way you would run a game. e.g. ```sh cargo run --release tetanes-core/test_roms/cpu/nestest.nes ``` #### Feature Flags - **webgpu** - Enables the [`BrowserWebGpu`](https://docs.rs/wgpu/latest/wgpu/enum.Backend.html) backend for TetaNES Web. The default is to use `WebGl2` until WebGPU is stable across all platforms and browsers. Currently pending Firefox and Chrome on Linux (See: ). ### Troubleshooting If you get an error running a ROM that's using the supported Mapper list above, it could be a corrupted or incompatible ROM format. If you're unsure which games use which mappers, see . Trying other versions of the same game from different sources sometimes resolves the issue. If you get some other error when trying to start a game that previously worked, try removing any configurations or save states from the [Directories](#directories) listed above to ensure it's not an incompatible savestate file causing the issue. If you encounter any shortcuts not working, ensure your operating system does not have a binding for it that is overriding it. macOS specifically has many things bound to `Ctrl-*`. If an an issue is not already created, please use the [github issue tracker][] to create it. ## Roadmap See [ROADMAP.md][]. ## Known Issues See the [github issue tracker][]. ## Documentation In addition to the wealth of information in the `docs/` directory, I also referenced these websites extensively during development: - [NES Documentation (PDF)](https://nesdev.org/NESDoc.pdf) - [NES Dev Wiki](https://wiki.nesdev.org/w/index.php/Nesdev_Wiki) - [6502 Datasheet](https://archive.6502.org/datasheets/rockwell_r650x_r651x.pdf) ## License `TetaNES` is licensed under a MIT or Apache-2.0 license. See the `LICENSE-MIT` or `LICENSE-APACHE` file in the root for a copy. ## Contribution While this is primarily a personal project, I welcome any contributions or suggestions. Feel free to submit a pull request if you want to help out! ### Contact For issue reporting, please use the [github issue tracker][]. You can also contact me directly at . ## Credits Implementation was inspiried by several amazing NES projects, without which I would not have been able to understand or digest all the information on the NES wiki. - [fogleman NES](https://github.com/fogleman/nes) - [sprocketnes](https://github.com/pcwalton/sprocketnes) - [nes-emulator](https://github.com/MichaelBurge/nes-emulator) - [LaiNES](https://github.com/AndreaOrru/LaiNES) - [ANESE](https://github.com/daniel5151/ANESE) - [FCEUX](https://fceux.com/web/home.html) I also couldn't have gotten this far without the amazing people over on the [NES Dev Forums](https://forums.nesdev.org/): - [blargg](https://forums.nesdev.org/memberlist.php?mode=viewprofile&u=17) for all his amazing [test roms](https://wiki.nesdev.org/w/index.php/Emulator_tests) - [bisqwit](https://bisqwit.iki.fi/) for his test roms & integer NTSC video implementation - [Disch](https://forums.nesdev.org/memberlist.php?mode=viewprofile&u=33) - [Quietust](https://forums.nesdev.org/memberlist.php?mode=viewprofile&u=7) - [rainwarrior](https://forums.nesdev.org/memberlist.php?mode=viewprofile&u=5165) - And many others who helped me understand the stickier bits of emulation Also, a huge shout out to [OneLoneCoder](https://github.com/OneLoneCoder/) for his [NES](https://github.com/OneLoneCoder/olcNES) and [olcPixelGameEngine](https://github.com/OneLoneCoder/olcPixelGameEngine) series as those helped a ton in some recent refactorings. [rust]: https://www.rust-lang.org/ [wgpu]: https://wgpu.rs/ [web assembly]: https://webassembly.org/ [github issue tracker]: https://github.com/lukexor/tetanes/issues [ROADMAP.md]: ROADMAP.md [Release]: https://github.com/lukexor/tetanes/releases/latest ================================================ FILE: ROADMAP.md ================================================ # Roadmap - NES Formats & Run Modes - [x] NTSC - [x] PAL - [x] Dendy - [x] Headless mode - Central Processing Unit (CPU) - [x] Official Instructions - [x] Unofficial Instructions - [x] Cycle Accurate - Picture Processing Unit (PPU) - [x] Pixellate Filter - [x] NTSC Filter - [x] CRT Filter - Audio Processing Unit (APU) - [x] Pulse Channels - [x] Triangle Channel - [x] Noise Channel - [x] Delta Modulation Channel (DMC) - Player Input - [x] 1-2 Player w/ Keyboard or Controllers - [x] 3-4 Player Support w/ Controllers - [x] Zapper (Light Gun) - Cartridge - [x] iNES Format - [x] NES 2.0 Format - [ ] Complete NES 2.0 support - Mappers - [x] Mapper 000 - NROM - [x] Mapper 001 - SxROM/MMC1B/C - [x] Mapper 002 - UxROM - [x] Mapper 003 - CNROM - [x] Mapper 004 - TxROM/MMC3/MMC6 - [x] Mapper 005 - ExROM/MMC5 - [x] Mapper 007 - AxROM - [x] Mapper 009 - PxROM/MMC2 - [x] Mapper 010 - FxROM/MMC4 - [x] Mapper 011 - Color Dreams - [x] Mapper 019 - Namco 163 - [ ] Mapper 023 - VRC2b/VRC4e - [ ] Mapper 025 - VRC4b/VRC4d - [x] Mapper 024 - VRC6a - [x] Mapper 026 - VRC6b - [x] Mapper 034 - BNROM/NINA-001 - [ ] Mapper 064 - RAMBO-1 - [x] Mapper 066 - GxROM/MxROM - [ ] Mapper 068 - After Burner - [x] Mapper 069 - FME-7/Sunsoft 5B - [x] Mapper 071 - Camerica/Codemasters/BF909x - [x] Mapper 079 - NINA-03/NINA-06 - [x] Mapper 155 - SxROM/MMC1A - [x] Mapper 206 - DxROM/Namco 118/MIMIC-1 - Releases - [x] macOS Binaries - [x] Linux Binaries - [x] Windows Binaries - [x] User Interface (UI) - [x] WebAssembly (WASM) - Run TetaNES in the browser! - [x] Configurable keybinds and default settings - Menus - [x] Configuration options - [x] Customize Keybinds & Controllers - [x] Load/Open ROM with file browser - [x] Recent Game Selection - [x] About Menu - [ ] Config paths overrides - [x] Increase/Decrease Speed - [x] Fast-forward - [x] Instant Rewind (2 seconds) - [x] Visual Rewind (Holding R will time-travel backward) - [x] Save/Load State - [x] Auto-save - [x] Take Screenshots - [x] Gameplay Recording - [x] Sound Recording (Save those memorable tunes!) - [x] Toggle Fullscreen - [x] Toggle Sound - [x] Toggle individual sound channels - [x] Toggle FPS - [x] Toggle Messages - [x] Change Video Filter - Game Genie Support - [x] Command-Line - [ ] UI Menu - [ ] [WideNES](https://prilik.com/ANESE/wideNES) - [ ] Network Multi-player - [ ] Self Updater - [x] Drag and drop load ROMs - Testing/Debugging/Documentation - [x] Debugger (Displays CPU/PPU status, registers, and disassembly) - [x] Step Into/Out/Over - [x] Step Scanline/Frame - [ ] Breakpoints - [ ] Modify state - [ ] Labels - [ ] Hex Memory Editor & Debugger - PPU Viewer - [ ] Scanline Hit Configuration (For debugging IRQ Nametable changes) - [x] Nametable Viewer (background rendering) - [x] CHR Viewer (sprite tiles) - [x] OAM Viewer (on screen sprites) - [x] Palette Viewer - [ ] APU Viewer (Displays audio status and registers) - [x] Automated ROM tests (including [nestest](https://www.qmtpro.com/~nes/misc/nestest.txt)) - [ ] Detailed Documentation - Logging - [x] Environment logging - [x] File logging ================================================ FILE: assets/linux/tetanes.desktop ================================================ [Desktop Entry] Name=TetaNES Exec=tetanes Icon=icon Type=Application Categories=Game; ================================================ FILE: assets/macos/Info.plist ================================================ CFBundleDevelopmentRegion English CFBundleIdentifier tech.lukeworks.tetanes CFBundleName tetanes CFBundleDisplayName TetaNES CFBundleExecutable tetanes CFBundleIconFile Icon CFBundleVersion %VERSION% ================================================ FILE: cliff.toml ================================================ [changelog] header = """ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n """ body = """ {% if version %}\ {% if previous.version %}\ ## [{{ version | trim_start_matches(pat="v") }}](/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }} {% else %}\ ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} {% endif %}\ {% else %}\ ## [unreleased] {% endif %}\ {% macro commit(commit) -%} - {% if commit.scope %}*({{ commit.scope }})* {% endif %}{% if commit.breaking %}[**breaking**] {% endif %}\ {{ commit.message | upper_first }} - ([{{ commit.id | truncate(length=7, end="") }}](/commit/{{ commit.id }}))\ {% endmacro -%} {% for group, commits in commits | group_by(attribute="group") %} ### {{ group | striptags | trim | upper_first }} {% for commit in commits | filter(attribute="scope") | sort(attribute="scope") %} {{ self::commit(commit=commit) }}\ {% endfor %} {% raw %}\n{% endraw %}\ {%- for commit in commits %} {%- if not commit.scope -%} {{ self::commit(commit=commit) }} {% endif -%} {% endfor -%} {% endfor %}\n """ trim = true footer = "" postprocessors = [ { pattern = '', replace = "https://github.com/lukexor/tetanes" }, ] [git] conventional_commits = true filter_unconventional = true split_commits = false commit_preprocessors = [ { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))" }, ] commit_parsers = [ { message = "^feat", group = "⛰️ Features" }, { message = "^fix", group = "🐛 Bug Fixes" }, { message = "^doc", group = "📚 Documentation" }, { message = "^perf", group = "⚡ Performance" }, { message = "^refactor", group = "🚜 Refactor" }, { message = "^style", group = "🎨 Styling" }, { message = "^test", group = "🧪 Testing" }, { message = "^chore\\(release\\): prepare for", skip = true }, { message = "^chore\\(deps\\)", skip = true }, { message = "^build\\(deps\\)", skip = true }, { message = "^chore\\(pr\\)", skip = true }, { message = "^chore\\(pull\\)", skip = true }, { message = "^chore|ci", group = "⚙️ Miscellaneous Tasks" }, { body = ".*security", group = "🛡️ Security" }, { message = "^revert", group = "◀️ Revert" }, ] protect_breaking_commits = false filter_commits = true tag_pattern = "v[0-9].*" skip_tags = "beta|alpha" ignore_tags = "rc" topo_order = false sort_commits = "newest" ================================================ FILE: deny.toml ================================================ [graph] all-features = true no-default-features = false [output] feature-depth = 1 [advisories] version = 2 db-path = "~/.cargo/advisory-db" db-urls = ["https://github.com/rustsec/advisory-db"] yanked = "warn" ignore = [ { id = "RUSTSEC-2024-0436", reason = "paste is a downstream dependency of many crates with no upgrade path" }, { id = "RUSTSEC-2025-0141", reason = "need to plan an upgrade path from bincode to rkyv" }, ] [licenses] version = 2 allow = [ "Apache-2.0", # https://tldrlegal.com/license/apache-license-2.0-(apache-2.0) "Apache-2.0 WITH LLVM-exception", # https://spdx.org/licenses/LLVM-exception.html "BSD-2-Clause", # https://tldrlegal.com/license/bsd-2-clause-license-(freebsd) "BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised) "BSL-1.0", # https://tldrlegal.com/license/boost-software-license-1.0-explained "CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/ "CDLA-Permissive-2.0", # https://cdla.dev/permissive-2-0/. Used by webpki-roots on Linux. "ISC", # https://www.tldrlegal.com/license/isc-license "MIT", # https://tldrlegal.com/license/mit-license "MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11. "OFL-1.1", # https://spdx.org/licenses/OFL-1.1.html "OpenSSL", # https://openssl-library.org/source/license/index.html "Ubuntu-font-1.0", # https://ubuntu.com/legal/font-licence "Unicode-3.0", # https://spdx.org/licenses/Unicode-3.0.html "Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib) ] confidence-threshold = 0.8 exceptions = [] [licenses.private] ignore = false registries = [] [bans] multiple-versions = "allow" wildcards = "deny" highlight = "all" workspace-default-features = "allow" external-default-features = "allow" allow = [] deny = [] skip = [] skip-tree = [] [sources] unknown-registry = "deny" unknown-git = "deny" allow-registry = ["https://github.com/rust-lang/crates.io-index"] allow-git = [] [sources.allow-org] github = [] gitlab = [] bitbucket = [] ================================================ FILE: docs/apu/apu_ref.txt ================================================ NES APU Sound Hardware Reference -------------------------------- This reference covers Nintendo Entertainment System (NES) sound hardware in as much detail as I know. It is intended primarily to assist in the implementation of emulators and might also be useful as a programmer reference. Tables, diagrams, and formulas are formatted for a mono-spaced font, like Courier. The latest version is kept at http://www.slack.net/~ant/nes-emu/apu_ref.txt ----- Intro ----- This reference is based on the results of tests I have run on a 1988-model US NTSC NES which contains the G revision of the 2A03 CPU/APU and the NES-CPU-07 version of the main board. PAL hardware will be covered once tests are performed on it. Feel free to incorporate this information in references and other documentation. While implementing a NES sound emulator, even after reading the available documentation I still had many unanswered questions, so I made a simple development cartridge to test on a real NES. This was very successful and revealed many new details. My notes consisted of differences from existing documentation, but this didn't seem to be a very reliable way to release my findings, so I decided to write a concise reference. For those familiar with NESSOUND.TXT and DMC.TXT, the following differences should be specifically noted: Corrections: - DMC table entry $D should be $2A0 instead of $2A8 - Frame sequencer - Square's duty generator Clarifications: - DMC - Triangle's linear counter - Length Counter operation and status register behavior It should go without saying that the model presented here probably doesn't match the actual logic gate arrangement in the NES. It makes no difference how the hardware is implemented, as long as its behavior matches what is described here Corrections, questions and additions are welcome. I keep up with the forum at http://nesdev.parodius.com/ and can be contacted via blargg at the mail.com domain. ----- To Do ----- - See if comprehensive emulator test ROM is even practical. - Probe complete power-up state and reset state. - Test PAL hardware to determine APU frame rate, DMC and noise period tables. - Check complete behavior of each unit of each channel to be sure common units behave the same for all channels. - Double-check details on NES hardware again. - Determine post-DAC filtering done before output. -------- Overview -------- The APU is composed of five channels: square 1, square 2, triangle, noise, delta modulation channel (DMC). Each has a variable-rate timer clocking a waveform generator, and various modulators driven by low-frequency clocks from a frame sequencer. The DMC plays samples while the other channels play waveforms. The waveform channels have duration control, some have a volume envelope unit, and a couple have a frequency sweep unit. Square 1/Square 2 $4000/4 ddle nnnn duty, loop env/disable length, env disable, vol/env period $4001/5 eppp nsss enable sweep, period, negative, shift $4002/6 pppp pppp period low $4003/7 llll lppp length index, period high Triangle $4008 clll llll control, linear counter load $400A pppp pppp period low $400B llll lppp length index, period high Noise $400C --le nnnn loop env/disable length, env disable, vol/env period $400E s--- pppp short mode, period index $400F llll l--- length index DMC $4010 il-- ffff IRQ enable, loop, frequency index $4011 -ddd dddd DAC $4012 aaaa aaaa sample address $4013 llll llll sample length Common $4015 ---d nt21 length ctr enable: DMC, noise, triangle, pulse 2, 1 $4017 fd-- ---- 5-frame cycle, disable frame interrupt Status (read) $4015 if-d nt21 DMC IRQ, frame IRQ, length counter statuses ------ Basics ------ Hexadecimal values are prefixed by a $ except for some single-hex-digit sequences where it's clear that they are hex. Bits are numbered from 0 to 7, corresponding with the least to most significant bits of a byte; bit n has a binary weight of 2^n. A flag is a two-state variable that can be either set or clear. When implemented in a bit, clear = 0 and set = 1. A divider outputs a clock every n input clocks, where n is the divider's period. It contains a counter which is decremented on the arrival of each clock. When it reaches 0, it is reloaded with the period and an output clock is generated. Resetting a divider reloads its counter without generating an output clock. Changing a divider's period doesn't affect its current count. A sequencer generates a series of values or events based on the repetition of a series of steps, starting with the first. When clocked the next step of the sequence is generated. In the block diagrams, the triangular symbol is a control gate; if control is non-zero, the input is passed unchanged to the output, otherwise the output is 0. control | v |\ in -->| >-- out |/ Except for the status register, all other registers are write-only. The "value of the register" refers to the last value written to the register. The NTSC NES has a master clock based on a 21.47727 MHz crystal which is divided by 12 to obtain a ~1.79 MHz clock source. Both clocks are used by the APU. The CPU's IRQ line is level-sensitive, so the APU's interrupt flags must be cleared once a CPU IRQ is acknowledged, otherwise the CPU will immediately be interrupt again once its inhibit flag is cleared. In general, the APU is a collection of many independent units which are always running in parallel. Modification of a channel's parameter usually affects only one sub-unit and doesn't take effect until that unit's next internal cycle begins. Each section begins with an overview and an optional block diagram, which provide a framework for the information that follows. In order to reduce ambiguity, there is very little re-statement of information. --------------- Frame Sequencer --------------- The frame sequencer contains a divider and a sequencer which clocks various units. The divider generates an output clock rate of just under 240 Hz, and appears to be derived by dividing the 21.47727 MHz system clock by 89490. The sequencer is clocked by the divider's output. On a write to $4017, the divider and sequencer are reset, then the sequencer is configured. Two sequences are available, and frame IRQ generation can be disabled. mi-- ---- mode, IRQ disable If the mode flag is clear, the 4-step sequence is selected, otherwise the 5-step sequence is selected and the sequencer is immediately clocked once. f = set interrupt flag l = clock length counters and sweep units e = clock envelopes and triangle's linear counter mode 0: 4-step effective rate (approx) --------------------------------------- - - - f 60 Hz - l - l 120 Hz e e e e 240 Hz mode 1: 5-step effective rate (approx) --------------------------------------- - - - - - (interrupt flag never set) l - l - - 96 Hz e e e e - 192 Hz At any time if the interrupt flag is set and the IRQ disable is clear, the CPU's IRQ line is asserted. -------------- Length Counter -------------- A length counter allows automatic duration control. Counting can be halted and the counter can be disabled by clearing the appropriate bit in the status register, which immediately sets the counter to 0 and keeps it there. The halt flag is in the channel's first register. For the square and noise channels, it is bit 5, and for the triangle, bit 7: --h- ---- halt (noise and square channels) h--- ---- halt (triangle channel) Note that the bit position for the halt flag is also mapped to another flag in the Length Counter (noise and square) or Linear Counter (triangle). Unless disabled, a write the channel's fourth register immediately reloads the counter with the value from a lookup table, based on the index formed by the upper 5 bits: iiii i--- length index bits bit 3 7-4 0 1 ------- 0 $0A $FE 1 $14 $02 2 $28 $04 3 $50 $06 4 $A0 $08 5 $3C $0A 6 $0E $0C 7 $1A $0E 8 $0C $10 9 $18 $12 A $30 $14 B $60 $16 C $C0 $18 D $48 $1A E $10 $1C F $20 $1E See the clarifications section for a possible explanation for the values left column of the table. When clocked by the frame sequencer, if the halt flag is clear and the counter is non-zero, it is decremented. --------------- Status Register --------------- The status register at $4015 allows control and query of the channels' length counters, and query of the DMC and frame interrupts. It is the only register which can also be read. When $4015 is read, the status of the channels' length counters and bytes remaining in the current DMC sample, and interrupt flags are returned. Afterwards the Frame Sequencer's frame interrupt flag is cleared. if-d nt21 IRQ from DMC frame interrupt DMC sample bytes remaining > 0 triangle length counter > 0 square 2 length counter > 0 square 1 length counter > 0 When $4015 is written to, the channels' length counter enable flags are set, the DMC is possibly started or stopped, and the DMC's IRQ occurred flag is cleared. ---d nt21 DMC, noise, triangle, square 2, square 1 If d is set and the DMC's DMA reader has no more sample bytes to fetch, the DMC sample is restarted. If d is clear then the DMA reader's sample bytes remaining is set to 0. ------------------ Envelope Generator ------------------ An envelope generator can generate a constant volume or a saw envelope with optional looping. It contains a divider and a counter. A channel's first register controls the envelope: --ld nnnn loop, disable, n Note that the bit position for the loop flag is also mapped to a flag in the Length Counter. The divider's period is set to n + 1. When clocked by the frame sequencer, one of two actions occurs: if there was a write to the fourth channel register since the last clock, the counter is set to 15 and the divider is reset, otherwise the divider is clocked. When the divider outputs a clock, one of two actions occurs: if loop is set and counter is zero, it is set to 15, otherwise if counter is non-zero, it is decremented. When disable is set, the channel's volume is n, otherwise it is the value in the counter. Unless overridden by some other condition, the channel's DAC receives the channel's volume value. ----- Timer ----- All channels use a timer which is a divider driven by the ~1.79 MHz clock. The noise channel and DMC use lookup tables to set the timer's period. For the square and triangle channels, the third and fourth registers form an 11-bit value and the divider's period is set to this value *plus one*. llll llll low 8 bits of period (third register) ---- -hhh upper 3 bits of period (fourth register) ---------- Sweep Unit ---------- The sweep unit can adjust a square channel's period periodically. It contains a divider and a shifter. A channel's second register configures the sweep unit: eppp nsss enable, period, negate, shift The divider's period is set to p + 1. The shifter continuously calculates a result based on the channel's period. The channel's period (from the third and fourth registers) is first shifted right by s bits. If negate is set, the shifted value's bits are inverted, and on the second square channel, the inverted value is incremented by 1. The resulting value is added with the channel's current period, yielding the final result. When the sweep unit is clocked, the divider is *first* clocked and then if there was a write to the sweep register since the last sweep clock, the divider is reset. When the channel's period is less than 8 or the result of the shifter is greater than $7FF, the channel's DAC receives 0 and the sweep unit doesn't change the channel's period. Otherwise, if the sweep unit is enabled and the shift count is greater than 0, when the divider outputs a clock, the channel's period in the third and fourth registers are updated with the result of the shifter. -------------- Square Channel -------------- +---------+ +---------+ | Sweep |--->|Timer / 2| +---------+ +---------+ | | | v | +---------+ +---------+ | |Sequencer| | Length | | +---------+ +---------+ | | | v v v +---------+ |\ |\ |\ +---------+ |Envelope |------->| >----------->| >----------->| >-------->| DAC | +---------+ |/ |/ |/ +---------+ There are two square channels beginning at registers $4000 and $4004. Each contains the following: Envelope Generator, Sweep Unit, Timer with divide-by-two on the output, 8-step sequencer, Length Counter. $4000/$4004: duty, envelope $4001/$4005: sweep unit $4002/$4006: period low $4003/$4007: reload length counter, period high In addition to the envelope, the first register controls the duty cycle of the square wave, without resetting the position of the sequencer: dd-- ---- duty cycle select d waveform sequence --------------------- _ 1 0 - ------ 0 (12.5%) __ 1 1 - ----- 0 (25%) ____ 1 2 - --- 0 (50%) _ _____ 1 3 -- 0 (25% negated) When the fourth register is written to, the sequencer is restarted. The sequencer is clocked by the divided timer output. When the sequencer output is low, the DAC receives 0. -------------- Linear Counter -------------- The Linear Counter serves as a second more-accurate duration counter for the triangle channel. It contains a counter and an internal halt flag. Register $4008 contains a control flag and reload value: crrr rrrr control flag, reload value Note that the bit position for the control flag is also mapped to a flag in the Length Counter. When register $400B is written to, the halt flag is set. When clocked by the frame sequencer, the following actions occur in order: 1) If halt flag is set, set counter to reload value, otherwise if counter is non-zero, decrement it. 2) If control flag is clear, clear halt flag. ---------------- Triangle Channel ---------------- +---------+ +---------+ |LinearCtr| | Length | +---------+ +---------+ | | v v +---------+ |\ |\ +---------+ +---------+ | Timer |------->| >----------->| >------->|Sequencer|--->| DAC | +---------+ |/ |/ +---------+ +---------+ The triangle channel contains the following: Timer, 32-step sequencer, Length Counter, Linear Counter, 4-bit DAC. $4008: length counter disable, linear counter $400A: period low $400B: length counter reload, period high When the timer generates a clock and the Length Counter and Linear Counter both have a non-zero count, the sequencer is clocked. The sequencer feeds the following repeating 32-step sequence to the DAC: F E D C B A 9 8 7 6 5 4 3 2 1 0 0 1 2 3 4 5 6 7 8 9 A B C D E F At the lowest two periods ($400B = 0 and $400A = 0 or 1), the resulting frequency is so high that the DAC effectively outputs a value half way between 7 and 8. ------------- Noise Channel ------------- +---------+ +---------+ +---------+ | Timer |--->| Random | | Length | +---------+ +---------+ +---------+ | | v v +---------+ |\ |\ +---------+ |Envelope |------->| >----------->| >------->| DAC | +---------+ |/ |/ +---------+ The noise channel starts at register $400C and contains the following: Length Counter, Envelope Generator, Timer, 15-bit right shift register with feedback, 4-bit DAC. $400C: envelope $400E: mode, period $400F: reload length counter Register $400E sets the random generator mode and timer period based on a 4-bit index into a period table: m--- iiii mode, period index i timer period ---------------- 0 $004 1 $008 2 $010 3 $020 4 $040 5 $060 6 $080 7 $0A0 8 $0CA 9 $0FE A $17C B $1FC C $2FA D $3F8 E $7F2 F $FE4 The shift register is clocked by the timer and the vacated bit 14 is filled with the exclusive-OR of *pre-shifted* bits 0 and 1 (mode = 0) or bits 0 and 6 (mode = 1), resulting in 32767-bit and 93-bit sequences, respectively. When bit 0 of the shift register is set, the DAC receives 0. On power-up, the shift register is loaded with the value 1. ------------------------------ Delta Modulation Channel (DMC) ------------------------------ +----------+ +---------+ |DMA Reader| | Timer | +----------+ +---------+ | | | v +----------+ +---------+ +---------+ +---------+ | Buffer |----| Output |---->| Counter |---->| DAC | +----------+ +---------+ +---------+ +---------+ The DMC can output samples composed of 1-bit deltas and its DAC can be directly changed. It contains the following: DMA reader, interrupt flag, sample buffer, Timer, output unit, 7-bit counter tied to 7-bit DAC. $4010: mode, frequency $4011: DAC $4012: address $4013: length On power-up, the DAC counter contains 0. Register $4010 sets the interrupt enable, loop, and timer period. If the new interrupt enabled status is clear, the interrupt flag is cleared. il-- ffff interrupt enabled, loop, frequency index f period ---------- 0 $1AC 1 $17C 2 $154 3 $140 4 $11E 5 $0FE 6 $0E2 7 $0D6 8 $0BE 9 $0A0 A $08E B $080 C $06A D $054 E $048 F $036 A write to $4011 sets the counter and DAC to a new value: -ddd dddd new DAC value Sample Buffer ------------- The sample buffer either holds a single sample byte or is empty. It is filled by the DMA reader and can only be emptied by the output unit, so once loaded with a sample it will be eventually output. DMA Reader ---------- The DMA reader fills the sample buffer with successive bytes from the current sample, whenever it becomes empty. It has an address counter and a bytes remain counter. When the DMC sample is restarted, the address counter is set to register $4012 * $40 + $C000 and the bytes counter is set to register $4013 * $10 + 1. When the sample buffer is in an empty state and the bytes counter is non-zero, the following occur: The sample buffer is filled with the next sample byte read from memory at the current address, subject to whatever mapping hardware is present (the same as CPU memory accesses). The address is incremented; if it exceeds $FFFF, it is wrapped around to $8000. The bytes counter is decremented; if it becomes zero and the loop flag is set, the sample is restarted (see above), otherwise if the bytes counter becomes zero and the interrupt enabled flag is set, the interrupt flag is set. When the DMA reader accesses a byte of memory, the CPU is suspended for 4 clock cycles. Output Unit ----------- The output unit continually outputs complete sample bytes or silences of equal duration. It contains an 8-bit right shift register, a counter, and a silence flag. When an output cycle is started, the counter is loaded with 8 and if the sample buffer is empty, the silence flag is set, otherwise the silence flag is cleared and the sample buffer is emptied into the shift register. On the arrival of a clock from the timer, the following actions occur in order: 1. If the silence flag is clear, bit 0 of the shift register is applied to the DAC counter: If bit 0 is clear and the counter is greater than 1, the counter is decremented by 2, otherwise if bit 0 is set and the counter is less than 126, the counter is incremented by 2. 1) The shift register is clocked. 2) The counter is decremented. If it becomes zero, a new cycle is started. ---------- DAC Output ---------- The DACs for each channel are implemented in a way that causes non-linearity and interaction between channels, so calculation of the resulting amplitude is somewhat involved. The normalized audio output level is the sum of two groups of channels: output = square_out + tnd_out 95.88 square_out = ----------------------- 8128 ----------------- + 100 square1 + square2 159.79 tnd_out = ------------------------------ 1 ------------------------ + 100 triangle noise dmc -------- + ----- + ----- 8227 12241 22638 where triangle, noise, dmc, square1 and square2 are the values fed to their DACs. The dmc ranges from 0 to 127 and the others range from 0 to 15. When the sub-denominator of a group is zero, its output is 0. The output ranges from 0.0 to 1.0. Implementation Using Lookup Table --------------------------------- The formulas can be efficiently implemented using two lookup tables: a 31-entry table for the two square channels and a 203-entry table for the remaining channels (due to the approximation of tnd_out, the numerators are adjusted slightly to preserve the normalized output range). square_table [n] = 95.52 / (8128.0 / n + 100) square_out = square_table [square1 + square2] The latter table is approximated (within 4%) by using a base unit close to the DMC's DAC. tnd_table [n] = 163.67 / (24329.0 / n + 100) tnd_out = tnd_table [3 * triangle + 2 * noise + dmc] Linear Approximation -------------------- A linear approximation can also be used, which results in slightly louder DMC samples, but otherwise fairly accurate operation since the wave channels use a small portion of the transfer curve. The overall volume will be reduced due to the headroom required by the DMC approximation. square_out = 0.00752 * (square1 + square2) tnd_out = 0.00851 * triangle + 0.00494 * noise + 0.00335 * dmc This linear approximation neglects the attenuating effect the DMC has when its DAC is in the upper level. This factor can be calculated using the main formula to form a ratio, and precalculated into a 128-entry lookup table. tnd_out(triangle=15,dmc=d) - tnd_out(triangle=0,dmc=d) attenuation(d) = ------------------------------------------------------ tnd_out(triangle=15,dmc=0) ------------------- Unreliable Behavior ------------------- (The following behaviors probably don't need to be emulated due to their unreliability since stable code will avoid invoking it, and since their behavior is somewhat difficult to precisely predict.) If the frame IRQ is set just as register $4015 is being read, it seems to be ignored (similar to polling $2002 for the vbl flag). The DMC's DMA reader seems to check for an empty buffer every few CPU cycles, rather than every cycle or continuously. Writing to the DAC register ($4011) while a sample is playing sometimes has no effect, probably because the DMC's output unit is clocking the counter at the same moment as the write. -------------- Clarifications -------------- (The following are meant only as re-statements of the main content, rather than additions of new content.) Because the envelope loop and length counter disable flags are mapped to the same bit, the length counter can't be used while the envelope is in loop mode. Similar applies to the triangle channel, where the linear counter and length counter are both controlled by the same bit in register $4008. Unlike the other waveform channels, the triangle channel is silenced by stopping its waveform at whatever phase it's at, rather than causing zero to be sent to its DAC. The length counter table seems to be set up for standard note durations for 4/4 time at 160 bpm and 180 bpm. If bit 3 is 0, the following results (Dn is bit n of the fourth channel register): 180bpm 160bpm D6-D4 D7=0 D7=1 note ------------------------------- $00 10 12 16th $01 20 24 8th $02 40 48 4th (one beat) $03 80 96 half $04 160 192 whole $05 60 72 4th dotted $06 14 16 8th triplet (*3 = a 4th) $07 26 32 4th triplet (*3 = a half) ------------- Collaborators ------------- Brad Taylor's NESSOUND.TXT and DMC.TXT as a starting point for testing. NTSC NES for testing on. Nesdev forum for feedback. xodnizel for testing results, correction to DMC table, feedback. Bloopaws/Draci for feedback, possible explanation of length counter table values. ------- History ------- 2003.12.01 Made development cartridge and started testing on NES hardware. 2003.12.14 Started project. 2003.12.20 A few draft sections were posted to Nesdev or e-mailed privately. 2004.01.02 Draft version posted to Nesdev. Corrected tnd_table formula. 2004.01.02 Corrected incorrect "correction" to tnd_table formula. Double-checked them. 2004.01.03 Corrected envelope flag name to "disable" (it was named "enable"). Added effective frequencies of frame sequencer outputs. Added Overview, Unreliable Behavior, Clarifications, and Collaborators sections. 2004.01.04 Adjusted linear approximation (difficult to find a compromise). A few minor edits. 2004.01.30 First release. Probably won't be doing much with it for a while. ================================================ FILE: docs/apu/audio_psuedo_code.txt ================================================ LENGTH_TABLE // used by $4003 WRITE Pulse1/2 APU sequencer_mode // $4017 WRITE D7, clock_frame_sequencer() sequencer_phase // $4017 WRITE 0, clock_frame_sequencer() sequencer_counter // $4017 WRITE clocks_to_next_phase(), clock_frame_sequencer() irq_pending // $4017 WRITE if !irq_enabled false, clock_frame_sequencer() // $4015 READ irq_pending = false irq_enabled // $4017 WRITE !D6, clock_frame_sequencer() clock() // Clocked every CPU cycle clock_frame_sequencer() clocks_to_next_phase() // used in $4017 WRITE clock_quarter_frame() // used in $4017 WRITE if sequencer_mode clock_half_frame() // used in $4017 WRITE if sequencer_mode sample() // Pulse1/2 DUTY_TABLE // used by duty_cycle/duty_counter enabled // $4015 WRITE D0 duty_cycle // $4000 WRITE D7..D6, clock(), output() duty_counter // $4003 WRITE 0, output() freq_timer // $4002 WRITE D7..D0, $4003 D2..D0 << 8, clock_half_frame() // sweep_forced_silent() freq_counter // $4003 WRITE freq_timer, clock() length_enabled // $4000 WRITE !D5, clock_half_frame() length_counter // $4003 WRITE if enabled LENGTH_TABLE[ D7..D3 ], output() // $4015 WRITE if !enabled 0, clock_half_frame() // $4015 READ decay_enabled // $4000 WRITE !D4, output() decay_loop // $4000 WRITE D5, clock_quarter_frame() decay_reset // $4003 WRITE true, clock_quarter_frame() decay_volume // $4000 WRITE D3..D0, output(), clock_quarter_frame() decay_constant_volume // output(), clock_quarter_frame() decay_counter // clock_quarter_frame() sweep_enabled // $4001 WRITE D7 && sweep_shift != 0, clock_half_frame() sweep_reload // $4001 WRITE true, clock_half_frame() sweep_timer // $4001 WRITE D6..D4, clock_half_frame() sweep_counter // clock_half_frame() sweep_negate // $4001 WRITE D3, clock_half_frame(), sweep_forced_silent() sweep_shift // $4001 WRITE D2..D0, clock_half_frame(), sweep_forced_silent() clock() // Clocked every APU cycle (CPU Cycle % 2 == 0) clock_quarter_frame() // clock_half_frame() // output() // sweep_forced_silent() // output(), clock_half_frame() Triangle enabled // ultrasonic // step // freq_timer // freq_counter // length_enabled // length_counter // linear_control // linear_load // linear_reload // clock() // clock_quarter_frame() // clock_half_frame() // output() // Noise FREQ_TABLE // enabled // freq_timer // freq_counter // shift // u16: default to 1 shift_mode // length_counter // decay_enabled // decay_reset // decay_loop // decay_volume // decay_constant_volume // decay_counter // clock() // clock_quarter_frame() // clock_half_frame() // output() // DMC addr // addr_load // length // length_load // irq_pending // loops // sample_buffer // output // output_bits // output_shift // output_silent // freq_timer // freq_counter // clock() // output() // ======================================================== $4000 write: duty_table = dutytables[ v.76 ] // duty_cycle length_enabled = !v.5 // length_counter.enabled // envelope decay_loop = v.5 // looping decay_enabled = !v.4 // enabled decay_V = v.3210 // volume ======================================================== $4001 write: sweep_timer = v.654 // divider_period sweep_negate = v.3 sweep_shift = v.210 sweep_reload = true sweep_enabled = v.7 && sweep_shift != 0 ======================================================== $4002 write: freq_timer = v (low 8 bits) // timer_period ======================================================== $4003 write: freq_timer = v.210 (high 3 bits) if( channel_enabled ) length_counter = lengthtable[ v.76543 ] ; phase is also reset here (important for games like SMB) freq_counter = freq_timer // timer duty_counter = 0 // sequencer_step ; decay is also flagged for reset here decay_reset_flag = true // envelope start ======================================================== $4015 write: channel_enabled = v.0 if( !channel_enabled ) length_counter = 0 ; ... other channels and DMC here ... ======================================================== $4017 write: sequencer_mode = v.7 ; switch between 5-step (1) and 4-step (0) mode irq_enabled = !v.6 next_seq_phase = 0 sequencer_counter = ClocksToNextSequence() ; see: http://wiki.nesdev.com/w/index.php/APU_Frame_Counter ; for example, this will be 3728.5 APU cycles, or 7457 CPU cycles. ; It might be easier to work in CPU cycles so you don't have to deal with ; half cycles. if(sequencer_mode) { Clock_QuarterFrame() ; see below Clock_HalfFrame() } if(!irq_enabled) irq_pending = false ; acknowledge Frame IRQ ======================================================== $4015 read: output = 0 if( length_counter != 0 ) output |= 0x01 ; ... other channels length counters here if( irq_pending ) output |= 0x40 ; ... DMC IRQ state read back here irq_pending = false ; IRQ acknowledged on $4015 read return output ======================================================== Every APU Cycle: ;;;;;;;;;;;;;;;;;;;;;;;;;;; ; clock pulse wave if( freq_counter > 0 ) // timer --freq_counter else { freq_counter = freq_timer duty_counter = (duty_counter + 1) & 7 } ; ... clock other channels here ;;;;;;;;;;;;;;;;;;;;;;;;;;; ; clock frame sequencer if( sequencer_counter > 0 ) --sequencer_counter else { ; see http://wiki.nesdev.com/w/index.php/APU_Frame_Counter for more details on here ; I'm just giving the basic idea here to conceptualize it if( next_seq_phase causes a Quarter Frame Clock ) Clock_QuarterFrame(); if( next_seq_phase causes a Half Frame Clock ) Clock_HalfFrame(); if( irq_enabled && next_seq_phase causes an IRQ ) irq_pending = true ; raise IRQ ++next_seq_phase if( next_seq_phase > max phases for this mode ) next_seq_phase = 0 sequencer_counter = ClocksToNextSequence() } ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; determine audio output if( duty_table[ duty_counter ] ; current duty phase is high && length_counter != 0 ; length counter is nonzero (channel active) && !IsSweepForcingSilence() ; sweep unit is not forcing channel to be silent ) { ; output current volume if(decay_enabled) output = decay_hidden_vol else output = decay_V } else ; low duty, or channel is silent output = 0 ; ... mix other channels with output here ======================================================== Clock_QuarterFrame: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; quarter frame clocks Decay if( decay_reset_flag ) { decay_reset_flag = false decay_hidden_vol = 0xF decay_counter = decay_V } else { if( decay_counter > 0 ) --decay_counter else { decay_counter = decay_V if( decay_hidden_vol > 0 ) --decay_hidden_vol else if( decay_loop ) decay_hidden_vol = 0xF } } ======================================================== Clock_HalfFrame: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; half frame clocks Sweep if( sweep_reload ) { sweep_counter = sweep_timer ; note there's an edge case here -- see http://wiki.nesdev.com/w/index.php/APU_Sweep ; for details. You can probably ignore it for now sweep_reload = false } else if( sweep_counter > 0 ) --sweep_counter else { sweep_counter = sweep_timer if( sweep_enabled && !IsSweepForcingSilence() ) { if(sweep_negate) freq_timer -= (freq_timer >> sweep_shift) + 1 ; note: +1 for Pulse1 only. Pulse2 has no +1 else freq_timer += (freq_timer >> sweep_shift) } } ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; half frame also clocks length if( length_enabled && length_counter > 0 ) --length_counter ======================================================== IsSweepForcingSilence: if( freq_timer < 8 ) return true else if( !sweep_negate && freq_timer + (freq_timer >> sweep_shift) >= 0x800 ) return true else return false Pulse 2: Identical to Pulse 1 with the following changes: - use $4004-4007 instead of $4000-4003 - $4015 reads/writes use bit 1 instead of bit 0 - no '+ 1' when doing sweep negate Triangle: A few things to note about the triangle: - It's clocked at twice the rate of other channels (use CPU clock instead of APU clock) - To silence it, you stop clocking the tri-step unit, but do not change its output. This is in contrast to other channels where you silence them by forcing output to zero. - There is no volume control, but Tri might appear quieter sometimes due to interference from the DMC. See http://wiki.nesdev.com/w/index.php/APU_Mixer for details - When the freq timer is < 2, it goes "ultrasonic" and is effectively silenced by forcing output to "7.5" (this causes a pop). $4015 read / write: Same as Pulse1, only use bit 2 instead of bit 0 Note 4015 touches length counter only, it does not do anything with linear counter ======================================================== $4008 write: linear_control = v.7 length_enabled = !v.7 linear_load = v.6543210 ======================================================== $400A write: freq_timer = v (low 8 bits) ======================================================== $400B write: freq_timer = v.210 (high 3 bits) if( channel_enabled ) length_counter = lengthtable[ v.76543 ] linear_reload = true ======================================================== Every **CPU** Cycle: ; Note the Triangle is clocked at twice the rate of other channels! ; It is clocked by CPU cycle and not by APU cycle! ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; clock tri wave ultrasonic = false if( freq_timer < 2 && freq_counter == 0 ) ultrasonic = true clock_triunit = true if( length_counter == 0 ) clock_triunit = false if( linear_counter == 0 ) clock_triunit = false if( ultrasonic ) clock_triunit = false if( clock_triunit ) { if( freq_counter > 0 ) --freq_counter else { freq_counter = freq_timer tri_step = (tri_step + 1) & 0x1F ; tri-step bound to 00..1F range } } ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; determine audio output ; the xor here creates the 'triangle' shape if( ultrasonic ) output = 7.5 else if( tri_step & 0x10 ) output = tri_step ^ 0x1F else output = tri_step ======================================================== Clock_QuarterFrame: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; quarter frame clocks Linear if( linear_reload ) linear_counter = linear_load else if( linear_counter > 0 ) --linear_counter if( !linear_control ) linear_reload = false ======================================================== Clock_HalfFrame: ; clock Length counter, same as Pulse Noise: Notes: - noise_shift must never be zero or the noise channel will never produce any output. Initialize it with 1 at bootup / hard reset. - with below implementation, noise_shift must not be signed-16 bit (unsigned is OK, or something larget than 16 bit is OK). If signed, the right-shift will feed in unwanted 1s. $4015 read / write: Same as Pulse1, only use bit 3 instead of bit 0 ======================================================== $400C write: ; same as $4000, only ignore bits 6 and 7 because noise has no duty ======================================================== $400E write: freq_timer = noise_freq_table[ v.3210 ] ; see http://wiki.nesdev.com/w/index.php/APU_Noise for freq table shift_mode = v.7 ======================================================== $400F write: if( channel_enabled ) length_counter = lengthtable[ v.76543 ] decay_reset_flag = true ======================================================== Every APU Cycle: ;;;;;;;;;;;;;;;;;;;;;;;;;;; ; clock noise shift if( freq_counter > 0 ) --freq_counter else { freq_counter = freq_timer ; note, set bit fifteen here, not bits 1 and 5 if( shift_mode ) noise_shift.15 = noise_shift.6 ^ noise_shift.0 else noise_shift.15 = noise_shift.1 ^ noise_shift.0 noise_shift >>= 1 } ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; determine audio output if( noise_shift.0 == 0 ; current noise output is low (output vol when low -- opposite of pulse) && length_counter != 0 ; length counter is nonzero (channel active) ) { ; output current volume if(decay_enabled) output = decay_hidden_vol else output = decay_V } else ; high shift output, or channel is silent output = 0 ======================================================== Clock_QuarterFrame: ; clock Decay, same as Pulse ======================================================== Clock_HalfFrame: ; clock Length counter, same as Pulse DMC $4010 write: dmcirq_enabled = v.7 dmc_loop = v.6 freq_timer = dmc_freq_table[ v.3210 ] ; see http://wiki.nesdev.com/w/index.php/APU_DMC for freq table if( !dmcirq_enabled ) dmcirq_pending = false ; acknowledge IRQ if disabled ======================================================== $4011 write: output = v.6543210 ; note there is some edge case weirdness here, see wiki for details ======================================================== $4012 write: addrload = $C000 | v<<6 ======================================================== $4013 write: lengthload = (v<<4) + 1 ======================================================== $4015 write: if( v.4 ) { if( length == 0 ) { length = lengthload addr = addrload } } else length = 0 dmcirq_pending = false ; acknowledge DMC IRQ on write ======================================================== $4015 read: v.4 = (length > 0) v.7 = dmcirq_pending ; ... other channels and frame IRQ set other bits ======================================================== Every ?CPU? cycle???? ( not sure if DMC runs on APU cycles or CPU cycles. It doesn't really matter because all the frequencies are even. The wiki lists freqs in CPU cycles, so.... *shrug* ) ;;;;;;;;;;;;;;;;;;;;;;;; ; Clock DMC unit if( freq_counter > 0 ) --freq_counter else { freq_counter = freq_timer if( !output_unit_silent ) { if( (output_shift & 1) && output < $7E ) output += 2 if(!(output_shift & 1) && output > $01 ) output -= 2 } --bits_in_output_unit output_shift >>= 1 if( bits_in_output_unit == 0 ) { bits_in_output_unit = 8 output_shift = sample_buffer output_unit_silent = is_sample_buffer_empty is_sample_buffer_empty = true } } ;;;;;;;;;;;;;;;;;;;;;;;;;; ; Perform DMA if necessary if( length > 0 && is_sample_buffer_empty ) { sample_buffer = DMAReadFromCPU( addr ) ; note: this DMA halts the CPU for up to 4 cycles. ; See wiki for timing details. Note that all commercial games will work ; fine if you ignore these stolen cycles, but some tech ; demos and test ROMs will glitch/fail. So getting these stolen cycles ; correct is not super important unless you're putting a lot of emphasis ; on accuracy. is_sample_buffer_empty = false addr = (addr + 1) | $8000 ; <- wrap $FFFF to $8000 --length if(length == 0) { if( dmc_loop ) { length = lengthload addr = addrload } else if( dmcirq_enabled ) dmcirq_pending = true ; raise IRQ } } ;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Determine channel output ; output is always 'output' ... the 7 bit value written to $4011 and modified by the DMC unit Specifically there are 3 filters: 1 lowpass: out[i]=(in[i]-out[i-1])*0.815686 and 2 highpass: out[i]=out[i-1]*0.996039+in[i]-in[i-1] out[i]=out[i-1]*0.999835+in[i]-in[i-1] // 'sample' is your output sample as generated by your APU // 'output' is what you will actually output // // initialize all intermediate vars to 0.0 // low pass LP_In = sample LP_Out = (LP_In - LP_Out) * 0.815686 // high pass A HPA_Out = HPA_Out*0.996039 + LP_Out - HPA_Prev HPA_Prev = LP_Out // high pass B HPB_Out = HPB_Out*0.999835 + HPA_Out - HPB_Prev HPB_Prev = HPA_Out output = HPB_Out // scale output to be within min/max bounds Spit out a line of text at each of the following events: - 4008-400B writes - 4017 writes - linear clocks - Frame start On each line, include the contents of the Linear Counter so you can see how it's being updated. If you can see where your counting is going wrong, you can see where the problem in your code is. We wary of denormal numbers. Set very tiny floating point values to 0 to avoid performance issues. ================================================ FILE: docs/apu/blargg_tests_readme.txt ================================================ NES APU Frame Counter Update ---------------------------- I have run more tests on the NES APU and come up with new information about the exact timing of the frame counter and length counter, and some subtle behavior. The information here either extends or contradicts what is stated in the NES APU reference and on the nesdev wiki. Not documented here is a delay when changing modes by writing to $4017. This is quite complex and I haven't fully worked out its exact operation. Once determined, documented, and tested, the information here should still be valid. This delay when changing modes involves the current mode running a few clocks before switching to the new mode, so it only affects the rare case where $4017 is written within a few clocks of a frame counter step. This delay does not cause the steps to occur any later than shown below; it only causes the first few clocks of the new mode to be transparent, allowing the previous mode to "show through". Also not documented is the exact operation of the envelope, sweep, and triangle's linear counter when register writes occur close to clocking. Refer to tests.txt for a description of the test ROMs included. I have not yet fully updated my APU emulator and tested it with this information, so report any problems you have with implementation. Shay (swap to e-mail) Clock Jitter ------------ Changes to the mode by writing to $4017 only occur on *even* internal APU clocks; if written on an odd clock, the first step of the mode is delayed by one clock. At power-up and reset, the APU is randomly in an odd or even cycle with respect to the first clock of the first instruction executed by the CPU. ; assume even APU and CPU clocks occur together lda #$00 sta $4017 ; mode begins in one clock sta <0 ; delay 3 clocks sta $4017 ; mode begins immediately Mode 0 Timing ------------- -5 lda #$00 -3 sta $4017 0 (write occurs here) 1 2 3 ... Step 1 7459 Clock linear ... Step 2 14915 Clock linear & length ... Step 3 22373 Clock linear ... Step 4 29830 Set frame irq 29831 Clock linear & length and set frame irq 29832 Set frame irq ... Step 1 37289 Clock linear ... etc. Mode 1 Timing ------------- -5 lda #$80 -3 sta $4017 0 (write occurs here) Step 0 1 Clock linear & length 2 ... Step 1 7459 Clock linear ... Step 2 14915 Clock linear & length ... Step 3 22373 Clock linear ... Step 4 29829 (do nothing) ... Step 0 37283 Clock linear & length ... etc. Length Halt ----------- Write to halt flag is delayed by one clock: $10->$4000 clear halt flag 0 $00->$4017 begin mode 0 14914 $30->$4000 set halt flag 14915 Length not clocked $10->$4000 clear halt flag 0 $00->$4017 begin mode 0 14915 $30->$4000 set halt flag Length clocked $30->$4000 set halt flag 0 $00->$4017 begin mode 0 14914 $10->$4000 clear halt flag 14915 Length clocked $30->$4000 set halt flag 0 $00->$4017 begin mode 0 14915 $10->$4000 clear halt flag Length not clocked Length Reload ------------- Length reload is completely ignored if written during length clocking and length counter is non-zero before clocking: $38->$4003 make length non-zero 0 $00->$4017 14914 Write to $4003 Length reloaded 14915 Length clocked $38->$4003 make length non-zero 0 $00->$4017 14915 Write to $4003 Length not reloaded Length clocked $00->$4015 clear length counter $01->$4015 0 $00->$4017 14915 Write to $4003 Length reloaded Length not clocked Misc ---- - The frame IRQ flag is cleared only when $4015 is read or $4017 is written with bit 6 set ($40 or $c0). - The IRQ handler is invoked at minimum 29833 clocks after writing $00 to $4017 (assuming the frame IRQ flag isn't already set, and nothing else generates an IRQ during that time). - After reset or power-up, APU acts as if $4017 were written with $00 from 9 to 12 clocks before first instruction begins. It is as if this occurs (this generates a 10 clock delay): lda #$00 sta $4017 ; 1 lda <0 ; 9 delay nop nop nop reset: ... - As shown, the frame irq flag is set three times in a row. Thus when polling it, always read $4015 an extra time after the flag is found to be set, to be sure it's clear afterwards, wait: bit $4015 ; V flag reflects frame IRQ flag bvc wait bit $4015 ; be sure irq flag is clear or better yet, clear it before polling it: bit $4015 ; clear flag first wait: bit $4015 ; V flag reflects frame IRQ flag bvc wait ================================================ FILE: docs/apu/mixer_readme.txt ================================================ NES APU Mixer Tests ------------------- These tests verify proper operation of the NES APU's sound channel mixer, including relative volumes of channels and non-linear mixing. Tests MUST be run from a freshly-powered NES, as this is the only way to ensure that the triangle wave doesn't interfere. All tests beep, play a test sound, then beep again. For all but the noise test, there should be near silence between the beeps. For the noise test, noise will fade in and out. There shouldn't be any noticeable tone when heard through a speaker (through headphones, faint tones might be audible). Internal operation ------------------ The tests have the channel under test generate a tone, then generate the inverse waveform using the DMC DAC, canceling to (near) silence if everything is correct. The DMC test verifies that non-linearity of the DMC DAC. The noise and triangle tests verify relative volume of the noise and triangle to the DMC, and that the DMC DAC affects attenuation of them properly. Finally, the square test verifies relative volume of the squares to the DMC, non-linearity of the square DACs, how one square affects the other (slightly), and that the square DAC non-linearity is separate from the DMC. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. -- Shay Green ================================================ FILE: docs/apu/test_readme.txt ================================================ NES APU Tests ------------- These ROMs test many aspects of the APU that are visible to the CPU. Really obsucre things are not tested here. 1-len_ctr --------- Tests length counter operation for the four main channels 2) Problem with length counter load or $4015 3) Problem with length table, timing, or $4015 4) Writing $80 to $4017 should clock length immediately 5) Writing 0 to $4017 shouldn't clock length immediately 6) Disabling via $4015 should clear length counter 7) When disabled via $4015, length shouldn't allow reloading 8) Halt bit should suspend length clocking 2-len_table ----------- Verifies all length table entries 3-irq_flag ---------- Verifies basic operation of frame irq flag 2) Flag shouldn't be set in $4017 mode $40 3) Flag shouldn't be set in $4017 mode $80 4) Flag should be set in $4017 mode $00 5) Reading flag should clear it 6) Writing $00 or $80 to $4017 shouldn't affect flag 7) Writing $40 or $C0 to $4017 should clear flag 4-jitter -------- Tests for APU clock jitter. Also tests basic timing of frame irq flag since it's needed to determine jitter. 3) Frame irq is set too late 4) Even jitter not handled properly 5) Odd jitter not handled properly 5-len_timing ------------ Verifies timing of length counter clocks in both modes 2) First length of mode 0 is too soon 3) First length of mode 0 is too late 4) Second length of mode 0 is too soon 5) Second length of mode 0 is too late 6) Third length of mode 0 is too soon 7) Third length of mode 0 is too late 8) First length of mode 1 is too soon 9) First length of mode 1 is too late 10) Second length of mode 1 is too soon 11) Second length of mode 1 is too late 12) Third length of mode 1 is too soon 13) Third length of mode 1 is too late 6-irq_flag_timing ----------------- Frame interrupt flag is set three times in a row 29831 clocks after writing $00 to $4017. 3) Flag first set too late 4) Flag last set too soon 5) Flag last set too late 7-dmc_basics ------------ Verifies basic DMC operation 2) DMC isn't working well enough to test further 3) Starting DMC should reload length from $4013 4) Writing $10 to $4015 should restart DMC if previous sample finished 5) Writing $10 to $4015 should not affect DMC if previous sample is still playing 6) Writing $00 to $4015 should stop current sample 7) Changing $4013 shouldn't affect current sample length 8) Shouldn't set DMC IRQ flag when flag is disabled 9) Should set IRQ flag when enabled and sample ends 10) Reading IRQ flag shouldn't clear it 11) Writing to $4015 should clear IRQ flag 12) Disabling IRQ flag should clear it 13) Looped sample shouldn't end until $00 is written to $4015 14) Looped sample shouldn't ever set IRQ flag 15) Clearing loop flag and then setting again shouldn't stop loop 16) Clearing loop flag should end sample once it reaches end 17) Looped sample should reload length from $4013 each time it reaches end 18) $4013=0 should give 1-byte sample 19) There should be a one-byte buffer that's filled immediately if empty 8-dmc_rates ----------- Verifies the DMC's 16 rates Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. -- Shay Green ================================================ FILE: docs/apu/volume_readme.txt ================================================ Volume tests for NES _____________________________________________________________________ Background The NES has four tone generator channels and one digital sample playback channel: 1. Rectangular pulse ("square") wave 2. Another square wave 3. 32-step triangle wave 4. Binary noise generated by a linear feedback shift register 5. Delta pulse code modulation; also allows writes of raw LPCM to the counter for use as a generic 7-bit DAC Unlike systems such as the GBA, which digitally add all channels before passing them to a DAC, the NES has a separate DAC for each channel and mixes them analog. Nonlinearities in this mixing can cause one channel to affect the volume of another channel. The NES uses an unsigned DAC, meaning that each channel can generate only positive signal values. Such a DAC generates a lot of DC, and the NES has a high-pass filter on its audio output to block the DC. Different emulators use different time constants on their DC filters, which human listeners generally can't perceive. So you can't just measure the maximum voltage; you have to measure the difference between the high and low values. Different emulators use different amounts of headroom in the 16-bit range, depending on what Famicom expansion audio chips are present. So you have to compare relative volumes, not absolute volumes. _____________________________________________________________________ The test pattern This program demonstrates the channel balance among implementations of the NES architecture. The pattern consists of a set of 12 tones, as close to 1000 Hz as the NES allows: 1. Channel 1, 1/8 duty 2. Channel 1, 1/4 duty 3. Channel 1, 1/2 duty 4. Channel 1, 3/4 duty 5. Channels 1 and 2, 1/8 duty 6. Channels 1 and 2, 1/4 duty 7. Channels 1 and 2, 1/2 duty 8. Channels 1 and 2, 3/4 duty 9. Channel 3 10. Channel 4, long LFSR period 11. Channel 4, short LFSR period 12. Channel 5, amplitude 30 When the user presses A on controller 1, the pattern plays three times, with channel 5 held steady at 0, 48, and 96. The high point of tone 12 each time is 30 units above the level for that time, that is, 30, 78, and 126 respectively. _____________________________________________________________________ Recordings The files in the 'recordings' folder are recordings of volumes.nes run in various environments. They were recorded at 44100 Hz and then encoded using OggDropXPd 1.90 (libvorbis 1.2.0) at -q 7.00. * nes-001.ogg: Nintendo Entertainment System (NTSC U/C) with PowerPak * nestopia.ogg: Nestopia 1.40 * nintendulator.ogg: Nintendulator snapshot 2009-02-28 * fceux.ogg: FCEUX 2.0.4-interim 2008-11-24 _____________________________________________________________________ Legal Copyright (c) 2009 Damian Yerrick The program and manual are under the following license: This work is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this work. Permission is granted to anyone to use this work for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this work must not be misrepresented; you must not claim that you wrote the original work. If you use this work in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original work. 3. This notice may not be removed or altered from any source distribution. The term "source" refers to the preferred form of a work for making changes to it. ================================================ FILE: docs/cartridge_board_list.txt ================================================ 10 Yard Fight Nintendo E NROM 110 in 1 (Canada?) Supervision 1942 Capcom C+ NROM 1943 Capcom C+ UNROM 6 in 1 Caltron A 720 Mindscape C SGROM 8 Eyes Taxan C- TLROM A Boy and His Blob Absolute C SLROM Abadox Milton Bradley C SLROM Action 52 Active Enterprises A 023-N507 Addams Family Ocean B SLROM Adv of Bayou Billy Konami C- SLROM Adv of Dino Riki Hudson C CNROM Adv of Lolo HAL C+ SEROM Adv of Lolo 2 HAL B- TEROM Adv of Lolo 3 HAL B+ SLROM Adv of Tom Saywer Seta B SLROM Adventure Island Hudson D CNROM Adventure Island 2 Hudson B- TLROM Adventure Island 3 Hudson B TLROM After Burner Tengen C+ 800042-01 Air Fortress HAL C SJROM Airwolf Acclaim C SHROM Al Unser Racing Data East C SKROM Aladdin Deck Enhancer Camerica A+ Alfred Chicken Mindscape B+ UNROM Alien 3 LJN B+ 55741 Alien Syndrome Tengen C+ All Pro Basketball Vic Tokai C SLROM Alpha Mission SNK C CNROM Amagon American Sammy C UNROM American Gladiators Gametek B- SLROM Anticipation Nintendo C SEROM Arch Rivals Acclaim C AMROM Archon Activision B UNROM Arkanoid Taito B+ CNROM Arkanoid Controller Taito A Arkistas Ring American Sammy B CNROM Astyanax Jaleco C TLROM Athena SNK C UNROM Athletic World Bandai B- CNROM Attack of the Killer Tomatoes T*HQ B- SLROM Baby Boomer Color Dreams B- Back to the Future LJN D CNROM Back to the Future 2 & 3 LJN C SLROM Bad Dudes Data East C TLROM Bad News Baseball Tecmo B SLROM Bad Street Brawler Mattel C SGROM Balloon Fight Nintendo C NROM Bandit Kings/China Koei B- ETROM Barbie Hi Tech B SLROM Bard's Tale FCI B- SNROM Base Wars Ultra C TKROM Baseball Nintendo E NROM Baseball Simulator Culture Brain C+ SKROM Baseball Stars SNK C+ SKROM Baseball Stars 2 Romstar B+ TKROM Bases Loaded Jaleco E SFROM Bases Loaded 2 Jaleco D SL3ROM Bases Loaded 3 Jaleco C TLROM Bases Loaded 4 Jaleco C+ TLROM Batman Sunsoft D NES_B4 Batman Return/Joker Sunsoft B- NES_BTR Batman Returns Konami B+ TLROM Battle Chess Data East B+ SKROM Battle of Olympus Broderbund C SGROM Battle Tank Absolute B CNROM Battle Toads Tradewest D AOROM Battle Toads & Double Dragon Tradewest B+ AOROM Battleship Mindscape B CNROM Bee 52 Camerica B+ Beetlejuice LJN C+ AMROM Best of the Best Electro Brain B+ UOROM Bible Adventures Wisdom Tree B+ Bible Adventures (blue label) Wisdom Tree B+ Bible Buffet Wisdom Tree B+ Big Bird Hide & Speak Hi Tech B SLROM Big Foot Acclaim B- SLROM Big Nose Freaks Out Camerica A Big Nose Freaks Out (Aladdin Cart) Camerica A+ Big Nose the Caveman Camerica B Bill & Ted's Excellent Adventure LJN C+ SLROM Bill Elliott's NASCAR Challenge Konami C+ TSROM Bionic Commando Capcom C SGROM Black Bass Hot B B UNROM Blackjack American Video A- Blades of Steel Konami D UNROM Blaster Master Sunsoft D SL2ROM Blue Marlin Hot B B+ TLROM Blues Brothers Titus B UNROM Bo Jackson Baseball Data East C+ TSROM Bomberman Hudson C NROM Bomberman 2 Hudson B SNROM Bonks Adventure Hudson B+ TLROM Boulder Dash JVC B+ SEROM Break Time FCI B+ SFROM Breakthru Data East C (SLROM) Bubble Bath Babes Panesian A+ Bubble Bobble Taito D SFROM Bubble Bobble 2 Taito B TLROM Bucky O'Hare Konami B TLROM Bugs Bunny Birthday Blowout Kemco B TSROM Bugs Bunny Crazy Castle Kemco B+ SBROM Bump & Jump Vic Tokai B- CNROM Burai Fighter Taxan B- TEROM Burger Time Data East B- NROM Cabal Milton Bradley C AMROM Caesars Palace Virgin B UNROM California Games Milton Bradley C UNROM Captain America Data East B TLROM Captain Comic Color Dreams B Captain Planet Mindscape B TLROM Captain Skyhawk Milton Bradley C AMROM Casino Kid Sofel C+ UNROM Casino Kid 2 Sofel B+ UNROM Castelian Triffix B UNROM Castle of Deceit Bunch Games B Castle of Dragon Seta B UNROM Castlequest Nexoft B CNROM Castlevania Konami E UNROM Castlevania 2 Konami D SLROM Castlevania 3 Konami C- ELROM Caveman Games Data East B- SLROM Challenge of the Dragon Color Dreams B Championship Bowling Romstar B CNROM Championship Pool Mindscape B+ UNROM Cheetahman II Active Enterprises A Chessmaster Hi Tech B SJROM Chiller American Game Carts Inc B+ Chubby Cherub Bandai B NROM Circus Caper Toho B SLROM City Connection Jaleco B CNROM Clash at Demonhead Vic Tokai B- SLROM Classic Concentration Gametek B- UNROM Cliffhanger Imagesoft B TLROM Clu Clu Land Nintendo B- NES_RROM Cobra Command Data East C SLROM Cobra Triangle Nintendo B- ANROM Codename Viper Capcom C TLROM Color A Dinosaur Virgin A- UNROM Commando Capcom C UNROM Conan Mindscape B UNROM Conflict Vic Tokai C+ SKROM Conquest Crystal Palace Asmik C+ TLROM Contra Konami E UNROM Contra Force Konami B TLROM Cool World Ocean B SLROM Cowboy Kid Romstar A- TLROM Crash & the Boys Street Challange American Technos B TLROM Crash Dummies LJN B 55741 Crystal Mines Color Dreams B+ Crystalis SNK C TKROM Cyberball Jaleco B TLROM Cybernoid Acclaim C CNROM Dance Aerobics Nintendo C+ SBROM Darkman Ocean B SLROM Darkwing Duck Capcom B+ SLROM Dash Galaxy Data East C CNROM Day Dreamin' Davey HAL B+ SLROM Days of Thunder Mindscape C+ TLROM Deadly Towers Broderbund C+ BNROM Death Race American Game Carts Inc B+ Deathbots American Video B+ Defender 2 HAL B NROM Defender of the Crown Ultra C+ SGROM Defenders of Dynacron City JVC B TLROM Deja Vu Kemco C+ TKROM Demon Sword Taito C+ SL1ROM Desert Commander Kemco C- SKROM Destination Earthstar Acclaim C CNROM Destiny/Emporer Capcom B- SNROM Dick Tracy Bandai C+ UNROM Die Hard Activision B+ SLROM Dig Dug 2 Bandai B NROM Digger Milton Bradley B+ AMROM Dirty Harry Mindscape C+ TLROM Disney Adventure Capcom B SLROM Dizzy the Adventurer (Aladdin Cart) Camerica A+ Donkey Kong Nintendo B- NROM Donkey Kong 3 Nintendo B- NROM Donkey Kong Classics Nintendo B- CNROM Donkey Kong Jr Nintendo B- NROM Donkey Kong Jr Math Nintendo A- NROM Double Dare Gametek B AOROM Double Dragon Tradewest E SLROM Double Dragon 2 Acclaim D TL1ROM Double Dragon 3 Acclaim C+ TLROM Double Dribble Konami D UNROM Double Strike American Video B+ Dr. Chaos FCI C UNROM Dr. Jeckyl/Mr. Hyde Bandai C+ SFROM (SFDOROM) Dr. Mario Nintendo D SEROM Dracula Imagesoft B TSROM Dragon Fighter Sofel B SLROM Dragon Power Bandai C GNROM Dragon Spirit Bandai B TLROM Dragon Strike FCI C TLROM Dragon Warrior Nintendo E SAROM Dragon Warrior 2 Enix C+ SNROM Dragon Warrior 3 Enix B+ SUROM Dragon Warrior 4 Enix B SUROM Dragon's Lair Imagesoft B UNROM Duck Hunt Nintendo F NROM Duck Tales Capcom C+ UNROM Duck Tales 2 Capcom B+ UNROM Dudes with Attitude American Video B+ Dungeon Magic Taito C SKROM Dusty Diamond All Star Softball Broderbund B- SLROM Dyno Warz Bandai C+ SLROM Elevator Action Taito B- NROM Eliminator Boat Duel Electro Brain C+ SLROM Empire Strikes Back JVC B TLROM Excitebike Nintendo E NROM Exodus Wisdom Tree B+ F 117 Stealth Microprose B+ TLROM F 15 City War American Video B F 15 Strike Eagle Microprose B+ TLROM F1 Built to Win Seta B+ SKROM Family Feud Gametek B+ SHROM Fantastic Adv Dizzy (Aladdin Cart) Camerica A+ Fantastic Adventures of Dizzy Camerica B- Fantasy Zone Tengen C Faria Nexoft B+ SKROM Fast Break Tradewest C SCROM Faxanadu Nintendo C- SGROM Felix the Cat Hudson B+ TSROM Ferrari Grand Prix Acclaim B SLROM Fester's Quest Sunsoft C SLROM Fighting Golf SNK C SLROM Final Fantasy Nintendo C+ SNROM Fire & Ice Tecmo A- TLROM Fire Hawk Camerica B Firehouse Rescue Gametek B CNROM Fist of the North Star Taxan C+ UNROM Flight of the Intruder Mindscape B UNROM Flintstones Taito C+ TLROM Flintstones 2 Taito A+ Flying Dragon Culture Brain C UNROM Flying Warriors Culture Brain C SLROM Frankenstein Bandai B+ SLRROM Freedom Force Sunsoft D SLROM Friday The 13th LJN C- CNROM Fun House Hi Tech B+ UNROM G I Joe Taxan B+ TLROM G I Joe Atlantis Factor Capcom C+ TLROM Galactic Crusader Bunch Games B Galaga Bandai B NROM Galaxy 5000 Activision A- TLROM Game Action Replay STD A- Game Genie Galoob D Gargoyle's Quest 2 Capcom B+ TLROM Gauntlet (licensed) Tengen C DRROM Gauntlet (unlicensed) Tengen C Gauntlet 2 Mindscape C TSROM Gemfire Koei B- EKROM Genghis Kahn Koei C+ SOROM George Forman Acclaim B 55741 Ghost & Goblins Capcom C UNROM Ghost Lion Kemco B SKROM Ghostbusters Activision B- CNROM Ghostbusters 2 Activision B SLROM Ghoul School Electro Brain B- SLROM Gilligans Island Bandai B+ UNROM Goal Jaleco D SL3ROM Goal 2 Jaleco B- TLSROM Godzilla Toho C+ SLROM Godzilla 2 Toho B SLROM Gold Metal Challenge Capcom B+ TKROM Golf Nintendo E NROM Golf Grand Slam Atlus B+ SLROM Golf Power Virgin B+ SNROM Golgo 13 Top Secret Episode Vic Tokai D SLROM Goonies 2 Konami C+ 351258 Gotcha LJN C CNROM Gradius Konami C- CNROM Great Waldo Search T*HQ B- SLROM Gremlins 2 Sunsoft B- TLROM Guardian Legend Broderbund C+ UNROM Guerrilla War SNK C SLROM Gum Shoe Nintendo C+ GNROM Gun Nac Ascii B TLROM Gunsmoke Capcom B- UNROM Gyromite Nintendo E NROM Gyruss Ultra C+ CNROM Harlem Globetrotters Gametek B SLROM Hatris Bullet Proof A SNROM Heavy Barrel Data East C TLROM Heavy Shreddin' Parker Brothers B- SLROM Heroes of the Lance FCI C SKROM High Speed Tradewest B- TQROM Hillsfar FCI B SNROM Hogan's Alley Nintendo D NROM Hollywood Squares Gametek B+ UNROM Home Alone T*HQ B- TSROM Home Alone 2 T*HQ B- TLROM Hook Imagesoft B SLROM Hoops Jaleco D SLROM Hot Slots Panesian A+ Hudson Hawk Imagesoft B SLROM Hunt for Red October Hi Tech C TLROM Hydlide FCI D NROM I Can Remember Gametek B CNROM Ice Climber Nintendo C+ NROM Ice Hockey Nintendo D NROM Ikari Warriors SNK C- UNROM Ikari Warriors 2 SNK C- SGROM Ikari Warriors 3 SNK B- SLROM Image Fight Irem B TSROM Immortal Electronic Arts B TLROM Impossible Mission 2 American Video A- Impossible Mission 2 SEI B+ Indiana Jones Last Crusade UBI Soft B+ UNROM Indiana Jones Last Crusade Taito B+ SGROM Indiana Jones Temple of Doom Tengen B- Indiana Jones Temple of Doom Mindscape C TFROM Indy Heat Tradewest B AMROM Infiltrator Mindscape B TLROM Iron Tank SNK C SLROM Isolated Warrior NTVIC B TLROM Jack Nicklaus Golf Konami B- 351258 Jackal Konami C UNROM Jackie Chan Kung Fu Hudson B TLROM James Bond Jr T*HQ B- TLROM Jaws LJN D CNROM Jeopardy Gametek C AOROM Jeopardy 25th Anniversary Gametek C+ ANROM Jeopardy Jr Gametek C ANROM Jeopardy Super Gametek B SLROM Jetsons Taito B+ TLROM Jimmy Connors Tennis UBI Soft B+ UNROM Joe & Mac Data East B+ TLROM Jordan vs. Bird Milton Bradley C UNROM Joshua Wisdom Tree B+ Journey to Silius Sunsoft C+ SLROM Joust HAL B CNROM Jungle Book Virgin A- TLROM Jurassic Park Ocean B TSROM Karate Champ Data East C CNROM Karate Kid LJN C CNROM Karnov Data East C DEIROM Kick Master Taito C+ TLROM Kickle Cubicle Irem B+ TLROM Kid Icarus Nintendo C- SNROM Kid Klown Kemco B TSROM Kid Kool Vic Tokai C+ UNROM Kid Niki Data East C SGROM King Neptune's Adventure Color Dreams B+ King of Kings Wisdom Tree B+ King of Kings (cartoon mule) Wisdom Tree B+ King of the Ring Acclaim B+ 55741 Kings Knight Square C+ CNROM Kings of the Beach Ultra C CNROM Kings Quest 5 Konami B+ TSROM Kirbys Adventure Nintendo B- TKROM Kiwi Kraze Taito B+ TLROM Klash Ball Sofel B- UNROM Klax Tengen B+ Knight Rider Acclaim B- SCIROM Krazy Kreatures American Video B+ Krion Conquest Vic Tokai B TLROM Krusty's Fun House Acclaim C+ TLROM Kung Fu Nintendo E NROM Kung Fu Heroes Culture Brain C CNROM L'Empereur Koei B ETROM Laser Invasion Konami B- ELROM Last Action Hero Imagesoft B+ TLROM Last Ninja Jaleco B- TLROM Last Starfighter Mindscape C+ CNROM Legacy/Wizard Broderbund C TFROM Legend of Kage Taito C- CNROM Legendary Wings Capcom B UNROM Legends/Diamond Bandai B+ TLROM Lemmings Sunsoft B- SLROM Lethal Weapon Ocean B SLROM Life Force Konami C 351258 Linus Spacehead Camerica B Linus Spacehead (Aladdin Cart) Camerica A+ Little League Baseball SNK B- SLROM Little Mermaid Capcom B UNROM Little Nemo Capcom B+ TLROM Little Ninja Brothers Culture Brain C TLROM Little Sampson Taito B TLROM Lode Runner Broderbund B NROM Lone Ranger Konami B- TLROM Loopz Mindscape B+ UNROM Low G Man Taxan C TLROM Lunar Pool FCI C+ NROM M C Kids Virgin C+ TSROM M.U.L.E. Mindscape C+ SNROM Mach Rider Nintendo C- NROM Mad Max Mindscape C+ TLROM Mafat Conspiracy Vic Tokai C TLROM Magic Darts Romstar B SLRROM Magic of Scheherazade Culture Brain C- SLROM Magician Taxan A- TKROM Magmax FCI C- NROM Major League Baseball LJN E CNROM Maniac Mansion Jaleco C+ SNROM Mappyland Taxan C+ TFROM Marble Madness Milton Bradley C+ ANROM Mario Brothers Nintendo B- NROM Mario Is Missing Mindscape B TLROM Mario Time Machine Mindscape A- TLROM Marvel's X-Men LJN B- UNROM Master Chu & the Drunkard Hu Color Dreams B Maxi 15 American Video A Mechanized Attack SNK C SCROM Mega Man Capcom B- UNROM Mega Man 2 Capcom C SGROM Mega Man 3 Capcom C TLROM Mega Man 4 Capcom B- TGROM Mega Man 5 Capcom B TLROM Mega Man 6 Nintendo B+ TGROM Menace Beach Color Dreams A- Mendel Palace Hudson B TLROM Mermaids of Atlantis American Video B+ Metal Fighter Color Dreams B Metal Gear Ultra D UNROM Metal Mech Jaleco B- SLROM Metal Storm Irem B+ TLROM Metroid Nintendo D SNROM Michael Andretti World GP American Sammy B- TLROM Mickey Mousecapade Capcom C+ CNROM Mickey Numbers Hi Tech B+ TLROM Mickey's Safari in Letterland Hi Tech B+ 55741 Micro Machines Camerica A- Micro Machines (Aladdin Cart) Camerica A+ Mig 29 Camerica C+ Might & Magic American Sammy B+ TKROM Mighty Bombjack Tecmo B CNROM Mighty Final Fight Capcom B+ TLROM Mike Tyson's Punch Out Nintendo D PNROM Millipede HAL B NROM Milon's Secret Castle Hudson C CNROM Miracle Piano Mindscape A- SJROM Mission Cobra Bunch Games A- Mission Impossible Ultra C+ 352026 Monopoly Parker Brothers B- SLROM Monster in my Pocket Konami B+ TLROM Monster Party Bandai B SLROM Monster Truck Rally INTV B CNROM Moon Ranger Bunch Games C Motor City Patrol Matchbox C+ SLROM Ms. Pacman Namco A NROM Ms. Pacman Tengen B+ Muppet Adventure Hi Tech B SGROM Muscle Bandai B- NROM Mutant Virus American Software B+ SLROM Mystery Quest Taxan C+ CNROM NARC Acclaim C AMROM NES Open Golf Nintendo B- SNROM NFL Football LJN C UNROM Nigel Mansell Gametek B+ SLROM Nightmare on Elm Street LJN B AMROM Nightshade Ultra C+ TLROM Ninja Crusaders American Sammy B TGROM Ninja Gaiden Tecmo E SLROM Ninja Gaiden 2 Tecmo C- TLROM Ninja Gaiden 3 Tecmo C+ TLROM Ninja Kid Bandai C+ CNROM Nombunagas Ambition Koei C SOROM Nombunagas Ambition 2 Koei B ETROM North & South Kemco B+ TSROM Operation Secret Storm Color Dreams A Operation Wolf Taito D SLROM ORB-3D Hi Tech C SCROM Othello Acclaim C NROM Overlord Virgin B- SN1 P'radikus Conflict Color Dreams B Pac Man Namco A 56504 Pac Man (licensed) Tengen B- NROM Pac Man (unlicensed) Tengen B- Pac Mania Tengen A Palamedes Hot B B+ SEROM Panic Resturant Taito B TLROM Paperboy Mindscape C CNROM Paperboy 2 Mindscape B UOROM Parodius (England) Palcom Pebble Beach Golf Bandai C+ CNROM Peek A Boo Poker Panesian A+ Perfect Fit Gametek B CNROM Pesterminator Color Dreams B Peter Pan & the Pirates T*HQ B- SFROM Phantom Fighter FCI C+ SGROM Pictionary LJN B- SLROM Pinball Nintendo D NROM Pinball Quest Jaleco B SLROM Pinbot Nintendo B TQROM Pipe Dream Bullet Proof B CNROM Pirates Ultra B SKROM Platoon Sunsoft D SLROM Play Action Football Nintendo D TLSROM Pool of Radiance FCI B+ TKROM Popeye Nintendo B- NROM POW SNK C SLROM Power Blade Taito B- TLROM Power Blade 2 Taito B- TLROM Power Punch 2 American Softworks B TLROM Predator Activision C+ SLROM Prince of Persia Virgin C+ UNROM Princess Tomato Hudson B+ SGROM Pro Am Nintendo D SEROM Pro Am 2 Tradewest B AOROM Pro Sport Hockey Jaleco B+ TLSROM Pro Wrestling Nintendo D UNROM Puggsly's Scavenger Hunt Ocean B SLROM Punch Out Nintendo C+ PNROM Punisher LJN B TLROM Puss N Boots Electro Brain B- UNROM Puzzle American Video A- Puzznic Taito B+ CNROM Pyramid American Video A- Q*Bert Ultra B- CNROM Qix Taito A SNROM Quantum Fighter HAL B TLROM Quarterback Tradewest D CNROM Quattro Adventure Camerica B+ Quattro Adventure (Aladdin Cart) Camerica A+ Quattro Arcade Camerica A- Quattro Sports Camerica B Quattro Sports (Aladdin Cart) Camerica A+ Race America Absolute B+ SLROM Racket Attack Jaleco C- SLROM Rad Gravity Activision B- SLROM Rad Racer Nintendo D SGROM Rad Racer 2 Square B- TVROM Rad Racket American Video A- Raid 2020 Color Dreams B Raid on Bungling Bay Broderbund B NROM Rainbow Island Taito B+ UNROM Rally Bike Romstar B UNROM Rambo Acclaim E UNROM Rampage Data East C+ TFROM Rampart Jaleco B TLROM RBI Baseball (licensed) Tengen C+ DEROM RBI Baseball (unlicensed) Tengen C RBI Baseball 2 Tengen C+ RBI Baseball 3 Tengen B- Remote Control Hi Tech C SLROM Ren + Stimpy Buckaroos T*HQ B- TLROM Renegade Taito C- UNROM Rescue Kemco C- SLROM Rescue Rangers Capcom C+ SLROM Rescue Rangers 2 Capcom B+ SLROM Ring King Data East C DEROM River City Ransom American Technos C TLROM Road Blasters Mindscape C+ SLROM Road Runner Tengen B+ Robin Hood Virgin C+ SGROM Robo Cop Data East C TL1ROM Robo Cop 2 Data East B- SLROM Robo Cop 3 Ocean B SLROM Robo Demons Color Dreams B Robo Warrior Jaleco B UNROM Rock N Ball NTVIC B TFROM Rocket Ranger Kemco B- SGROM Rocketeer Bandai B- SGROM Rockin Kats Atlus B+ TLROM Rocky & Bullwinkle T*HQ B- TLROM Roger Clemens LJN C 53361 Roller Games Ultra B- TLROM Rollerball HAL B SFROM Rollerblade Racer Hi Tech B 53361 Rolling Thunder Tengen C+ Romance/3 Kingdoms Koei C SOROM Romance/3 Kingdoms 2 Koei B EWROM Roundball Mindscape B- TSROM Rush N Attack Konami D UNROM Rygar Tecmo C UNROM SCAT Natsume B SLROM Secret Scout Color Dreams A Section Z Capcom C UNROM Seicross FCI D NROM Sesame Street 1-2-3 Hi Tech C+ SEROM (SCROROM) Sesame Street 123/ABC Hi Tech B SLROM Sesame Street A-B-C Hi Tech C+ SEROM Sesame Street Countdown Hi Tech A- SLROM Shadow of the Ninja Natsume B TLROM Shadowgate Kemco C+ TKROM Shatterhand Jaleco B- TLROM Shingen the Ruler Hot B C SNROM Shinobi Tengen C+ Shockwave American Game Carts Inc B Shooting Range Bandai B CNROM Short Order/Eggsplode Nintendo B+ SBROM Side Pocket Data East B UNROM Silent Assault Color Dreams B Silent Service Ultra C- 351258 Silk Worm American Sammy C SLROM Silver Surfer Arcadia B TSROM Simpsons Bart Meets Radioactive Man Acclaim B+ 55741 Simpsons Bart Vs Space Mutants Acclaim D SLROM Simpsons Bart Vs World Acclaim B- 53361 Skate or Die Ultra D 351258 Skate or Die 2 Electronic Arts B SLROM Ski or Die Ultra B- 351908 Skull & Crossbones Tengen B Sky Kid Sunsoft B- SCEOROM Sky Shark Taito D SL1ROM Slalom Nintendo B- NROM Smash TV Acclaim C+ 51555 Snake Rattle & Roll Nintendo C+ SEROM Snakes Revenge Ultra C SLROM Snoopy Silly Sports Kemco B+ SLROM Snow Brothers Capcom B SLROM Soccer Nintendo D NROM Solar Jetman Tradewest C AOROM Solitaire American Video A- Soloman's Key Tecmo C+ CNROM Solstice Imagesoft C ANROM Space Shuttle Absolute B+ SGROM Spelunker Broderbund C+ NROM Spiderman LJN B- 53361 Spiritual Warfare Wisdom Tree B+ Spot Arcadia B- SNROM Spy Hunter Sunsoft C CNROM Spy vs. Spy Kemco B- NROM Sqoon Irem B+ NROM Stack Up Nintendo A- HVC Stadium Events Bandai B Stanley Electro Brain B TLROM Star Force Tecmo C+ CNROM Star Soldier Taxan C- CNROM Star Trek 25th Anniversary Ultra B TLROM Star Trek: The Next Generation Absolute A- UNROM Star Tropics Nintendo C- HKROM Star Voyager Acclaim C+ CNROM Star Wars JVC B TSROM Starship Hector Hudson B- UNROM Stealth ATF Activision C+ SLROM Stinger Konami B- UNROM Street Cop Bandai A- SLROM Street Fighter 2010 Capcom B TLROM Strider Capcom C SGROM Stunt Kids Camerica A- Sunday Funday Wisdom Tree B+ Super C Konami C 352026 Super Cars Electro Brain B+ UNROM Super Dodge Ball Imagesoft C SLROM Super Glove Ball Mattel C UNROM Super Mario Brothers Nintendo F NROM Super Mario Brothers 2 Nintendo D TSROM Super Mario Brothers 3 Nintendo D TSROM Super Mario/Duck Hunt Nintendo F MH Super Mario/Duck Hunt/Track Meet Nintendo C COB Super Off Road Tradewest C- AMROM Super Pitfall Activision C+ UNROM Super Spike V'Ball Nintendo C- TLROM Super Spike/World Cup Nintendo B- COB Super Sprint Tengen C COB Super Spy Hunter Sunsoft B+ TLROM Super Team Games Nintendo C- CNROM Superman Kemco B+ SLROM Swamp Thing T*HQ C+ SLROM Sword Master Activision A- TLROM Swords & Serpents Acclaim B+ UNROM T&C Surf Design LJN E CNROM Taboo Tradewest C+ SEROM Tag Team Wrestling Data East C NROM Taggin Dragon Bunch Games B Talespin Capcom B SLROM Target Renegade Taito C+ SLROM Tecmo Baseball Tecmo C SGROM Tecmo Basketball Tecmo C+ TKROM Tecmo Bowl Tecmo E SLROM Tecmo Cup Soccer Tecmo B+ SLROM Tecmo Super Bowl Tecmo C- TKROM Tecmo Wrestling Tecmo C SLROM Teenage Turtles Ultra E 351908 Teenage Turtles 2 Ultra D TLROM Teenage Turtles 3 Konami B- TLROM Teenage Turtles Tournament Fighters Konami B+ TLROM Tennis Nintendo D NROM Terminator Mindscape B+ TLROM Terminator 2 Judgement Day LJN B- 53361 Terra Cresta Vic Tokai B+ UNROM Tetris Tengen A- (CNROM compatible) Tetris Nintendo D SEROM Tetris 2 Nintendo B- TSROM Three Stooges Activision C+ SLROM Thrilla Safari LJN B 53361 Thunder & Lightning Romstar B+ GNROM Thunderbirds Activision C+ SLROM Thundercade American Sammy C+ UNROM Tiger Heli Acclaim C+ CNROM Tiles of Fate American Video B+ Time Lord Milton Bradley C AMROM Times of Lore Toho B- UNROM Tiny Toon Konami C+ TLROM Tiny Toon Cartoon Workshop Konami B+ TSROM Tiny Toons 2 Konami B+ TLROM To The Earth Nintendo C+ TEROM Toki Taito B+ TLROM Tom & Jerry Hi Tech B+ TLROM Tombs & Treasures Infocom A- SGROM Toobin Tengen B Top Gun Konami D 351298 Top Gun 2 Konami D 352026 Top Players Tennis Asmik C+ SLROM Total Recall Acclaim D UNROM Totally Rad Jaleco B- TLROM Touchdown Fever SNK B+ SFROM Toxic Crusader Bandai B TLROM Track & Field Konami D CNROM Track & Field 2 Konami D SLROM Treasure Master American Softworks A- SLROM Trick Shooting Nintendo C+ SCROM Trog Acclaim B+ UNROM Trojan Capcom C UNROM Trolls on Treasure Island American Video A- Twin Cobra American Sammy C+ TLROM Twin Eagle Romstar C+ UNROM Ultima/Exodus FCI C- SNROM Ultima/Quest Avatar FCI B SNROM Ultima/War Destiny FCI B+ SNROM Ultimate Air Combat Activision B+ TLROM Ultimate Basketball American Sammy C TLROM Ultimate League Soccer American Video A- Ultimate Stuntman Camerica C+ Uncharted Waters Koei B- ETROM Uninvited Kemco B+ TKROM Untouchables Ocean B- SLROM Urban Champion Nintendo D NROM Vegas Dream HAL B SKROM Venice Beach Volleyball American Video A- Vice Project Doom American Sammy C+ TLROM Videomation T*HQ B+ CPROM Vindicators Tengen C Volleyball Nintendo C NROM Wacky Races Atlus B+ TLROM Wall Street Kid Sofel B UNROM Wally Bear and the No Gang American Video A- Wario Woods Nintendo B+ TKROM Wayne Gretzky T*HQ C+ UNROM Wayne's World T*HQ B TLROM Werewolf Data East C TLROM Wheel/Fortune Gametek C AOROM Wheel/Fortune/Family Gametek C ANROM Wheel/Fortune/Junior Gametek C ANROM Wheel/Fortune/Vanna Gametek B AOROM Where in Time is Carmen Konami B TSROM Where's Waldo T*HQ C+ TSROM Who Framed Roger Rabbit LJN B- ANROM Whomp Em Jaleco B TLROM Widget Atlus B TLROM Wild Gunman Nintendo C NROM Willow Capcom B- SLROM Win Lose or Draw Hi Tech C SGROM Winter Games Acclaim B- UNROM Wizardry Nexoft C+ SKROM Wizardry 2 Knight of Diamonds Ascii B+ TKROM Wizards & Warriors Acclaim D ANROM Wizards & Warriors 2 - Ironsword Acclaim D AOROM Wizards & Warriors 3 Acclaim C- 54425 Wolverine LJN B- TLROM World Champ Romstar B+ TLROM World Championship Wrestling FCI D TLROM World Class Track Meet Nintendo D CNROM World Cup Soccer Nintendo C- TLROM World Games Milton Bradley B- ANROM World Runner 3D Acclaim C- UNROM Wrath of the Black Manta Taito C+ SLROM Wrecking Crew Nintendo C+ NROM Wrestlemania Acclaim D ANROM Wrestlemania Challenge LJN C- UNROM Wrestlemania Steel Cage LJN C+ 53361 Wurm Asmik B- TLROM Xenophobe Sunsoft B- SFROM Xevious Bandai C+ NROM Xexyz Hudson C+ SLROM Yo Noid Capcom C+ SLROM Yoshi Nintendo C- SFROM Yoshi's Cookie Nintendo B- TFROM Young Indy Jaleco B+ TLROM Zanac FCI C+ UNROM Zelda Nintendo E SNROM Zelda 2 Nintendo D SKROM Zen Konami B TLROM Zoda's Revenge, Startropics 2 Nintendo B- HKROM Zombie Nation Meldac B TLROM ================================================ FILE: docs/cpu/branch_timing_readme.txt ================================================ NES 6502 Branch Timing Test ROMs -------------------------------- These ROMs test timing of the branch instruction, including edge cases which an emulator might get wrong. When run on a NES they all give a passing result. Each ROM runs several tests and reports the result on screen and by beeping a number of times. See below for the meaning of failure codes for each test. THE TESTS MUST BE RUN (*AND* *PASS*) IN ORDER, because some earlier ROMs test things that later ones assume will work properly. Source code for each test is included, and most tests are clearly divided into sections. Support code is also included, but it runs on a custom devcart and assembler so it will require some effort to assemble. Contact me if you'd like assistance porting them to your setup. Branch Timing Summary --------------------- An untaken branch takes 2 clocks. A taken branch takes 3 clocks. A taken branch that crosses a page takes 4 clocks. Page crossing occurs when the high byte of the branch target address is different than the high byte of address of the next instruction: branch_target: ... bne branch_target next_instruction: nop ... branch_target: 1.Branch_Basics --------------- Tests branch timing basics and PPU NMI timing, which is needed for the tests 2) NMI period is too short 3) NMI period is too long 4) Branch not taken is too long 5) Branch not taken is too short 6) Branch taken is too long 7) Branch taken is too short 2.Backward_Branch ----------------- Tests backward (negative) branch timing. 2) Branch from $E4FD to $E4FC is too long 3) Branch from $E4FD to $E4FC is too short 4) Branch from $E5FE to $E5FD is too long 5) Branch from $E5FE to $E5FD is too short 6) Branch from $E700 to $E6FF is too long 7) Branch from $E700 to $E6FF is too short 8) Branch from $E801 to $E800 is too long 9) Branch from $E801 to $E800 is too short 3.Forward_Branch ---------------- Tests forward (positive) branch timing. 2) Branch from $E5FC to $E5FF is too long 3) Branch from $E5FC to $E5FF is too short 4) Branch from $E6FD to $E700 is too long 5) Branch from $E6FD to $E700 is too short 6) Branch from $E7FE to $E801 is too long 7) Branch from $E7FE to $E801 is too short 8) Branch from $E8FF to $E902 is too long 9) Branch from $E8FF to $E902 is too short -- Shay Green ================================================ FILE: docs/cpu/dummy_writes_readme.txt ================================================ NES Double-Write Behavior Tests ---------------------------------- These tests verify that the CPU is doing double-writes properly. Double-write is a side effect of the NES CPU when it is executing a read-modify-write instruction: It first reads the original value, then writes back the same value, and then writes the modified value. For example, the cycle by cycle listing of an absolute-addressing instruction such as INC is as follows (from 65doc.txt by John West and Marko Mkel): Read-Modify-Write instructions (ASL, LSR, ROL, ROR, INC, DEC, SLO, SRE, RLA, RRA, ISB, DCP) # address R/W description --- ------- --- ------------------------------------------ 1 PC R fetch opcode, increment PC 2 PC R fetch low byte of address, increment PC 3 PC R fetch high byte of address, increment PC 4 address R read from effective address 5 address W write the value back to effective address, and do the operation on it 6 address W write the new value to effective address Two sets of tests are provided: One that uses OAM data ($2004) for testing, and one that uses PPU memory ($2007). The OAM data testing is only valid on emulators. The actual NES console fails the test, because the OAM read port is not reliable on the real console. The PPUMEM test can be used on emulators and on the real NES. The PPUMEM test requires that the emulator implements open bus behavior properly. Without open bus behavior the testing will not work as expected. Because of that, an extensive set of tests is first performed for the open bus behavior. Tests in the OAM version: #2: OAM reading is too unreliable. #3: Writes to OAM should automatically increment SPRADDR #4: Reads from OAM should not automatically increment SPRADDR #5: Some opcodes failed the test. #6: OAM reads are unreliable. #7: ROM should not be writable. Fail codes #2 and #6 are basically the same thing, except #2 is given if #5 also fails. If #5 passes, but the OAM read test failed, #6 is given instead. Expected output in the OAM version: TEST: cpu_dummy_writes_oam This program verifies that the CPU does 2x writes properly. Any read-modify-write opcode should first write the origi- nal value; then the calculated value exactly 1 cycle later. Requirement: OAM memory reads MUST be reliable. This is often the case on emulators, but NOT on the real NES. Nevertheless, this test can be used to see if the CPU in the emulator is built properly. Testing OAM. The screen will go blank for a moment now. OK; Verifying opcodes... 0E2E4E6ECEEE 1E3E5E7EDEFE 0F2F4F6FCFEF 1F3F5F7FDFFF 03234363C3E3 13335373D3F3 1B3B5B7BDBFB Passed Tests in the PPUMEM version: #2: Non-palette PPU memory reads should have one-byte buffer #3: A single write to $2005 must not change the address used by $2007 when vblank is on. #4: Even two writes to $2005 must not change the address used by $2007 when vblank is on. #5: A single write to $2006 must not change the address used by $2007 when vblank is on. #6: A single write to $2005 must change the address toggle for both $2005 and $2006. #7: Sequential PPU memory read does not work #8: Sequential PPU memory write does not work #9: Some opcodes failed the test. #10: Open bus behavior is wrong. #11: ROM should not be writable. Expected output in the PPUMEM version: TEST: cpu_dummy_writes_ppumem This program verifies that the CPU does 2x writes properly. Any read-modify-write opcode should first write the origi- nal value; then the calculated value exactly 1 cycle later. Verifying open bus behavior. W- W- WR W- W- W- W- WR 2000+ 0 1 2 3 4 5 6 7 R0: 0- 0- 00 0- 0- 0- 0- 00 R1: 0- 0- 00 0- 0- 0- 0- 00 R3: 0- 0- 00 0- 0- 0- 0- 00 R5: 0- 0- 00 0- 0- 0- 0- 00 R6: 0- 0- 00 0- 0- 0- 0- 00 OK; Verifying opcodes... 0E2E4E6ECEEE 1E3E5E7EDEFE 0F2F4F6FCFEF 1F3F5F7FDFFF 03234363C3E3 13335373D3F3 1B3B5B7BDBFB Passed Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The text output may include ANSI color codes, which take the form of an esc character ($1B), an opening bracket ('['), and a sequence of numbers and semicolon characters, terminated by a non-digit character ('m'). The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 -- Shay Green Joel Yliluoma ================================================ FILE: docs/cpu/exec_space_readme.txt ================================================ NES Memory Execution Tests ---------------------------------- These tests verify that the CPU can execute code from any possible memory location, even if that is mapped as I/O space. In addition, two obscure side effects are tested: 1. The PPU open bus. Any write to PPU will update the open bus. Reading from 2002 updates the low 5 bits. Reading from 2007 updates 8 bits. The open bus is shown in any addresss/bit that the PPU does not write to. Read from 2000, you get open bus. Read from 2006, ditto. Read from 2002, you get that in high 3 bits. Additionally, the open bus decays automatically to zero in about one second if not refreshed. This test requires that a value written to $2003 can be read back from $2001 within a time window of one or two frames. 2. One-byte opcodes must issue a dummy read to the byte immediately following that opcode. The CPU always does a fetch of the second byte, before it has even begun executing the opcode in the first place. Additionally, the following PPU features must be working properly: 1. PPU memory writes and reads through $2006/$2007 2. The address high/low toggle reset at $2002 3. A single write through $2006 must not affect the address used by $2007 4. NMI should fire sometimes to salvage a broken program, if the JSR/JMP never reaches its intended destination. (Only required in the test IF the CPU and/or open bus are not working properly.) The test is done FIVE times: Once with JSR $2001, again with JMP $2001, and then with RTS (with target address of $2001), and then with a JMP that expects to return with an RTI opcode. Finally, with a regular JSR, but the return from the code is done through a BRK instruction. Tests and results: #2: PPU memory access through $2007 does not work properly. (Use other tests to determine the exact problem.) #3: PPU open bus implementation is missing or incomplete: A write to $2003, followed by a read from $2001 should return the same value as was written. #4: The RTS at $2001 was never executed. (If NMI has not been implemented in the emulator, the symptom of this failure is that the program crashes and does not output either "Fail" nor "Passed"). #5: An RTS opcode should still do a dummy fetch of the next opcode. (The same goes for all one-byte opcodes, really.) #6: I have no idea what happened, but the test did not work as supposed to. In any case, the problem is in the PPU. #7: A jump to $2001 should never execute code from $8001 / $9001 / $A001 / $B001 / $C001 / $D001 / $E001. #8: Okay, the test passed when JSR was used, but NOT when the opcode was JMP. I definitely did not think any emulator would trigger this result. #9: Your PPU is broken in mind-defyingly random ways. #10: RTS to $2001 never returned. This message never gets displayed. #11: The test passed when JSR was used, and when JMP was used, but NOT when RTS was used. Caught ya! Paranoia wins. #12: Your PPU gave up reason at the last moment. #13: JMP to $2001 never returned. Again, this message never gets displayed. #14: An RTI opcode should still do a dummy fetch of the next opcode. (The same goes for all one-byte opcodes, really.) #15: An RTI opcode should not destroy the PPU. Somehow that still appears to be the case here. #16: IRQ occurred uncalled #17: JSR to $2001 never returned. (Never displayed) #18: The BRK instruction should issue an automatic fetch of the byte that follows right after the BRK. (The same goes for all one-byte opcodes, but with BRK it should be a bit more obvious than with others.) #19: A BRK opcode should not destroy the PPU. Somehow that still appears to be the case here. Expected output: TEST:test_cpu_exec_space_ppuio This program verifies that the CPU can execute code from any possible location that it can address, including I/O space. In addition, it will be tested that an RTS instruction does a dummy read of the byte that immediately follows the instructions. JSR+RTS TEST OK JMP+RTS TEST OK RTS+RTS TEST OK JMP+RTI TEST OK JMP+BRK TEST OK Passed Expected output in the other test: TEST: test_cpu_exec_space_apu This program verifies that the CPU can execute code from any possible location that it can address, including I/O space. In this test, it is also verified that not only all write-only APU I/O ports return the open bus, but also the unallocated I/O space in $4018..$40FF. 40FF 40 Passed Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The text output may include ANSI color codes, which take the form of an esc character ($1B), an opening bracket ('['), and a sequence of numbers and semicolon characters, terminated by a non-digit character ('m'). The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 -- Shay Green Joel Yliluoma ================================================ FILE: docs/cpu/instr_misc_readme.txt ================================================ NES CPU Instruction Behavior Misc Tests ---------------------------------------- These tests verify miscellaneous instruction behavior. 01-abs_x_wrap ------------- Verifies that $FFFF wraps around to 0 for STA abs,X and LDA abs,X. 02-branch_wrap -------------- Verifies that branching past end or before beginning of RAM wraps around. 03-dummy_reads -------------- Tests some instructions that do dummy reads before the real read/write. Doesn't test all instructions. Tests LDA and STA with modes (ZP,X), (ZP),Y and ABS,X Dummy reads for the following cases are tested: LDA ABS,X or (ZP),Y when carry is generated from low byte STA ABS,X or (ZP),Y ROL ABS,X always 04-dummy_reads_apu ------------------ Tests dummy reads for (hopefully) ALL instructions which do them, including unofficial ones. Prints opcode(s) of failed instructions. Requires that APU implement $4015 IRQ flag reading. Multi-tests ----------- The NES/NSF builds in the main directory consist of multiple sub-tests. When run, they list the subtests as they are run. The final result code refers to the first sub-test that failed. For more information about any failed subtests, run them individually from rom_singles/ and nsf_singles/. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Text output ----------- Tests generally print information on screen, but also output information in other ways, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. When building as an NSF, the final result is reported as a series of beeps (see below). Any important diagnostic bytes are also reported as beeps, before the final result. All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. See the source code for more information about a particular test and why it might be failing. Each test has comments anout its operation. NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. In addition to the other text output methods described above, NSF builds report essential information bytes audibly, including the final result. A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason as listed in the source code by the corresponding set_code line. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 -- Shay Green ================================================ FILE: docs/cpu/instr_test_readme.txt ================================================ NES CPU Instruction Behavior Tests ---------------------------------- These tests verify most instruction behavior fairly thoroughly, including unofficial instructions. Failing instructions are listed by their opcode and name. Serious errors in behavior of basic opcodes might cause many false errors. These tests will NOT help you figure out what is wrong with your implementation of the failed instructions, simply whether you are failing any, and which those are. all_instrs.nes tests (almost) all instructions, including unofficial ones, while official_only.nes tests only official ("documented") instructions. The *_singles/ test all instructions, but test the official ones first, so you can tell whether you pass those even if your emulator hangs on the unofficial ones. The nsf_singles builds audibly report the opcodes of any failed instructions before the final result. Internal operation ------------------ Instructions are tested by setting many combinations of input values for registers, flags, and memory, running the instruction under test, then updating a running checksum with the resulting values. After trying all interesting input combinations, the checksum is compared with the correct one to find whether the instruction passed. This approach is used for all instructions, even those that shouldn't care the value of any registers or modify them. This catches an emulator incorrectly looking at or modifying registers in those instructions. This approach makes it very easy to write the tests, since the instructions don't have to be each coded for separately; instead, only the different addressing modes need separate tests. instrs: what opcodes to test, along with their names. instr_template: template for instructions. First byte is replaced with opcode. After executing instruction and anything after, it should jump to instr_done. operand: where to place byte operand for instruction. This value comes from the table of values to test, using an index separate from that used to set the other registers before executing the instruction. set_in: things to execute before the instruction. On entry, A is value put in operand, and Y is index used in table. check_out: things to execute after the instruction. values2: if defined, set of values to use for operand. Default uses same set as for other registers. test_values: routine to actually run the tests. test_normal does what's described above. correct_checksums: list of checksums for each instruction. Generated when CALIBRATE=1 is uncommented. Instructions ------------ U = Unofficial X = Freezes CPU, so not tested ? = Inconsistent/unknown behavior, so not tested 00 BRK #n 01 ORA (z,X) 02 X KIL 03 U SLO (z,X) 04 U DOP z 05 ORA z 06 ASL z 07 U SLO z 08 PHP 09 ORA #n 0A ASL A 0B U AAC #n 0C U TOP abs 0D ORA a 0E ASL a 0F U SLO abs 10 BPL r 11 ORA (z),Y 12 X KIL 13 U SLO (z),Y 14 U DOP z,X 15 ORA z,X 16 ASL z,X 17 U SLO z,X 18 CLC 19 ORA a,Y 1A U NOP 1B U SLO abs,Y 1C U TOP abs,X 1D ORA a,X 1E ASL a,X 1F U SLO abs,X 20 JSR a 21 AND (z,X) 22 X KIL 23 U RLA (z,X) 24 BIT z 25 AND z 26 ROL z 27 U RLA z 28 PLP 29 AND #n 2A ROL A 2B U AAC #n 2C BIT a 2D AND a 2E ROL a 2F U RLA abs 30 BMI r 31 AND (z),Y 32 X KIL 33 U RLA (z),Y 34 U DOP z,X 35 AND z,X 36 ROL z,X 37 U RLA z,X 38 SEC 39 AND a,Y 3A U NOP 3B U RLA abs,Y 3C U TOP abs,X 3D AND a,X 3E ROL a,X 3F U RLA abs,X 40 RTI 41 EOR (z,X) 42 X KIL 43 U SRE (z,X) 44 U DOP z 45 EOR z 46 LSR z 47 U SRE z 48 PHA 49 EOR #n 4A LSR A 4B U ASR #n 4C JMP a 4D EOR a 4E LSR a 4F U SRE abs 50 BVC r 51 EOR (z),Y 52 X KIL 53 U SRE (z),Y 54 U DOP z,X 55 EOR z,X 56 LSR z,X 57 U SRE z,X 58 CLI 59 EOR a,Y 5A U NOP 5B U SRE abs,Y 5C U TOP abs,X 5D EOR a,X 5E LSR a,X 5F U SRE abs,X 60 RTS 61 ADC (z,X) 62 X KIL 63 U RRA (z,X) 64 U DOP z 65 ADC z 66 ROR z 67 U RRA z 68 PLA 69 ADC #n 6A ROR A 6B U ARR #n 6C JMP (a) 6D ADC a 6E ROR a 6F U RRA abs 70 BVS r 71 ADC (z),Y 72 X KIL 73 U RRA (z),Y 74 U DOP z,X 75 ADC z,X 76 ROR z,X 77 U RRA z,X 78 SEI 79 ADC a,Y 7A U NOP 7B U RRA abs,Y 7C U TOP abs,X 7D ADC a,X 7E ROR a,X 7F U RRA abs,X 80 U DOP #n 81 STA (z,X) 82 U DOP #n 83 U AAX (z,X) 84 STY z 85 STA z 86 STX z 87 U AAX z 88 DEY 89 U DOP #n 8A TXA 8B ? XAA #n 8C STY a 8D STA a 8E STX a 8F U AAX abs 90 BCC r 91 STA (z),Y 92 X KIL 93 ? AXA (z),Y 94 STY z,X 95 STA z,X 96 STX z,Y 97 U AAX z,Y 98 TYA 99 STA a,Y 9A TXS 9B ? XAS abs,Y 9C U SYA abs,X 9D STA a,X 9E U SXA abs,Y 9F ? AXA abs,Y A0 LDY #n A1 LDA (z,X) A2 LDX #n A3 U LAX (z,X) A4 LDY z A5 LDA z A6 LDX z A7 U LAX z A8 TAY A9 LDA #n AA TAX AB U ATX #n AC LDY a AD LDA a AE LDX a AF U LAX abs B0 BCS r B1 LDA (z),Y B2 X KIL B3 U LAX (z),Y B4 LDY z,X B5 LDA z,X B6 LDX z,Y B7 U LAX z,Y B8 CLV B9 LDA a,Y BA TSX BB ? LAR abs,Y BC LDY a,X BD LDA a,X BE LDX a,Y BF U LAX abs,Y C0 CPY #n C1 CMP (z,X) C2 U DOP #n C3 U DCP (z,X) C4 CPY z C5 CMP z C6 DEC z C7 U DCP z C8 INY C9 CMP #n CA DEX CB U AXS #n CC CPY a CD CMP a CE DEC a CF U DCP abs D0 BNE r D1 CMP (z),Y D2 X KIL D3 U DCP (z),Y D4 U DOP z,X D5 CMP z,X D6 DEC z,X D7 U DCP z,X D8 CLD D9 CMP a,Y DA U NOP DB U DCP abs,Y DC U TOP abs,X DD CMP a,X DE DEC a,X DF U DCP abs,X E0 CPX #n E1 SBC (z,X) E2 U DOP #n E3 U ISC (z,X) E4 CPX z E5 SBC z E6 INC z E7 U ISC z E8 INX E9 SBC #n EA NOP EB U SBC #n EC CPX a ED SBC a EE INC a EF U ISC abs F0 BEQ r F1 SBC (z),Y F2 X KIL F3 U ISC (z),Y F4 U DOP z,X F5 SBC z,X F6 INC z,X F7 U ISC z,X F8 SED F9 SBC a,Y FA U NOP FB U ISC abs,Y FC U TOP abs,X FD SBC a,X FE INC a,X FF U ISC abs,X Multi-tests ----------- The NES/NSF builds in the main directory consist of multiple sub-tests. When run, they list the subtests as they are run. The final result code refers to the first sub-test that failed. For more information about any failed subtests, run them individually from rom_singles/ and nsf_singles/. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. -- Shay Green ================================================ FILE: docs/cpu/instr_timing_readme.txt ================================================ NES CPU Instruction Timing Test ------------------------------- These tests verify timing of all NES CPU instructions, except the 12 that freeze the CPU. The individual tests report the opcode of any failed instructions. instr_timing prints the measured and correct times. branch_timing runs the branch instruction in 8 different situations: four not taken, and four taken. For each of these four, the first two are for a non-page-cross both negative and positive, and the second two cross a page. The correct times are 2 2 2 2 3 3 4 4. Requirements ------------ - Basic CPU instruction behavior - Basic APU length counter operation Internal operation ------------------ Each instruction is timed by setting up appropriate conditions, synchronizing to the APU length counter and then loading it with 2, executing the instruction in a loop that stops once the length counter expires. The number of loop iterations indicates how many clocks the instruction took. Multi-tests ----------- The NES/NSF builds in the main directory consist of multiple sub-tests. When run, they list the subtests as they are run. The final result code refers to the first sub-test that failed. For more information about any failed subtests, run them individually from rom_singles/ and nsf_singles/. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. -- Shay Green ================================================ FILE: docs/cpu/interrupts_readme.txt ================================================ NES CPU Interrupt Tests ----------------------- Tests behavior and timing of CPU in the presence of interrupts, both IRQ and NMI. CLI Latency Summary ------------------- The RTI instruction affects IRQ inhibition immediately. If an IRQ is pending and an RTI is executed that clears the I flag, the CPU will invoke the IRQ handler immediately after RTI finishes executing. The CLI, SEI, and PLP instructions effectively delay changes to the I flag until after the next instruction. For example, if an interrupt is pending and the I flag is currently set, executing CLI will execute the next instruction before the CPU invokes the IRQ handler. This delay only affects inhibition, not the value of the I flag itself; CLI followed by PHP will leave the I flag cleared in the saved status byte on the stack (bit 2), as expected. 1-cli_latency ------------- Tests the delay in CLI taking effect, and some basic aspects of IRQ handling and the APU frame IRQ (needed by the tests). It uses the APU's frame IRQ and first verifies that it works well enough for the tests. The later tests execute CLI followed by SEI and equivalent pairs of instructions (CLI, PLP, where the PLP sets the I flag). These should only allow at most one invocation of the IRQ handler, even if it doesn't acknowledge the source of the IRQ. RTI is also tested, which behaves differently. These tests also *don't* disable interrupts after the first IRQ, in order to test whether a pair of instructions allows only one interrupt or causes continuous interrupts that block the main code from continuing. 2) RTI should not adjust return address (as RTS does) 3) APU should generate IRQ when $4017 = $00 4) Exactly one instruction after CLI should execute before IRQ is taken 5) CLI SEI should allow only one IRQ just after SEI 6) In IRQ allowed by CLI SEI, I flag should be set in saved status flags 7) CLI PLP should allow only one IRQ just after PLP 8) PLP SEI should allow only one IRQ just after SEI 9) PLP PLP should allow only one IRQ just after PLP 10) CLI RTI should not allow any IRQs 11) Unacknowledged IRQ shouldn't let any mainline code run 12) RTI RTI shouldn't let any mainline code run 2-nmi_and_brk ------------- NMI behavior when it interrupts BRK. Occasionally fails on NES due to PPU-CPU synchronization. Result when run: NMI BRK -- 27 36 00 NMI before CLC 26 36 00 NMI after CLC 26 36 00 36 00 00 NMI interrupting BRK, with B bit set on stack 36 00 00 36 00 00 36 00 00 36 00 00 27 36 00 NMI after SEC at beginning of IRQ handler 27 36 00 3-nmi_and_irq ------------- NMI behavior when it interrupts IRQ vectoring. Result when run: NMI IRQ 23 00 NMI occurs before LDA #1 21 00 NMI occurs after LDA #1 (Z flag clear) 21 00 20 00 NMI occurs after CLC, interrupting IRQ 20 00 20 00 20 00 20 00 20 00 20 00 Same result for 7 clocks before IRQ is vectored 25 20 IRQ occurs, then NMI occurs after SEC in IRQ handler 25 20 4-irq_and_dma ------------- Has IRQ occur at various times around sprite DMA. First column refers to what instruction IRQ occurred after. Second column is time of IRQ, in CPU clocks relative to some arbitrary starting point. 0 +0 1 +1 1 +2 2 +3 2 +4 4 +5 4 +6 7 +7 7 +8 7 +9 7 +10 8 +11 8 +12 8 +13 ... 8 +524 8 +525 8 +526 9 +527 5-branch_delays_irq ------------------- A taken non-page-crossing branch ignores IRQ during its last clock, so that next instruction executes before the IRQ. Other instructions would execute the NMI before the next instruction. The same occurs for NMI, though that's not tested here. test_jmp T+ CK PC 00 02 04 NOP 01 01 04 02 03 07 JMP 03 02 07 04 01 07 05 02 08 NOP 06 01 08 07 03 08 JMP 08 02 08 09 01 08 test_branch_not_taken T+ CK PC 00 02 04 CLC 01 01 04 02 02 06 BCS 03 01 06 04 02 07 NOP 05 01 07 06 04 0A JMP 07 03 0A 08 02 0A 09 01 0A JMP test_branch_taken_pagecross T+ CK PC 00 02 0D CLC 01 01 0D 02 04 00 BCC 03 03 00 04 02 00 05 01 00 06 04 03 LDA $100 07 03 03 08 02 03 09 01 03 test_branch_taken T+ CK PC 00 02 04 CLC 01 01 04 02 03 07 BCC 03 02 07 04 05 0A LDA $100 *** This is the special case 05 04 0A 06 03 0A 07 02 0A 08 01 0A 09 03 0A JMP Multi-tests ----------- The NES/NSF builds in the main directory consist of multiple sub-tests. When run, they list the subtests as they are run. The final result code refers to the first sub-test that failed. For more information about any failed subtests, run them individually from rom_singles/ and nsf_singles/. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. -- Shay Green ================================================ FILE: docs/cpu/opcode_list.txt ================================================ 0 #00 BRK : mode: 6 Implied size: 2 cycles: 7 page_cycles: 0 1 #01 ORA : mode: 7 IndexedIndirect size: 2 cycles: 6 page_cycles: 0 2 #02 KIL : mode: 6 Implied size: 0 cycles: 2 page_cycles: 0 3 #03 SLO : mode: 7 IndexedIndirect size: 0 cycles: 8 page_cycles: 0 4 #04 NOP : mode: 11 ZeroPaged size: 2 cycles: 3 page_cycles: 0 5 #05 ORA : mode: 11 ZeroPaged size: 2 cycles: 3 page_cycles: 0 6 #06 ASL : mode: 11 ZeroPaged size: 2 cycles: 5 page_cycles: 0 7 #07 SLO : mode: 11 ZeroPaged size: 0 cycles: 5 page_cycles: 0 8 #08 PHP : mode: 6 Implied size: 1 cycles: 3 page_cycles: 0 9 #09 ORA : mode: 5 Immediate size: 2 cycles: 2 page_cycles: 0 10 #0A ASL : mode: 4 Accumulator size: 1 cycles: 2 page_cycles: 0 11 #0B ANC : mode: 5 Immediate size: 0 cycles: 2 page_cycles: 0 12 #0C NOP : mode: 1 Absolute size: 3 cycles: 4 page_cycles: 0 13 #0D ORA : mode: 1 Absolute size: 3 cycles: 4 page_cycles: 0 14 #0E ASL : mode: 1 Absolute size: 3 cycles: 6 page_cycles: 0 15 #0F SLO : mode: 1 Absolute size: 0 cycles: 6 page_cycles: 0 16 #10 BPL : mode: 10 Relative size: 2 cycles: 2 page_cycles: 1 17 #11 ORA : mode: 9 IndirectIndexed size: 2 cycles: 5 page_cycles: 1 18 #12 KIL : mode: 6 Implied size: 0 cycles: 2 page_cycles: 0 19 #13 SLO : mode: 9 IndirectIndexed size: 0 cycles: 8 page_cycles: 0 20 #14 NOP : mode: 12 ZeroPagedX size: 2 cycles: 4 page_cycles: 0 21 #15 ORA : mode: 12 ZeroPagedX size: 2 cycles: 4 page_cycles: 0 22 #16 ASL : mode: 12 ZeroPagedX size: 2 cycles: 6 page_cycles: 0 23 #17 SLO : mode: 12 ZeroPagedX size: 0 cycles: 6 page_cycles: 0 24 #18 CLC : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 25 #19 ORA : mode: 3 AbsoluteY size: 3 cycles: 4 page_cycles: 1 26 #1A NOP : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 27 #1B SLO : mode: 3 AbsoluteY size: 0 cycles: 7 page_cycles: 0 28 #1C NOP : mode: 2 AbsoluteX size: 3 cycles: 4 page_cycles: 1 29 #1D ORA : mode: 2 AbsoluteX size: 3 cycles: 4 page_cycles: 1 30 #1E ASL : mode: 2 AbsoluteX size: 3 cycles: 7 page_cycles: 0 31 #1F SLO : mode: 2 AbsoluteX size: 0 cycles: 7 page_cycles: 0 32 #20 JSR : mode: 1 Absolute size: 3 cycles: 6 page_cycles: 0 33 #21 AND : mode: 7 IndexedIndirect size: 2 cycles: 6 page_cycles: 0 34 #22 KIL : mode: 6 Implied size: 0 cycles: 2 page_cycles: 0 35 #23 RLA : mode: 7 IndexedIndirect size: 0 cycles: 8 page_cycles: 0 36 #24 BIT : mode: 11 ZeroPaged size: 2 cycles: 3 page_cycles: 0 37 #25 AND : mode: 11 ZeroPaged size: 2 cycles: 3 page_cycles: 0 38 #26 ROL : mode: 11 ZeroPaged size: 2 cycles: 5 page_cycles: 0 39 #27 RLA : mode: 11 ZeroPaged size: 0 cycles: 5 page_cycles: 0 40 #28 PLP : mode: 6 Implied size: 1 cycles: 4 page_cycles: 0 41 #29 AND : mode: 5 Immediate size: 2 cycles: 2 page_cycles: 0 42 #2A ROL : mode: 4 Accumulator size: 1 cycles: 2 page_cycles: 0 43 #2B ANC : mode: 5 Immediate size: 0 cycles: 2 page_cycles: 0 44 #2C BIT : mode: 1 Absolute size: 3 cycles: 4 page_cycles: 0 45 #2D AND : mode: 1 Absolute size: 3 cycles: 4 page_cycles: 0 46 #2E ROL : mode: 1 Absolute size: 3 cycles: 6 page_cycles: 0 47 #2F RLA : mode: 1 Absolute size: 0 cycles: 6 page_cycles: 0 48 #30 BMI : mode: 10 Relative size: 2 cycles: 2 page_cycles: 1 49 #31 AND : mode: 9 IndirectIndexed size: 2 cycles: 5 page_cycles: 1 50 #32 KIL : mode: 6 Implied size: 0 cycles: 2 page_cycles: 0 51 #33 RLA : mode: 9 IndirectIndexed size: 0 cycles: 8 page_cycles: 0 52 #34 NOP : mode: 12 ZeroPagedX size: 2 cycles: 4 page_cycles: 0 53 #35 AND : mode: 12 ZeroPagedX size: 2 cycles: 4 page_cycles: 0 54 #36 ROL : mode: 12 ZeroPagedX size: 2 cycles: 6 page_cycles: 0 55 #37 RLA : mode: 12 ZeroPagedX size: 0 cycles: 6 page_cycles: 0 56 #38 SEC : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 57 #39 AND : mode: 3 AbsoluteY size: 3 cycles: 4 page_cycles: 1 58 #3A NOP : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 59 #3B RLA : mode: 3 AbsoluteY size: 0 cycles: 7 page_cycles: 0 60 #3C NOP : mode: 2 AbsoluteX size: 3 cycles: 4 page_cycles: 1 61 #3D AND : mode: 2 AbsoluteX size: 3 cycles: 4 page_cycles: 1 62 #3E ROL : mode: 2 AbsoluteX size: 3 cycles: 7 page_cycles: 0 63 #3F RLA : mode: 2 AbsoluteX size: 0 cycles: 7 page_cycles: 0 64 #40 RTI : mode: 6 Implied size: 1 cycles: 6 page_cycles: 0 65 #41 EOR : mode: 7 IndexedIndirect size: 2 cycles: 6 page_cycles: 0 66 #42 KIL : mode: 6 Implied size: 0 cycles: 2 page_cycles: 0 67 #43 SRE : mode: 7 IndexedIndirect size: 0 cycles: 8 page_cycles: 0 68 #44 NOP : mode: 11 ZeroPaged size: 2 cycles: 3 page_cycles: 0 69 #45 EOR : mode: 11 ZeroPaged size: 2 cycles: 3 page_cycles: 0 70 #46 LSR : mode: 11 ZeroPaged size: 2 cycles: 5 page_cycles: 0 71 #47 SRE : mode: 11 ZeroPaged size: 0 cycles: 5 page_cycles: 0 72 #48 PHA : mode: 6 Implied size: 1 cycles: 3 page_cycles: 0 73 #49 EOR : mode: 5 Immediate size: 2 cycles: 2 page_cycles: 0 74 #4A LSR : mode: 4 Accumulator size: 1 cycles: 2 page_cycles: 0 75 #4B ALR : mode: 5 Immediate size: 0 cycles: 2 page_cycles: 0 76 #4C JMP : mode: 1 Absolute size: 3 cycles: 3 page_cycles: 0 77 #4D EOR : mode: 1 Absolute size: 3 cycles: 4 page_cycles: 0 78 #4E LSR : mode: 1 Absolute size: 3 cycles: 6 page_cycles: 0 79 #4F SRE : mode: 1 Absolute size: 0 cycles: 6 page_cycles: 0 80 #50 BVC : mode: 10 Relative size: 2 cycles: 2 page_cycles: 1 81 #51 EOR : mode: 9 IndirectIndexed size: 2 cycles: 5 page_cycles: 1 82 #52 KIL : mode: 6 Implied size: 0 cycles: 2 page_cycles: 0 83 #53 SRE : mode: 9 IndirectIndexed size: 0 cycles: 8 page_cycles: 0 84 #54 NOP : mode: 12 ZeroPagedX size: 2 cycles: 4 page_cycles: 0 85 #55 EOR : mode: 12 ZeroPagedX size: 2 cycles: 4 page_cycles: 0 86 #56 LSR : mode: 12 ZeroPagedX size: 2 cycles: 6 page_cycles: 0 87 #57 SRE : mode: 12 ZeroPagedX size: 0 cycles: 6 page_cycles: 0 88 #58 CLI : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 89 #59 EOR : mode: 3 AbsoluteY size: 3 cycles: 4 page_cycles: 1 90 #5A NOP : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 91 #5B SRE : mode: 3 AbsoluteY size: 0 cycles: 7 page_cycles: 0 92 #5C NOP : mode: 2 AbsoluteX size: 3 cycles: 4 page_cycles: 1 93 #5D EOR : mode: 2 AbsoluteX size: 3 cycles: 4 page_cycles: 1 94 #5E LSR : mode: 2 AbsoluteX size: 3 cycles: 7 page_cycles: 0 95 #5F SRE : mode: 2 AbsoluteX size: 0 cycles: 7 page_cycles: 0 96 #60 RTS : mode: 6 Implied size: 1 cycles: 6 page_cycles: 0 97 #61 ADC : mode: 7 IndexedIndirect size: 2 cycles: 6 page_cycles: 0 98 #62 KIL : mode: 6 Implied size: 0 cycles: 2 page_cycles: 0 99 #63 RRA : mode: 7 IndexedIndirect size: 0 cycles: 8 page_cycles: 0 100 #64 NOP : mode: 11 ZeroPaged size: 2 cycles: 3 page_cycles: 0 101 #65 ADC : mode: 11 ZeroPaged size: 2 cycles: 3 page_cycles: 0 102 #66 ROR : mode: 11 ZeroPaged size: 2 cycles: 5 page_cycles: 0 103 #67 RRA : mode: 11 ZeroPaged size: 0 cycles: 5 page_cycles: 0 104 #68 PLA : mode: 6 Implied size: 1 cycles: 4 page_cycles: 0 105 #69 ADC : mode: 5 Immediate size: 2 cycles: 2 page_cycles: 0 106 #6A ROR : mode: 4 Accumulator size: 1 cycles: 2 page_cycles: 0 107 #6B ARR : mode: 5 Immediate size: 0 cycles: 2 page_cycles: 0 108 #6C JMP : mode: 8 Indirect size: 3 cycles: 5 page_cycles: 0 109 #6D ADC : mode: 1 Absolute size: 3 cycles: 4 page_cycles: 0 110 #6E ROR : mode: 1 Absolute size: 3 cycles: 6 page_cycles: 0 111 #6F RRA : mode: 1 Absolute size: 0 cycles: 6 page_cycles: 0 112 #70 BVS : mode: 10 Relative size: 2 cycles: 2 page_cycles: 1 113 #71 ADC : mode: 9 IndirectIndexed size: 2 cycles: 5 page_cycles: 1 114 #72 KIL : mode: 6 Implied size: 0 cycles: 2 page_cycles: 0 115 #73 RRA : mode: 9 IndirectIndexed size: 0 cycles: 8 page_cycles: 0 116 #74 NOP : mode: 12 ZeroPagedX size: 2 cycles: 4 page_cycles: 0 117 #75 ADC : mode: 12 ZeroPagedX size: 2 cycles: 4 page_cycles: 0 118 #76 ROR : mode: 12 ZeroPagedX size: 2 cycles: 6 page_cycles: 0 119 #77 RRA : mode: 12 ZeroPagedX size: 0 cycles: 6 page_cycles: 0 120 #78 SEI : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 121 #79 ADC : mode: 3 AbsoluteY size: 3 cycles: 4 page_cycles: 1 122 #7A NOP : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 123 #7B RRA : mode: 3 AbsoluteY size: 0 cycles: 7 page_cycles: 0 124 #7C NOP : mode: 2 AbsoluteX size: 3 cycles: 4 page_cycles: 1 125 #7D ADC : mode: 2 AbsoluteX size: 3 cycles: 4 page_cycles: 1 126 #7E ROR : mode: 2 AbsoluteX size: 3 cycles: 7 page_cycles: 0 127 #7F RRA : mode: 2 AbsoluteX size: 0 cycles: 7 page_cycles: 0 128 #80 NOP : mode: 5 Immediate size: 2 cycles: 2 page_cycles: 0 129 #81 STA : mode: 7 IndexedIndirect size: 2 cycles: 6 page_cycles: 0 130 #82 NOP : mode: 5 Immediate size: 0 cycles: 2 page_cycles: 0 131 #83 SAX : mode: 7 IndexedIndirect size: 0 cycles: 6 page_cycles: 0 132 #84 STY : mode: 11 ZeroPaged size: 2 cycles: 3 page_cycles: 0 133 #85 STA : mode: 11 ZeroPaged size: 2 cycles: 3 page_cycles: 0 134 #86 STX : mode: 11 ZeroPaged size: 2 cycles: 3 page_cycles: 0 135 #87 SAX : mode: 11 ZeroPaged size: 0 cycles: 3 page_cycles: 0 136 #88 DEY : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 137 #89 NOP : mode: 5 Immediate size: 0 cycles: 2 page_cycles: 0 138 #8A TXA : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 139 #8B XAA : mode: 5 Immediate size: 0 cycles: 2 page_cycles: 0 140 #8C STY : mode: 1 Absolute size: 3 cycles: 4 page_cycles: 0 141 #8D STA : mode: 1 Absolute size: 3 cycles: 4 page_cycles: 0 142 #8E STX : mode: 1 Absolute size: 3 cycles: 4 page_cycles: 0 143 #8F SAX : mode: 1 Absolute size: 0 cycles: 4 page_cycles: 0 144 #90 BCC : mode: 10 Relative size: 2 cycles: 2 page_cycles: 1 145 #91 STA : mode: 9 IndirectIndexed size: 2 cycles: 6 page_cycles: 0 146 #92 KIL : mode: 6 Implied size: 0 cycles: 2 page_cycles: 0 147 #93 AHX : mode: 9 IndirectIndexed size: 0 cycles: 6 page_cycles: 0 148 #94 STY : mode: 12 ZeroPagedX size: 2 cycles: 4 page_cycles: 0 149 #95 STA : mode: 12 ZeroPagedX size: 2 cycles: 4 page_cycles: 0 150 #96 STX : mode: 13 ZeroPagedY size: 2 cycles: 4 page_cycles: 0 151 #97 SAX : mode: 13 ZeroPagedY size: 0 cycles: 4 page_cycles: 0 152 #98 TYA : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 153 #99 STA : mode: 3 AbsoluteY size: 3 cycles: 5 page_cycles: 0 154 #9A TXS : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 155 #9B TAS : mode: 3 AbsoluteY size: 0 cycles: 5 page_cycles: 0 156 #9C SHY : mode: 2 AbsoluteX size: 0 cycles: 5 page_cycles: 0 157 #9D STA : mode: 2 AbsoluteX size: 3 cycles: 5 page_cycles: 0 158 #9E SHX : mode: 3 AbsoluteY size: 0 cycles: 5 page_cycles: 0 159 #9F AHX : mode: 3 AbsoluteY size: 0 cycles: 5 page_cycles: 0 160 #A0 LDY : mode: 5 Immediate size: 2 cycles: 2 page_cycles: 0 161 #A1 LDA : mode: 7 IndexedIndirect size: 2 cycles: 6 page_cycles: 0 162 #A2 LDX : mode: 5 Immediate size: 2 cycles: 2 page_cycles: 0 163 #A3 LAX : mode: 7 IndexedIndirect size: 0 cycles: 6 page_cycles: 0 164 #A4 LDY : mode: 11 ZeroPaged size: 2 cycles: 3 page_cycles: 0 165 #A5 LDA : mode: 11 ZeroPaged size: 2 cycles: 3 page_cycles: 0 166 #A6 LDX : mode: 11 ZeroPaged size: 2 cycles: 3 page_cycles: 0 167 #A7 LAX : mode: 11 ZeroPaged size: 0 cycles: 3 page_cycles: 0 168 #A8 TAY : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 169 #A9 LDA : mode: 5 Immediate size: 2 cycles: 2 page_cycles: 0 170 #AA TAX : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 171 #AB LAX : mode: 5 Immediate size: 0 cycles: 2 page_cycles: 0 172 #AC LDY : mode: 1 Absolute size: 3 cycles: 4 page_cycles: 0 173 #AD LDA : mode: 1 Absolute size: 3 cycles: 4 page_cycles: 0 174 #AE LDX : mode: 1 Absolute size: 3 cycles: 4 page_cycles: 0 175 #AF LAX : mode: 1 Absolute size: 0 cycles: 4 page_cycles: 0 176 #B0 BCS : mode: 10 Relative size: 2 cycles: 2 page_cycles: 1 177 #B1 LDA : mode: 9 IndirectIndexed size: 2 cycles: 5 page_cycles: 1 178 #B2 KIL : mode: 6 Implied size: 0 cycles: 2 page_cycles: 0 179 #B3 LAX : mode: 9 IndirectIndexed size: 0 cycles: 5 page_cycles: 1 180 #B4 LDY : mode: 12 ZeroPagedX size: 2 cycles: 4 page_cycles: 0 181 #B5 LDA : mode: 12 ZeroPagedX size: 2 cycles: 4 page_cycles: 0 182 #B6 LDX : mode: 13 ZeroPagedY size: 2 cycles: 4 page_cycles: 0 183 #B7 LAX : mode: 13 ZeroPagedY size: 0 cycles: 4 page_cycles: 0 184 #B8 CLV : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 185 #B9 LDA : mode: 3 AbsoluteY size: 3 cycles: 4 page_cycles: 1 186 #BA TSX : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 187 #BB LAS : mode: 3 AbsoluteY size: 0 cycles: 4 page_cycles: 1 188 #BC LDY : mode: 2 AbsoluteX size: 3 cycles: 4 page_cycles: 1 189 #BD LDA : mode: 2 AbsoluteX size: 3 cycles: 4 page_cycles: 1 190 #BE LDX : mode: 3 AbsoluteY size: 3 cycles: 4 page_cycles: 1 191 #BF LAX : mode: 3 AbsoluteY size: 0 cycles: 4 page_cycles: 1 192 #C0 CPY : mode: 5 Immediate size: 2 cycles: 2 page_cycles: 0 193 #C1 CMP : mode: 7 IndexedIndirect size: 2 cycles: 6 page_cycles: 0 194 #C2 NOP : mode: 5 Immediate size: 0 cycles: 2 page_cycles: 0 195 #C3 DCP : mode: 7 IndexedIndirect size: 0 cycles: 8 page_cycles: 0 196 #C4 CPY : mode: 11 ZeroPaged size: 2 cycles: 3 page_cycles: 0 197 #C5 CMP : mode: 11 ZeroPaged size: 2 cycles: 3 page_cycles: 0 198 #C6 DEC : mode: 11 ZeroPaged size: 2 cycles: 5 page_cycles: 0 199 #C7 DCP : mode: 11 ZeroPaged size: 0 cycles: 5 page_cycles: 0 200 #C8 INY : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 201 #C9 CMP : mode: 5 Immediate size: 2 cycles: 2 page_cycles: 0 202 #CA DEX : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 203 #CB AXS : mode: 5 Immediate size: 0 cycles: 2 page_cycles: 0 204 #CC CPY : mode: 1 Absolute size: 3 cycles: 4 page_cycles: 0 205 #CD CMP : mode: 1 Absolute size: 3 cycles: 4 page_cycles: 0 206 #CE DEC : mode: 1 Absolute size: 3 cycles: 6 page_cycles: 0 207 #CF DCP : mode: 1 Absolute size: 0 cycles: 6 page_cycles: 0 208 #D0 BNE : mode: 10 Relative size: 2 cycles: 2 page_cycles: 1 209 #D1 CMP : mode: 9 IndirectIndexed size: 2 cycles: 5 page_cycles: 1 210 #D2 KIL : mode: 6 Implied size: 0 cycles: 2 page_cycles: 0 211 #D3 DCP : mode: 9 IndirectIndexed size: 0 cycles: 8 page_cycles: 0 212 #D4 NOP : mode: 12 ZeroPagedX size: 2 cycles: 4 page_cycles: 0 213 #D5 CMP : mode: 12 ZeroPagedX size: 2 cycles: 4 page_cycles: 0 214 #D6 DEC : mode: 12 ZeroPagedX size: 2 cycles: 6 page_cycles: 0 215 #D7 DCP : mode: 12 ZeroPagedX size: 0 cycles: 6 page_cycles: 0 216 #D8 CLD : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 217 #D9 CMP : mode: 3 AbsoluteY size: 3 cycles: 4 page_cycles: 1 218 #DA NOP : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 219 #DB DCP : mode: 3 AbsoluteY size: 0 cycles: 7 page_cycles: 0 220 #DC NOP : mode: 2 AbsoluteX size: 3 cycles: 4 page_cycles: 1 221 #DD CMP : mode: 2 AbsoluteX size: 3 cycles: 4 page_cycles: 1 222 #DE DEC : mode: 2 AbsoluteX size: 3 cycles: 7 page_cycles: 0 223 #DF DCP : mode: 2 AbsoluteX size: 0 cycles: 7 page_cycles: 0 224 #E0 CPX : mode: 5 Immediate size: 2 cycles: 2 page_cycles: 0 225 #E1 SBC : mode: 7 IndexedIndirect size: 2 cycles: 6 page_cycles: 0 226 #E2 NOP : mode: 5 Immediate size: 0 cycles: 2 page_cycles: 0 227 #E3 ISC : mode: 7 IndexedIndirect size: 0 cycles: 8 page_cycles: 0 228 #E4 CPX : mode: 11 ZeroPaged size: 2 cycles: 3 page_cycles: 0 229 #E5 SBC : mode: 11 ZeroPaged size: 2 cycles: 3 page_cycles: 0 230 #E6 INC : mode: 11 ZeroPaged size: 2 cycles: 5 page_cycles: 0 231 #E7 ISC : mode: 11 ZeroPaged size: 0 cycles: 5 page_cycles: 0 232 #E8 INX : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 233 #E9 SBC : mode: 5 Immediate size: 2 cycles: 2 page_cycles: 0 234 #EA NOP : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 235 #EB SBC : mode: 5 Immediate size: 0 cycles: 2 page_cycles: 0 236 #EC CPX : mode: 1 Absolute size: 3 cycles: 4 page_cycles: 0 237 #ED SBC : mode: 1 Absolute size: 3 cycles: 4 page_cycles: 0 238 #EE INC : mode: 1 Absolute size: 3 cycles: 6 page_cycles: 0 239 #EF ISC : mode: 1 Absolute size: 0 cycles: 6 page_cycles: 0 240 #F0 BEQ : mode: 10 Relative size: 2 cycles: 2 page_cycles: 1 241 #F1 SBC : mode: 9 IndirectIndexed size: 2 cycles: 5 page_cycles: 1 242 #F2 KIL : mode: 6 Implied size: 0 cycles: 2 page_cycles: 0 243 #F3 ISC : mode: 9 IndirectIndexed size: 0 cycles: 8 page_cycles: 0 244 #F4 NOP : mode: 12 ZeroPagedX size: 2 cycles: 4 page_cycles: 0 245 #F5 SBC : mode: 12 ZeroPagedX size: 2 cycles: 4 page_cycles: 0 246 #F6 INC : mode: 12 ZeroPagedX size: 2 cycles: 6 page_cycles: 0 247 #F7 ISC : mode: 12 ZeroPagedX size: 0 cycles: 6 page_cycles: 0 248 #F8 SED : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 249 #F9 SBC : mode: 3 AbsoluteY size: 3 cycles: 4 page_cycles: 1 250 #FA NOP : mode: 6 Implied size: 1 cycles: 2 page_cycles: 0 251 #FB ISC : mode: 3 AbsoluteY size: 0 cycles: 7 page_cycles: 0 252 #FC NOP : mode: 2 AbsoluteX size: 3 cycles: 4 page_cycles: 1 253 #FD SBC : mode: 2 AbsoluteX size: 3 cycles: 4 page_cycles: 1 254 #FE INC : mode: 2 AbsoluteX size: 3 cycles: 7 page_cycles: 0 255 #FF ISC : mode: 2 AbsoluteX size: 0 cycles: 7 page_cycles: 0 ================================================ FILE: docs/cpu/reset_readme.txt ================================================ CPU Power/Reset Tests --------------------- Verifies CPU register values at power, and changes that occur during reset. Also verifies that RAM isn't modified during reset. Expected behavior ----------------- At power: A, X, Y = 0 P = $34 S = $FD After reset: A, X, Y unchanged I flag set (P ORed with $04) S decremented by 3, but nothing written to stack Multi-tests ----------- The NES/NSF builds in the main directory consist of multiple sub-tests. When run, they list the subtests as they are run. The final result code refers to the first sub-test that failed. For more information about any failed subtests, run them individually from rom_singles/ and nsf_singles/. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. -- Shay Green ================================================ FILE: docs/genie_codes ================================================ Castlevania: SZSVLYSA : Infinite Health AVEEZZSA : Infinite Energy OXNGLZVK : Infinite Lives ASOGOPIA : Start w/ 80 Hearts AANGSAGE AANKXPGE : Start w/ 8 Lives KXESUZKA KZSSEZKA : Weapons don't use Hearts Castlevania III: OXEEZZSE : Infinite Health OXOAUPSE : Infinite Lives OOKPPAIE : Start stage w/ 99 Hearts Donkey Kong: SXNGOZVG : Infinite Lives Back to the Future II/III: SXXELOVK : Infinite Lives GZEEPZST GZOEZZST : Infinite Fuel ================================================ FILE: docs/mapper/000.txt ================================================ ======================== = Mapper 000 = ======================== aka -------------------------- NROM "no mapper" Example Games: -------------------------- Ice Climber Excitebike Balloon Fight Super Mario Bros. Notes: -------------------------- No swapping of any kind. All slots fixed, mirroring is hardwired, etc. ================================================ FILE: docs/mapper/001.txt ================================================ ======================== = Mapper 001 = ======================== aka -------------------------- MMC1 SxROM Example Games: -------------------------- Final Fantasy Mega Man 2 Blaster Master Metroid Kid Icarus Zelda Zelda 2 Castlevania 2 Notes: --------------------------- MMC1 is unique in that not only must the registers be written to *one bit at a time*, but also you cannot write to the registers directly. Internal registers are 5 bits wide. Meaning to complete a "full" write, games must write to a register 5 times (low bit first). This is usually accomplished with something like the following: LDA value_to_write STA $9FFF ; 1st bit written LSR A STA $9FFF ; 2nd bit written LSR A STA $9FFF ; 3rd bit written LSR A STA $9FFF ; 4th bit written LSR A STA $9FFF ; final 5th bit written -- full write is complete Writing to anywhere in $8000-FFFF will do -- however the address you write to on the last of the 5 writes will determine which internal register gets filled. The address written to for the first 4 writes *does not matter at all*... though games generally write to the same address anyway (like in the above example). To illustrate this: LDA #$00 ; we want to write 0 to a reg STA $8000 STA $8000 STA $8000 STA $8000 ; first 4 writes go to $8000 STA $E000 ; 5th write goes to $E000 The above code will affects reg $E000 only!!! Despite $8000 being written to several times, reg $8000 remains totally unchanged! How this works is that when the game writes to $8000-FFFF, it goes to a hidden temporary register. That register records the bits being written. Only after all 5 bits are written does the final 5-bit value move to the desired *actual* register. Only bits 7 and 0 are significant when writing to a register: Temporary reg port ($8000-FFFF): [r... ...d] r = reset flag d = data bit When 'r' is set: - 'd' is ignored - hidden temporary reg is reset (so that the next write is the "first" write) - bits 2,3 of reg $8000 are set (16k PRG mode, $8000 swappable) - other bits of $8000 (and other regs) are unchanged When 'r' is clear: - 'd' proceeds as the next bit written in the 5-bit sequence - If this completes the 5-bit sequence: - temporary reg is copied to actual internal reg (which reg depends on the last address written to) - temporary reg is reset (so that next write is the "first" write) Confusing? Yeah it looks confusing, but isn't really. For an example: LDA #$00 STA $8000 ; 1st write ('r' bit is clear) STA $8000 ; 2nd write LDA #$80 STA $8000 ; reset ('r' bit is set) LDA #$00 STA $8000 ; 1st write (not 3rd!) Variants: -------------------------- There are also a slew of board variations which are assigned to mapper 001 as well. See the sections at the bottom for details. Determining which variant a game uses is difficult -- likely you'll need to fall back to a CRC or hash check. Registers: -------------------------- Note again, these registers are internal and are not accessed directly! Read notes above. $8000-9FFF: [...C PSMM] C = CHR Mode (0=8k mode, 1=4k mode) P = PRG Size (0=32k mode, 1=16k mode) S = Slot select: 0 = $C000 swappable, $8000 fixed to page $00 (mode A) 1 = $8000 swappable, $C000 fixed to page $0F (mode B) This bit is ignored when 'P' is clear (32k mode) M = Mirroring control: %00 = 1ScA %01 = 1ScB %10 = Vert %11 = Horz $A000-BFFF: [...C CCCC] CHR Reg 0 $C000-DFFF: [...C CCCC] CHR Reg 1 $E000-FFFF: [...W PPPP] W = WRAM Disable (0=enabled, 1=disabled) P = PRG Reg Disabled WRAM cannot be read or written. Earlier MMC1 versions apparently do not have this bit implemented. Later ones do. CHR Setup: -------------------------- There are 2 CHR regs and 2 CHR modes. $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------------------------------------------------------+ C=0: | <$A000> | +---------------------------------------------------------------+ C=1: | $A000 | $C000 | +-------------------------------+-------------------------------+ PRG Setup: -------------------------- There is 1 PRG reg and 3 PRG modes. $8000 $A000 $C000 $E000 +-------------------------------+ P=0: | <$E000> | +-------------------------------+ P=1, S=0: | { 0 } | $E000 | +---------------+---------------+ P=1, S=1: | $E000 | {$0F} | +---------------+---------------+ On Powerup: ---------------------------- This varies from version to version. Earlier MMC1 versions have no determined startup state. Later ones do. - bits 2,3 of $8000 are set (16k PRG mode, $8000 swappable) WRAM Disable varies wildly from version to version. Some versions don't have it at all, other versions have it cleared initially, others have it set initially, and others have it random. To be "safe", when homebrewing, assume it's disabled (and have your game explicitly enable it before accessing WRAM), and when emudeving, assume it's enabled at startup (or else some early MMC1 games will break in your emu). Additional Notes: ---------------------------- Consecutive writes that are too close together are apparently ignored. One game where this is significant is Bill & Ted's Excellent Video Game Adventure. That game does the following HORRIBLY SLOPPY code to reset the mapper: INC $FFFF (where $FFFF contains $FF when read) For those of you who really know your 6502... you know that this will read $FFFF (getting $FF), write that value ($FF) back to $FFFF, increment it by one, then write the new value ($00) to $FFFF. This results in two register writes: $FF, then $00. Normally, such writes would reset the mapper, then write a single data bit. However if your emu does it like that, the game will crash, as the game expects the next write to be the 1st in a 5-bit sequence (and your emu will treat it like the 2nd). However these writes are performed on consecutive CPU cycles -- which apparently are too close to each other. As such, only the first write (of $FF) is acknowledged and performed, and the second write (of $00) is ignored. Emulating in this manner results in a fully functioning game. So while it is unsure exactly how far apart the writes must be, you can assume that the distance between them must be at least 2 CPU cycles. Such that Read/Modify/Write instructions (like INC) will only acknowledge the first write, but two consecutive write instructions (like 2 side-by-side STA's) will work normally. ----------------------------------------- ----------------------------------------- Special Variant -- SUROM: -------------------------- Example Games: Dragon Warrior 4 Dragon Quest 4 The MMC1 PRG reg is only 4 bits wide. This means that normally, page $0F is the highest page number you can access. With 16k pages... this limits typical MMC1 to 256k PRG ($10 pages * $4000 per page). SUROM "hijacks" one of the bits from the CHR registers and uses it as an additional PRG bit. This allows for access to $1F pages, allowing 512k PRG. $A000-BFFF: [...C CCCC] CHR reg 0 [...P ....] hijacked PRG bit $C000-DFFF: [...C CCCC] CHR reg 1 [...P ....] hijacked PRG bit When in 4k CHR mode, 'P' in both $A000 and $C000 *must* be set to the same value, or else pages will constantly be swapped as graphics render! In 8k CHR mode (which is what DQ4 uses), $C000 is irrelevant since it is ignored, and $A000 is used exclusively. The hijacked PRG bit selects which 256k block is used for *ALL* PRG... *including* fixed pages. Meaning fixed page $0F @ $C000 can swap between page $0F and $1F. Special Variant -- SOROM: -------------------------- Example Games: Nobunaga's Ambition Romance of the Three Kingdoms Genghis Khan SOROM has 16k PRG-RAM (instead of the typical 8k), and hijacks unused bits from the CHR regs in order to select which 8k PRG-RAM page is at $6000-7FFF. The first 8k of PRG-RAM (page 0) is not battery backed -- but the second 8k is. When in 4k CHR Mode: $A000-BFFF: [.... R..C] R = PRG-RAM page select C = CHR reg 0 $C000-DFFF: [.... R..C] R = PRG-RAM page select C = CHR reg 1 In 4k CHR mode, above 'R' bits MUST be set to the same value or else PRG-RAM will automatically swap as the PPU fetches tiles to render! When in 8k mode: $A000-BFFF: [.... R...] PRG-RAM page select $C000-DFFF: [.... ....] Unused Special Variant -- SXROM: -------------------------- Example Games: Final Fantasy 1 & 2 (the combo cart, not the individual games) Best Play Pro Yakyuu Special SXROM is sort of like a combination of SUROM and SOROM. It uses bits from CHR regs to have an additional PRG bit, and also to have swappable PRG-RAM. SXROM has a whopping 32k PRG-RAM (all of which can be battery backed). When in 8k CHR mode: $A000-BFFF: [...P RR..] P = PRG-ROM 256k block select (just like on SUROM) R = PRG-RAM page select (selects 8k @ $6000-7FFF, just like SOROM) The behaviour when in 4k CHR mode is similar to SUROM, in that the registers must be identical or else undesired swapping will occur as the PPU renders. ================================================ FILE: docs/mapper/002.txt ================================================ ======================== = Mapper 002 = ======================== aka -------------------------- UxROM (and compatible) Example Games: -------------------------- Mega Man Castlevania Contra Duck Tales Metal Gear Notes: --------------------------- UxROM has bus conflicts, however mapper 002 is meant to be UxROM and compatible. So some mappers which were similar in function, but did not have bus conflicts are included. Additionally, UxROM does not have an 8 bit reg. UNROM is capped at 128k PRG, and UOROM is capped at 256k. So to be "safe": - for homebrewing: assume bus conflicts, do not exceed 256k - for emudev: assume no bus conflicts, use all 8 PRG reg bits There is no CHR swapping. Every mapper 002 game I've ever seen has CHR-RAM. Registers (**BUS CONFLICTS** sometimes): -------------------------- $8000-FFFF: [PPPP PPPP] PRG Reg PRG Setup: -------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | $8000 | { -1} | +---------------+---------------+ ================================================ FILE: docs/mapper/003.txt ================================================ ======================== = Mapper 003 = ======================== aka -------------------------- CNROM (and compatible) Example Games: -------------------------- Solomon's Key Arkanoid Arkista's Ring Bump 'n' Jump Cybernoid Registers (**BUS CONFLICTS** sometimes): -------------------------- $8000-FFFF: [CCCC CCCC] CHR Reg (selects 8k @ $0000) Notes: --------------------------- CNROM has bus conflicts, however mapper 003 is meant to be CNROM and compatible. So some mappers which were similar in function, but did not have bus conflicts are included. Additionally, CNROM's reg is only 2 bits wide... therefore it is capped at 32k CHR. So to be "safe": - for homebrewing: assume bus conflicts, do not exceed 32k CHR - for emudev: assume no bus conflicts, use all 8 reg bits There is no PRG swapping. The game Cybernoid seems to behave very strangely. It uses unprepped system RAM... and it is as if it actually relies on bus conflicts (AND written value with value read from address)! ================================================ FILE: docs/mapper/004.txt ================================================ ======================== = Mapper 004 = ======================== aka -------------------------- MMC3 TxROM (MMC6) (HxROM) Example Games: -------------------------- Mega Man 3, 4, 5, 6 Kirby's Adventure Gauntlet Rad Racer 2 Startropics 1, 2 (MMC6) Super Mario Bros. 2, 3 ... a zillion other games (most common mapper) 4-Screen Notes: --------------------------- TR1ROM and TVROM are two of the *very few* boards to use 4-screen mirroring. The only mapper 004 games which use 4-screen mirroring I know of are Rad Racer 2 and Gauntlet. Several other games are incorrectly labelled as being 4-screen when they are in fact, not (ex: Gauntlet 2). TR1ROM and TVROM are both configured in a way which uses on-cart WRAM as VRAM for the nametables. However this means the WRAM is not tied to the CPU, therefore they do not have any SRAM/WRAM! So to be "safe": - when homebrewing: choose either 4-screen or WRAM. You can't have both - when emudeving: permanently disable WRAM when 4-screen. This does not break any games. 4-screen mirroring for these boards is hardwired! When in 4-screen mode, your emu must ignore writes to the mirroring reg. Also note that many Rad Racer 2 dumps are floating around which do not indicate it is 4-screen. So if you try that game in your emu and the graphics are screwed, that's the first thing to check. IRQ Notes: --------------------------- IRQ Operation on this mapper is simple at first glance, however its precise operation gets very complex. This mapper is infamously one of the hardest (if not the very hardest) mapper to emulate accurately -- a double-whammy since it's also hands down the most common mapper around. Be sure to read 'Basic IRQ operation' below, and I recommend you seriously consider skimming 'Detailed IRQ operation' as well -- especially if you're emudeving. Other notes: --------------------------- Low G Man will actually confirm that WRAM disabling works properly, and will break if it isn't. So if your emu ignores WRAM disabling and always has it enabled, Low G Man will break (specifically, during the level 1 boss fight) Registers: --------------------------- Range,Mask: $8000-FFFF, $E001 $8000: [CP.. .AAA] C = CHR mode select (see CHR setup) P = PRG mode select (see PRG setup) A = Address for use with $8001 $8001: [DDDD DDDD] -- data port R:0 -> CHR reg 0 R:1 -> CHR reg 1 R:2 -> CHR reg 2 R:3 -> CHR reg 3 R:4 -> CHR reg 4 R:5 -> CHR reg 5 R:6 -> PRG reg 0 R:7 -> PRG reg 1 $A000: [.... ...M] Mirroring: 0=Vert 1=Horz Ignore when 4-screen $A001: [EW.. ....] E = Enable WRAM (0=disabled, 1=enabled) W = WRAM write protect (0=writable, 1=not writable) $C000: [IIII IIII] IRQ Reload value $C001: [.... ....] IRQ Clear $E000: [.... ....] IRQ Acknowledge / Disable $E001: [.... ....] IRQ Enable CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------+---------------+-------+-------+-------+-------+ CHR Mode 0: | | | R:2 | R:3 | R:4 | R:5 | +---------------+---------------+---------------+---------------+ CHR Mode 1: | R:2 | R:3 | R:4 | R:5 | | | +-------+-------+-------+-------+---------------+---------------+ PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ PRG Mode 0: | R:6 | R:7 | { -2} | { -1} | +-------+-------+-------+-------+ PRG Mode 1: | { -2} | R:7 | R:6 | { -1} | +-------+-------+-------+-------+ Basic IRQ Operation --------------------------- MMC3 IRQs utilize a scanline counter. The basic steps to making it work are as follows: 1) Set the desired number of scanlines you want to wait by writing N to $C000 2) Reset the internal IRQ counter by writing any value to $C001 3) Enable IRQs by writing any value to $E001 An IRQ will then fire after N+1 rendered scanlines, at which point, you would write any value to $E000 to acknowledge the IRQ, and disable further IRQs (and possibly repeat the above 3 steps if you want to fire another IRQ this frame). The MMC3 counts scanlines by watching the CHR accesses the NES makes. Therefore in order for this IRQ counter to work properly, the NES must be making the accesses that the MMC3 is expecting. If you have abnormal settings, you will confuse the MMC3 and IRQs will not work properly. Therefore, you must follow these rules: 1) The IRQ counter will only count when the PPU is on (sprites and/or BG enabled). 2) Do not manipulate $2006 or $2007 while using IRQ counter 3) Do not set $C000 to $00 4) BG and Sprites must use opposing pattern tables for CHR. EG: a) if 8x16 sprites, BG must use $0xxx, *ALL* sprites must use $1xxx b) if 8x8 sprites, if BG is using $0xxx, sprites must use $1xxx c) if 8x8 sprites, if BG is using $1xxx, sprites must use $0xxx (slightly abnormal) With settings 'a' and 'b', the IRQ will occur after dot 260. With setting 'c', it will occur after dot 324 of the *previous* scanline. The IRQ Counter consists of several parts: 1) Actual 8-bit IRQ counter (not directly accessable) 2) 8-bit latch, or reload value (reg $C000) 3) IRQ Enable flag 4) IRQ Pending flag IRQ Registers interact with the above parts as follows: reg $C000 - sets IRQ Reload value reg $C001 - sets the actual IRQ counter to 0 (regardless of what value is written) reg $E000 - clears both IRQ Enable flag and IRQ Pending flag reg $E001 - sets IRQ Enable flag Every time the MMC3 detects a scanline, the following IRQ Counter logic is executed. Note this occurs EVEN IF IRQs are disabled (the IRQ counter is always counting): - If IRQ Counter is 0... a) reload IRQ counter with IRQ Reload value - Otherwise... a) Decrement IRQ counter by 1 b) If IRQ counter is now 0 and IRQs are enabled, trigger IRQ Note that 241 scanlines are counted per frame (the 240 rendered scanlines, and the "prerender" scanline). Detailed IRQ Operation --------------------------- MMC3 detects scanlines by watching A12 ($1000) on the PPU bus. Every time a rising edge occurs (transitions from 0->1), and it hasn't been too close to the previous rising edge, the IRQ counter gets clocked. Under *normal* conditions (BG using $0xxx, sprites using $1xxx), A12 will rise exactly 8 times every scanline (once for each sprite CHR fetch). However the 8 rises are so close together that only the first is 'seen'. During rendering and pre-render scanlines the PPU is fetching NT and CHR data from the cart through a series of reads. Each read updates the PPU Address lines (including A12), and each read takes 2 PPU cycles (2 dots). There are 4 reads per tile, and 42 tiles per scanline: - 32 BG tiles - 8 Sprite tiles (for the next scanline) - 2 BG tiles (for the next scanline) Each tile requires 4 reads, each read is 2 dots: dot 0: Name table fetch ($2xxx -- A12 is low) dot 2: Attribute fetch ($2xxx -- A12 is low) dot 4: Low CHR fetch ($0xxx or $1xxx -- A12 is low or high) dot 6: High CHR fetch ($0xxx or $1xxx -- A12 is low or high) If the tile being fetched is using the right-hand pattern table ($1xxx), then A12 goes high on dot 4 of that 8-dot sequence. Otherwise, A12 stays low throughout. This 8-dot sequence is repeated for each tile.. meaning there are 42 opportunities for A12 to rise. These opportunities occur on the following dots: 4, 12, 20, ..., 244, 252 (32 BG tiles) 260, 268, 276, 284, 292, 300, 308, 316 (8 Spr tiles) 324, 332 (2 BG tiles) (You might be able to see now how I came up with those 260, 324 numbers I threw at you earlier) MMC3 seems to ignore rises that are too close together. This is why the 8 sprite fetches will only clock the counter once. Exactly how far apart the rising edges have to be is unknown, but it is somewhere between 14 and 16 dots. So any two consecutive opportunities are too close together (including the most distant 332->4), but any two non-consecutive opportunities will both be acknowledged. Figuring whether the tile is being fetched from $0xxx or $1xxx is usually easy. BG and 8x8 sprites are always fetched from an assigned pattern table (configurable by PPU reg $2000). However, 8x16 sprites can come from either pattern table. So which tile is begin fetched depends on which sprite is being fetched.... which depends on what scanline you're on, and what sprites are found to be in-range on that scanline. For scanlines which contain less than 8 sprites, tile $FF is fetched as a dummy (in 8x16 sprites, this would be from the $1xxx pattern table). This is why, when you have 8x16 sprites, ALL sprites must use the right-hand pattern table. If you have sprites using the left and the right, you'll probably end up having some scanlines where the IRQ counter counts the same scanline multiple times! All depending on which sprites are in-range and when. For example, if there are 4 sprites on the scanline using $0xxx, and 4 using $1xxx, the IRQ counter might count the scanline anywhere from 1 to 4 times! 0,0,0,0,1,1,1,1 <--- all 4 rises consecutive, will only clock once 0,1,0,1,0,1,0,1 <---- all 4 rises nonconsecutive, counter clocked each time! This is also why the IRQ counter isn't clocked when both BG and sprites use the left pattern table (since there is never any rising edge, the MMC3 never detects any scanlines). $2006 and $2007 --------------------------- A game can manually clock the IRQ counter (either on accident, or by design) by manipulating $2006 and $2007. A12 is updated (potentially triggering a rising edge) when the PPU address is updated by these registers. On $2007 reads/writes, and on the second $2006 write. This is why messing with $2006 and $2007 after you prep your IRQ stuff may screw up your IRQs unless you're careful. IRQ Counter priming --------------------------- Some games seem to prime the IRQ counter by repeatedly writing $0000 and $1010 to $2006. This toggles A12, clocking the IRQ counter. It is unknown whether or not this is actually required. Reload value of $00 --------------------------- Different MMC3 versions behave differently when you set $C000 to $00. There are at least two (possibly more) behaviors. These behaviors are mentioned in the readme accompanied with blargg's MMC3 test ROMs, which, if you're emudeving, I highly recommend you pick up. Otherwise, the behavior of having a reload value of $00 is unreliable and/or undesirable, and should be avoided at all costs when homebrewing/hacking. Special Variant -- MMC6: -------------------------- Startropics 1 and 2 are both MMC6 games (not MMC3). However, they are unfortunately assigned the same mapper number, despite being slightly incompatible. There is no simple way to determine MMC3 from MMC6. You'll probably have to use a CRC or hash check or something. For the most part they are the same -- but the big difference is the WRAM. MMC6 has only 1k of WRAM, whereas MMC3 games have 8k. It is also mapped a bit differently, and is enabled/disabled differently from MMC3. MMC6 registers are as follows. All other registers behave just as they do on MMC3: $8000: [CPW. .AAA] C,P,A = Same as on MMC3 W = WRAM Enable (0=disabled, 1=enabled) $A001: [HhLl ....] H,L = Enable WRAM block (0=disabled, 1=enabled) h,l = WRAM block write protect (0=writes disabled, 1=writes enabled) The 1k of WRAM is split into 2 512 byte blocks... one at $7000-71FF and another at $7200-73FF. Each block can be controlled independently through $A001. H,h bits deal with the high block ($7200), and L,l bits deal with the low block ($7000). If only one block is enabled, the disabled block will read back as $00. However if BOTH blocks are disabled, reading either will return open bus. $7000-73FF is mirrored throughout $7400-7FFF. However, $6000-6FFF is always open bus (unmapped). $8000.5, when clear (to disable WRAM), simply sets $A001 to $00 and keeps it there. Writing to $A001 when $8000.5 is clear will have no effect. ================================================ FILE: docs/mapper/005.txt ================================================ ======================== = Mapper 005 = ======================== aka -------------------------- MMC5 ExROM Example Games: -------------------------- Castlevania 3 Just Breed Uncharted Waters Romance of the 3 Kingdoms 2 Laser Invasion Metal Slader Glory Uchuu Keibitai SDF Shin 4 Nin Uchi Mahjong - Yakuman Tengoku Test ROM Notes: --------------------------- - Uchuu Keibitai SDF is the only known game to use split screen mode (during the intro, where it shows ship stats) - Shin 4 Nin Uchi Mahjong uses the extra PCM channel ($5011) as well as the other extra sound - Uncharted Waters does PRG-RAM swapping - Just Breed uses ExAttribute mode everywhere, as well as the extra sound. - Bandit Kings of Ancient China writes PRG-RAM through the $8000+ ROM area. Failure to emulate this causes corruption when the background is restored on the world map. General Notes: --------------------------- MMC5 is the infamous juggernaut mapper. It does a whole slew of neat tricks, making it far more powerful than any other mapper around. Though despite its apparent complexity, it's surprisingly straightforward to emulate (that doesn't mean it's easy, though). It's a shame that the only real games to use this mapper were a ton of really, really terrible Koei strategy games. Such a waste. RAM Notes: ---------------------------- MMC5 can address up to 64k PRG-RAM! This is significantly more than the usual 8k. When emulating, it's easiest just to give MMC5 games a full 64k, since the header doesn't really provide a decent way to indicate how much PRG-RAM actually exists. In addition to PRG-RAM, the MMC5 itself has a full 1k of 'ExRAM' which can be accessed by both the CPU and PPU. This ExRAM can be used for many things... from plain vanilla WRAM, to an extra nametable, to a seperate split screen, to extending normal attribute tables. This document's organization: --------------------------- Since there are so many registers for this mapper, and it has so many features, registers will be listed and outlined as the features are explained... and the overall registers section will be extremely brief -- serving primarily as a very quick reference or checklist. Misc Modes and Setup: --------------------------- $5102: [.... ..AA] PRG-RAM Protect A $5103: [.... ..BB] PRG-RAM Protect B To allow writing to PRG-RAM you must set these regs to the following values: A=%10 B=%01 Any other values will prevent PRG-RAM writing. $5104: [.... ..XX] ExRAM mode %00 = Extra Nametable mode ("Ex0") %01 = Extended Attribute mode ("Ex1") %10 = CPU access mode ("Ex2") %11 = CPU read-only mode ("Ex3") CHR Setup: --------------------------- The MMC5 has two sets of CHR regs. One set is used for sprites, the other is used for BG. The MMC5 carefully watches what tiles are being fetched and when (or has some other way of syncing with the NES somehow), which allows it to tell when the NES is fetching BG tiles, and when it's fetching sprite tiles. As such, it can use different regs accordingly, allowing games to basically have 12k of CHR "active" at once instead of the usual 8k! This means you can have a full 512 tiles exclusively for sprites, and have an additional 256 tiles for the BG! CHR Mode Select Reg: $5101: [.... ..CC] %00 = 8k Mode %01 = 4k Mode %10 = 2k Mode %11 = 1k Mode 'High' CHR Reg: $5130 [.... ..HH] (see below) 'A' Regs: $5120 - $5127 'B' Regs: $5128 - $512B When in 8x16 sprite mode, both sets of registers are used. The 'A' set is used for sprite tiles, and the 'B' set is used for BG. This makes it so that sprites can have a full 8k of CHR available, without having to share any of the tiles with the BG (since the BG uses its own 4k of CHR, designated by the 'B' set). It is unsure what you will get when reading CHR via $2007. When in 8x8 sprite mode, only one set is used for both BG and sprites. Either 'A' or 'B', depending on which set is written to last. If 'B' is used, $1000-1FFF always mirrors $0000-0FFF (making the 'B' set pretty worthless with 8x8 sprites) 'A' Set (sprites): $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------------------------------------------------------+ C=%00: | $5127 | +---------------------------------------------------------------+ C=%01: | $5123 | $5127 | +-------------------------------+-------------------------------+ C=%10: | $5121 | $5123 | $5125 | $5127 | +---------------+---------------+---------------+---------------+ C=%11: | $5120 | $5121 | $5122 | $5123 | $5124 | $5125 | $5126 | $5127 | +-------+-------+-------+-------+-------+-------+-------+-------+ 'B' Set (BG): $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------------------------------+-------------------------------+ C=%00: | $512B | +-------------------------------+-------------------------------+ C=%01: | $512B | $512B | +-------------------------------+-------------------------------+ C=%10: | $5129 | $512B | $5129 | $512B | +---------------+---------------+---------------+---------------+ C=%11: | $5128 | $5129 | $512A | $512B | $5128 | $5129 | $512A | $512B | +-------+-------+-------+-------+-------+-------+-------+-------+ Note that unlike most other mappers, these CHR pages are in *actual* sizes. IE: when in 4k mode, registers contain 4k page numbers. But when in 2k mode, register contain 2k page numbers. CHR Regs are actually 10 bits wide, not just 8. When you write to the regs, the value written is the low 8 bits, and the high 2 bits are copied from $5130. Example: LDA #$00 STA $5130 ; high bits = 0 LDA #$20 STA $5127 ; $5127 now = $020 LDA #$02 STA $5130 LDA #$41 STA $5123 ; $5123 now = $241 ; and $5127 still = $020 (not $220) $5130 has an additional role in ExAttribute mode. PRG/RAM Setup: --------------------------- $5100: [.... ..PP] PRG Mode Select: %00 = 32k %01 = 16k %10 = 16k+8k %11 = 8k $5113: [.... .PPP] (simplified, but technically inaccurate -- see below) 8k PRG-RAM page @ $6000 $5114-5117: [RPPP PPPP] R = ROM select (0=select RAM, 1=select ROM) **unused in $5117** P = PRG page The high bit allows the game to select between ROM and RAM. This allows the game to put PRG-RAM anywhere between $6000-DFFF (but no higher, since $5117 always selects ROM) Only RAM can be swapped to $6000-7FFF. $5117 always selects ROM, never RAM (ROM always at $E000-FFFF). $6000 $8000 $A000 $C000 $E000 +-------+-------------------------------+ P=%00: | $5113 | <<$5117>> | +-------+-------------------------------+ P=%01: | $5113 | <$5115> | <$5117> | +-------+---------------+-------+-------+ P=%10: | $5113 | <$5115> | $5116 | $5117 | +-------+---------------+-------+-------+ P=%11: | $5113 | $5114 | $5115 | $5116 | $5117 | +-------+-------+-------+-------+-------+ Technically, $5113 should look something like: [.... .CPP] C = Chip select P = 8k PRG-RAM page on selected chip MMC5 can address two seperate RAM chips, each up to 32k in size. This detail can impact how RAM is mirrored across pages if the chip sizes are less than 32k. For example, Uncharted Waters has two 8k chips (only 16k total -- but on two seperate chips), so it uses selects pages $00 and $04, rather than $00 and $01 like you may expect. This is because bit 2 is the chip select, and the 8k on each chip is mirrored to every page on that chip... that is... $00-$03 would all select the first 8k. Note that no commercial games rely on this mirroring -- therefore you can take the easy way out and simply give all MMC5 games 64k PRG-RAM. Mirroring: --------------------------- $5105: [DDCC BBAA] MMC5 allows each NT slot to be configured: [ A ][ B ] [ C ][ D ] Values can be the following: %00 = NES internal NTA %01 = NES internal NTB %10 = use ExRAM as NT %11 = Fill Mode For example... some typical mirroring setups would be: ( D C B A) Horz: $50 (%01 01 00 00) Vert: $44 (%01 00 01 00) 1ScA: $00 (%00 00 00 00) 1ScB: $55 (%01 01 01 01) Fill: $ff (%11 11 11 11) ExRAM can act as a 3rd nametable here... but only in Ex0 or Ex1 (see $5104 above). If in Ex2 or Ex3, the PPU will get $00 when it attempts to read from the nametable. Note that while ExRAM can be used as a nametable in Ex1, it's probably a bad idea, since ExRAM is also used for Extended attributes in that mode. Therefore, when using ExRAM as a nametable, you should stick to Ex0. Fill Mode is a virtual nametable. It is not a full nametable, but rather, as the PPU attempts to read it, the MMC5 will feed it a specific tile -- thus appearing as though there's a full nametable filled with a single tile. The tile can be configured by the game with the following regs: $5106: [TTTT TTTT] Fill Tile $5107: [.... ..AA] Fill Attribute bits Extended Attribute Mode: --------------------------- When in Ex1 mode (see $5104 above), ordinary attribute tables and BG CHR regs are ignored, and instead, each byte in ExRAM coresponds with an onscreen tile, and assigns that tile a 4k CHR page (allowing you to choose from 16k tiles instead of 256) and its own attribute bits (allowing each 8x8 tile to have its own palette, rather than having the normal 16x16 blocks). Bytes in ExRAM: [AACC CCCC] A = Attribute bits C = 4k CHR Page Additionally... $5130 is used directly as the high 2 bits of CHR for every on-screen BG tile when in this mode. It effectively selects a 256k block for BG to use (in addition to its normal use with CHR swapping). $5130's runtime value affects all BG tiles, therefore changing $5130 will immediately swap all on-screen BG when in this mode. Therefore, if/when you change $5130 to swap CHR for sprites, you must write to $5130 again with the desired value for the BG. Sprites are unaffected by this mode and still use the normal CHR regs. Which tile uses which byte in ExRAM depends on its position in the nametable. Scrolling is irrelevent. The tile at $2000 always uses the first byte in ExRAM, $2001 uses the second, etc. $2400, $2800, and $2C00 also use the first byte of ExRAM. CPU Accessing ExRAM: --------------------------- ExRAM can be accessed by the CPU via $5C00-$5FFF. Whether or not you can read or write depends on the current mode (see $5104): Mode Readable Writable ------------------------- Ex0 no * Ex1 no * Ex2 yes yes Ex3 yes no In Ex0 and Ex1, ExRAM can only be written DURING RENDERING (insane, I know). If a game attempts to write outside of rendering, $00 is written instead of the desired value. Writes have absolutely no effect in Ex3. Attempting to read when not readable will return open bus. 8 * 8 -> 16 Multiplier: --------------------------- MMC5 has a nifty multiplier, similar to the SNES's. on write: $5205: multiplicand $5206: multiplier on read: $5205: low 8 bits of product $5206: high 8 bits of product Basic functionality is, you write two values you want multiplied to $5205 and $5206, then read the product back. Multiplication is unsigned. There is no noticable delay -- that is, the product can be read back right after writing. Split Screen: --------------------------- A unique feature to MMC5 is its ability to split the screen vertically down the middle. However due to some limitations that couldn't be avoided, it ended up not being that useful of a feature. Note: Split screen mode is only allowed in Ex0 or Ex1. When in Ex2 and Ex3, it is always disabled. I do not know whether or not the split is affected by Extended Attributes when in Ex1. Judging by the $5202, I would assume it isn't, but that's a total guess. $5200: [ER.T TTTT] Split control E = Enable (0=split mode disabled, 1=split mode enabled) R = Right side (0=split will be on left side, 1=split will be on right) T = tile number to split at $5201: [YYYY YYYY] Split Y scroll $5202: [CCCC CCCC] 4k CHR Page for split 34 BG tiles are fetched per scanline. MMC5 performs the split by watching which BG tile is being fetched, and if it is within the split region, replacing the normal NT data with the split screen data according to the absolute screen position of the tile (i.e., ignoring the coarse horizontal and vertical scroll output as part of the VRAM address for the fetch). Since it operates on a per-tile basis... fine horizontal scrolling "carries into" the split region. Setting the horizontal scroll to 1-7 will result in the split being moved to the left 1-7 pixels, however when you scroll to 8, the split will "snap" back to its normal position. Left Split: Tiles 0 to T-1 are the split. Tiles T and on are rendered normally. Right Split: Tiles 0 to T-1 are rendered normally. Tiles T and on are the split. There is no coarse horizontal scrolling of any kind for the split. Right-side splits will always show the right-hand side of the nametable, and left-hand splits will always show the left-hand side of the nametable. Coarse horizontal scrolling can still be used for the non-split region. ExRAM is always used as the nametable in split screen mode. Vertical scrolling for the split operates like normal vertical scrolling. 0-239 are valid scroll values, whereas 240-255 will display Attribute table data as NT data for the first few scanlines. The split nametable will wrap so that the top of the nametable will appear below as you scroll (just as if vertical mirroring were employed). $5202 selects (yet another) CHR page to use for the BG. This page is used for the split region only. IRQ Operation: --------------------------- MMC5 has a scanline counter for IRQs, however it is significantly more sophisticated than MMC3's, and doesn't suffer from the same restrictions. It is also a bit easier to use. Write: $5203: [IIII IIII] IRQ Target $5204: [E... ....] IRQ Enable (0=disabled, 1=enabled) Read: $5204: [PI.. ....] P = IRQ currently pending I = "In Frame" signal Reading $5204 will clear the pending flag (acknowledging the IRQ). Basic operation: 1) Write the desired scanline number to $5203 2) Enable IRQs by setting $5204.7 IRQ will then trip on the given scanline number (provided PPU rendering is enabled). The only thing to note here is that this behavior changes drastically if you turn the PPU off mid-frame... and that an IRQ will never occur when the target scanline number is 0 or greater than (?or equal to?) $F0. The "In Frame" signal will read back as set when the PPU is rendering (during scanlines 0-239). Though its actual behavior and how it interacts with the IRQ counter is a bit more complex. Detailed Operation: The IRQ counter is an up counter, rather than a down counter (like MMC3). Every time the MMC5 detects a scanline, it does the following: - If In Frame Signal is clear... a) Set In Frame signal b) Reset IRQ counter to 0 c) Clear IRQ pending flag (automatically acknowledging IRQ) - otherwise... a) Increment IRQ counter b) If IRQ counter now equals the trigger value, raise IRQ pending flag Note that the IRQ pending flag is raised *regardless* of whether or not IRQs are enabled. However, this will only trigger an IRQ on the 6502 if both this flag *and* the IRQ enable flag is set. Therefore IRQs must still be enabled for this to have an effect, however the pending flag can still be read back as set via $5204 even when IRQs are disabled. Also note that the IRQ counter is compared after it is incremented. This is why a trigger value of 0 will never trigger an IRQ. At any time when the MMC5 detects that the PPU is inactive, the In Frame signal is automatically cleared. The MMC5 will detect this after rendering for the frame is complete, and as soon as the PPU is turned off via $2001. This is why turning off the PPU mid-frame will disrupt IRQs -- since the In Frame signal being cleared will reset the IRQ counter next scanline. HOW the MMC5 detects scanlines is still unknown. One theory is that it looks for the two dummy nametable fetches at the end of the scanline. Or perhaps it counts the number of fetches the PPU performs. Nobody knows for sure. The IRQ will trip at the *start* of the desired scanline. Or, more precisely, near the very end of the previous scanline (closest I can figure is dot 336). That is... if the trigger line is set to 1, the IRQ will trip on dot 336 of scanline 0. I am unsure whether or not the last rendered scanline (239) is detected by the MMC5. I would assume it is, which would mean a trigger value of $F0 would trip an IRQ at the end of rendering. Trigger values above $F0 will never be reached, since rendering stops before then, and the in-frame signal would automatically clear. Sound: --------------------------- The MMC5 also has 3 additional sound channels! (Will the list of features ever stop?!?!). Unfortunately, due to the NES being dumbed down, these can only be heard on a Famicom (or a modified NES). There are 2 additional Pulse channels, and 1 additional PCM channel. Registers for them are as follows: Write: $5000-5003: Regs for Pulse 1 $5004-5007: Regs for Pulse 2 $5010: PCM read-only mode output (no games use this part of the PCM) $5011: PCM read/write mode output $5015: [.... ..BA] Enable flags for Pulse 1 (A), 2 (B) (0=disable, 1=enable) Read: $5015 [.... ..BA] Length status for Pulse 1 (A), 2 (B) Pulse channels behave identically to the native NES pulse channels, only they lack a sweep unit. Rather than going into details on their function, I recommend you pick up blargg's apu reference. $5000-5007 operate just as $4000-4007 do $5015 operates just as $4015 does (for reads and writes) Nobody knows exactly how the PCM channel of the MMC5 works. The patent documentation is unclear, and no games seem to use it apart from $5011. $5010 likely does *something*... but nobody knows what. $5011 operates exactly like $4011, only it is 8 bits wide instead of 7. Games *do* use this register to output sound. Powerup: --------------------------- Games seem to expect $5117 to be $FF on powerup (last PRG page swapped in). Additionally, Romance of the 3 Kingdoms 2 seems to expect it to be in 8k PRG mode ($5100 = $03). Register Overview: --------------------------- Due to the massive number of registers on this mapper, this section will be brief. Registers were all covered in detail in the sections above -- this is just to recap them all: Writable Regs: $5000-5003: Sound, Pulse 1 $5004-5007: Sound, Pulse 2 $5010-5011: Sound, PCM $5015: Sound, General $5100: PRG Mode Select $5101: CHR Mode Select $5102-5103: PRG-RAM Write protect $5104: ExRAM Mode $5105: Mirroring Mode $5106: Fill Tile $5107: Fill Attribute $5113: PRG-RAM reg $5114-5117: PRG regs $5120-5127: CHR regs 'A' $5128-512B: CHR regs 'B' $5130: CHR high bits $5200: Split Screen control $5201: Split Screen V Scroll $5202: Split Screen CHR Page $5203: IRQ Trigger $5204: IRQ Control $5205-5206: 8*8->16 Multiplier $5C00-5FFF: ExRAM CPU Access Readable Regs: $5015: Sound Status $5204: IRQ Status $5205-5206: 8*8->16 Multiplier Product $5C00-5FFF: ExRAM CPU Access ================================================ FILE: docs/mapper/007.txt ================================================ ======================== = Mapper 007 = ======================== aka -------------------------- AxROM Example Games: -------------------------- Battletoads Time Lord Marble Madness Notes: --------------------------- AMROM and AOROM have bus conflicts, ANROM does not AMROM and ANROM are capped at 128k PRG AOROM is capped at 256k PRG There is no CHR swapping. Every mapper 007 game I've ever seen has CHR-RAM. Registers (**BUS CONFLICTS** sometimes): -------------------------- $8000-FFFF: [...M .PPP] M = Mirroring: 0 = 1ScA 1 = 1ScB P = PRG Reg (only 2 bits wide on AMROM/ANROM) PRG Setup: -------------------------- $8000 $A000 $C000 $E000 +-------------------------------+ | $8000 | +-------------------------------+ ================================================ FILE: docs/mapper/009.txt ================================================ ======================== = Mapper 009 = ======================== aka -------------------------- MMC2 PxROM Example Game: -------------------------- Mike Tyson's Punch Out!! Registers: --------------------------- Range,Mask: $A000-FFFF, $F000 $A000: PRG Reg $B000: CHR Reg 0A $C000: CHR Reg 0B $D000: CHR Reg 1A $E000: CHR Reg 1B $F000: [.... ...M] Mirroring: 0 = Vert 1 = Horz PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | $A000 | { -3} | { -2} | { -1} | +-------+-------+-------+-------+ CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------------------------------+-------------------------------+ | $B000 or $C000 | $D000 or $E000 | +-------------------------------+-------------------------------+ Which reg is used depends on the state of the respective latch. See below. Latch: --------------------------- There are two latches on the MMC2. One associated with the left pattern table ($0xxx) and another associated with right ($1xxx). Each latch operates independently. Whenever tile $FD is fetched, the appropriate latch is cleared, and whenever tile $FE is fetched, the appropriate latch is set. This allows games to do mid-scanline swapping automatically by having $FD and $FE be special marker tiles. When the $0xxx latch is clear, $B000 is used. When set, $C000 is used. When the $1xxx latch is clear, $D000 is used. When set, $E000 is used. The swap occurs after the tile is fetched, not before. So if the latch is clear, and tile $FE is loaded, tile $FE from the first reg will be drawn to the screen, but the next tile drawn will be from the second reg. Latches can be manipulated by hand by reading from the appropriate PPU address ($0FDx, $0FEx, $1FDx, $1FEx) via $2007. ================================================ FILE: docs/mapper/010.txt ================================================ ======================== = Mapper 010 = ======================== aka -------------------------- MMC4 Example Game: -------------------------- Fire Emblem Notes: -------------------------- This mapper is identical to MMC2 (mapper 009) with the exception of the PRG setup. See Mapper 009 for details. PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | $A000 | { -1} | +---------------+---------------+ ================================================ FILE: docs/mapper/011.txt ================================================ ======================== = Mapper 011 = ======================== Example Games: -------------------------- Crystal Mines Metal Fighter Notes: -------------------------- This mapper suffers from bus conflicts! Registers **BUS CONFLICTS**: -------------------------- $8000-FFFF: [CCCC LLPP] P = Select 32k PRG page @ $8000-FFFF L = Lockout defeat usage C = Select 8k CHR page @ $0000-1FFF Lockout defeat: -------------------------- I have no idea how this works. Kevtris page makes mention of it. From an emulation standpoint, it's not all that important. ================================================ FILE: docs/mapper/013.txt ================================================ ======================== = Mapper 013 = ======================== aka: -------------------------- CPROM Example Game: -------------------------- Videomation Notes: -------------------------- This mapper uses 16k of CHR-RAM. CHR-RAM is swappable. It also has bus conflicts! Registers (**BUS CONFLICTS**): -------------------------- $8000-FFFF: [.... ..CC] C = Select 4k CHR (RAM) page @ $1000-1FFF CHR Setup: -------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------------------------------+-------------------------------+ | { 0 } | $8000 | +-------------------------------+-------------------------------+ ================================================ FILE: docs/mapper/015.txt ================================================ ======================== = Mapper 015 = ======================== Example Game: -------------------------- 100-in-1 Contra Function 16 Notes: --------------------------- Possible bus conflicts??? Registers: --------------------------- $8000-FFFF: A~[.... .... .... ..OO] [pMPP PPPP] O = Mode p = Low bit of PRG page (not always used) P = High bits of PRG page M = Mirroring (0=Vert, 1=Horz) PRG Setup: --------------------------- Depending on the Mode used, the 'p' bit may not be used. In the chart below, "P" will indicate that only the 'P' bits are used to form a 6-bit page number... whereas "Pp" will indicate the full 7-bit page number. $8000 $A000 $C000 $E000 +---------------+---------------+ Mode 0: | P | P OR 1 | +---------------+---------------+ Mode 1: | P | { -1} | +---------------+---------------+ Mode 2: | Pp | Pp | Pp | Pp | +---------------+---------------+ Mode 3: | P | P | +---------------+---------------+ Powerup: --------------------------- All regs reset to 0 on powerup. ================================================ FILE: docs/mapper/016.txt ================================================ ======================== = Mapper 016 = = + 159 = ======================== aka -------------------------- Bandai (something or other) Example Games: -------------------------- Dragon Ball - Dai Maou Jukkatsu (016) Dragon Ball Z Gaiden (016) Dragon Ball Z 2 (016) Rokudenashi Blues (016) Akuma-kun - Makai no Wana (016) Dragon Ball Z - Kyoushuu! Saiya Jin (159) SD Gundam Gaiden (159) Magical Taruruuto Kun 1, 2 (159) Two Mappers: --------------------------- 016 and 159 are mapped the exact same way. Registers are all the same and whatnot. And in fact, for a while, both mappers were assigned the same mapper number (016). Therefore, you may come across mapper 159 games that are still marked as mapper 016. The difference between the two is in the EPROM. These mappers don't have traditional SRAM (I couldn't tell you why). Instead, they have EPROM that has to be written to one bit at a time, with very strange register writes. Mapper 016 has 256 bytes of EPROM, and is accessed high bit first Mapper 159 has 128 bytes of EPROM, and is accessed low bit first For further details, see the section at the bottom. Apart from EPROM, the mappers are 100% identical in function. Notes: --------------------------- Since there's EPROM, there's no SRAM (EPROM is used to save games). Registers: --------------------------- Range,Mask: $6000-FFFF, $000F Note: below regs are listed as $800x, but note they also exist at $6000-7FFF $8000-8007: CHR Regs $8008: PRG Reg (16k @ $8000) $8009: [.... ..MM] Mirroring: %00 = Vert %01 = Horz %10 = 1ScA %11 = 1ScB $800A: [.... ...E] IRQ Enable (0=disabled) $800B: Low 8 bits of IRQ Counter $800C: High 8 bits of IRQ Counter $800D: EPROM I/O another note: since PRG is mapped to $8000-FFFF, EPROM I/O reg can only be read via $6xxx or $7xxx. To my knowledge no other registers are readable. It also appears that reading from *ANY* address in $6xxx-7xxx will read the EPROM I/O reg. Rokudenashi Blues will poll $7F00 and will wait for bit 4 to be 0 before continuing (so if you're giving open bus @ 7F00, the game will deadlock) CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------+-------+-------+-------+-------+-------+-------+-------+ | $8000 | $8001 | $8002 | $8003 | $8004 | $8005 | $8006 | $8007 | +-------+-------+-------+-------+-------+-------+-------+-------+ PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | $8008 | { -1} | +---------------+---------------+ IRQs: --------------------------- IRQs are nice and simple. When enabled, the 16-bit IRQ counter counts down every CPU cycle, wrapping from $0000->FFFF. When the counter makes the transition from $0001->$0000, an IRQ is generated. When disabled, the IRQ counter does not count. Any write to $800A will acknowledge the IRQ $800B and $800C change the IRQ counter directly -- not a reload value. EPROM: --------------------------- EPROM is a real nightmare. Nobody knows for sure exactly how it works -- but by examining the game code, patterns surface. Games do a series of extremely cryptic writes to $800D, and occasionally read a single bit from $800D. By examining some logs I made of the games I've noticed a small bit of patterns which I list below, along with my guess as to what the game is attempting to do by performing that pattern: write $00 write $40 write $60 Start I/O write $20 write $00 write $00 write $20 Output '0' bit write $00 write $00 write $40 write $60 Output '1' bit write $40 write $00 write $00 write $20 write $A0 I have absolutly no clue Read write $00 write $60 write $E0 Read a single bit Read write $40 write $00 write $20 write $60 Stop I/O write $40 write $C0 These likely aren't the only patterns that games perform. I recall seeing occasional writes of $80 and other stuff thrown in there in some games. Also -- not all games follow this pattern, so looking for these specific writes will not work for at least one other game. It seems that only bits 5-7 of the written value are relevent (hereon, they will be referred to as D5 - D7). Bit 4 ($10) is the only significant bit when read. Other bits are most likely open bus. When writing bytes to EPROM, games will generally perform 8 "output" patterns (either output 0 or output 1, depending on the bits it wants to write), followed by a 9th output pattern, which I would assume finalizes the write and/or possibly moves the 8 bits from a latch to EPROM. When reading bytes, games will generally perform 8 "read" patterns, followed by a single output pattern (which I would assume finalizes the read). Sometimes when the game is writing bits, it's writing data to be stored on EPROM, and other times it's setting the desired EPROM address and/or read/write mode. Knowing which it's doing involves keeping track of the state it's currently it and what it has done last, etc, etc. But again -- nobody *really* knows how it works. The method I've employed in my emu is outlined below -- and it appears to work for every game I've tried, but I *KNOW* it's not accurate. But, short of some hardware guru acquiring a handful of these carts and doing a thorough RE job, that's about the best anyone can do. Emulating EPROM: ----------------------- SUPER FAT IMPORTANT NOTE: This is just the method of EPROM emulation I employ in my emu. ***THIS IS NOT HOW THE ACTUAL HARDWARE WORKS*** Do not use this as a final word or anything -- this is simply the product of lots of guesswork, speculation, and trial and error. D5 appears to be the "trigger" bit, and D6 appears to be the "signal" bit. I have no clue what D7 does, and ignoring it completely has worked for me (though I'm sure it does have some purpose). "Commands" are sent by toggling D5 (0->1->0). Two states of D6 are observed -- one when D5 rises (0->1), and one when it falls (1->0). Using these two observed states, you get 4 possible commands. The command is sent when D5 falls. Example: byte D6 D5 write: $00 0 0 write: $40 1 0 write: $60 1 1 <-- D5 rise: D6=1 write: $40 1 0 <-- D5 fall: D6=1, command "1,1" sent here write: $00 0 0 The above sequence would issue a "1,1" command. Commands: Name rise,fall example write sequence ------------------------------------------------ Write 0 0,0 $00, $20, $00 Write 1 1,1 $00, $40, $60, $40, $00 Open 1,0 $00, $40, $60, $20, $00 Close 0,1 $00, $20, $60, $40, $C0 The unit can be in one of several modes: - Closed - Select - Address - Write - Read I also use an 8-bit temporary value, an 8-bit address (or 7-bit address, if 128 byte EPROM) and 9-step bit counter. I would assume the unit is Closed on startup (and possibly reset). Basic Concept overview: "Write 0" and "Write 1" commands advance the 9-step bit counter. The first 8 writes fill the appropriate bit in the temporary value. The 9th write will take the temp value and move it to either the address (if in Address mode), or to the desired area in EPROM (if in Write mode), and the mode will update accordingly. Basically the first 8 writes fill the temp value and the 9th moves it to where it needs to go. Reads operate similarly... but the temp buffer isn't affected by the writes, and the 9th step doesn't copy the temp value anywhere. Note however that games will perform a write between each bit read (presumably to advance it to the next bit) -- so you should do nothing but return the appropriate bit when the game reads the EPROM I/O Reg (do not advance it to the next bit on read). "Select" mode exists on 256 byte EPROM only (mapper 016). It is used to select between read/write mode. Bit 0 of the 8-bit value written when in Select mode determines read/write mode. On 128 byte EPROM (mapper 159), the high bit of the address selects read/write mode. In both cases, 1=read mode, 0=write mode. Remember that on 128 byte, values are written low bit first... but on 256 byte, they're written high bit first. Bits are read the same order they're written. Doing anything but opening when the unit is closed has no effect. Logic Flow Details (256-byte ... mapper 016) -------------------------------------------- Opening from Closed Mode: a) Enter Select Mode Opening from non-Closed Mode: a) if in Select Mode, increment address by 1 b) enter Select Mode. c) Reset bit counter (next write is the first write in the 9-write sequence) Writing in Select Mode: a) If low bit of written value = 1 -) Enter Read Mode b) otherwise... -) Enter Address Mode Writing in Address Mode: a) written value becomes address b) Enter Write mode Writing in Write Mode: a) written value moves to current address of EPROM b) mode is not changed Writing in Read Mode: a) Enter Select Mode Logic Flow Details (128-byte ... mapper 159) -------------------------------------------- Opening from Closed Mode: a) Enter Address Mode Opening from non-Closed Mode: a) increment address by 1 (wrap $7F->00) b) do not change mode c) Reset bit counter (next write is the first write in the 9-write sequence) Writing in Address Mode: a) written value becomes address (low 7 bits only) b) if high bit of written value is set... -) Enter Read Mode c) otherwise... -) Enter Write Mode Writing in Write Mode: a) written value moves to current address of EPROM b) Enter Address mode Writing in Read Mode: a) Enter Address Mode ================================================ FILE: docs/mapper/018.txt ================================================ ======================== = Mapper 018 = ======================== Example Games: -------------------------- The Lord of King Magic John Pizza Pop Registers: --------------------------- Range,Mask: $8000-FFFF, $F003 $800x,$900x: [.... PPPP] PRG Regs $A00x-$D00x: [.... CCCC] CHR Regs $E00x: [.... IIII] IRQ Reload value $F000: [.... ....] IRQ Reset $F001: [.... SSSE] IRQ Control S = Size of IRQ counter E = Enable $F002: [.... ..MM] Mirroring %00 = Horz %01 = Vert %10 = 1ScA %11 = 1ScB CHR Setup: --------------------------- Only low 4 bits of written value significant [.... CCCC] 2 regs combined to get an 8-bit page number $x000 or $x002 are the low 4 bits $x001 or $x003 are the high 4 bits $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------+-------+-------+-------+-------+-------+-------+-------+ |$A000+1|$A002+3|$B000+1|$B002+3|$C000+1|$C002+3|$D000+1|$D002+3| +-------+-------+-------+-------+-------+-------+-------+-------+ PRG Setup: --------------------------- Same as CHR, $x000 low, $x001 high $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ |$8000+1|$8002+3|$9000+1| { -1} | +-------+-------+-------+-------+ IRQ: --------------------------- 16-bit IRQ Reload value is set via regs $E00x. $E000 sets the low 4 bits, $E003 sets the high 4 bits. When enabled, the IRQ counter counts down every CPU cycle. When it wraps, an IRQ is generated. The 'S' bits in the control reg determine the size of the IRQ counter. It can be 4, 8, 12, or 16 bits wide: %000 = 16 bits wide %001 = 12 bits wide %01x = 8 bits wide %1xx = 4 bits wide If the counter is less than 16 bits, the high bits are not altered by IRQ counter clocking; they retain their value. Example: if the IRQ counter contains $1232, and is in 4-bit mode, it counts like so: $1232 $1231 $1230 $123F <--- IRQ here $123E ... Any write to the reset reg ($F000) will copy the 16-bit reload value into the IRQ counter (full 16 bits are copied, regardless of current 'S' value). Any write to $F000 or $F001 will acknowledge the IRQ. ================================================ FILE: docs/mapper/019.txt ================================================ ======================== = Mapper 019 = = + 210 = ======================== aka -------------------------- Namcot 106 N106 Example Games: -------------------------- Digital Devil Story - Megami Tensei 2 (019) Final Lap (019) Rolling Thunder (J) (019) Splatter House (019) Mappy Kids (019) Family Circuit '91 (210) Wagyan Land 2,3 (210) Dream Master (210) General Notes: -------------------------- For a while, this mapper number was shared with 210. Therefore, there are a lot of ROMs floating around that are labelled as mapper 019 that are really mapper 210. Some games require CHR-RAM in addition to any CHR-ROM present. I'm uncertain exactly how much, but giving them 8k seems to work. Mapper 019 also has an additional 128 bytes of Sound RAM, which is used for waveform tables and sound registers. Kaijuu Monogatari uses this as battery backed SRAM. The rest of the doc applies to both mapper numbers. Differences between the two (mirroring and sound) will be noted where appropriate. Registers: -------------------------- Range,Mask: $4800-FFFF, $F800 Writable and Readable: $4800: [DDDD DDDD] Sound Data port (see Sound section for details) (mapper 019 only) $5000: [IIII IIII] Low 8 bits of IRQ counter $5800: [EIII IIII] E = IRQ Enable (0=disabled, 1=enabled) I = High 7 bits of IRQ counter $6000-7FFF: mapped to PRG-RAM, not registers Writable only: $8000-B800: CHR Regs $C000-D800: Mirroring Regs (mapper 019 only) $E000: [..PP PPPP] PRG Reg 0 (8k @ $8000) $E800: [HLPP PPPP] H = High CHR RAM Disable (see CHR setup for details) L = Low CHR RAM Disable P = PRG Reg 1 (8k @ A000) $F000: [..PP PPPP] PRG Reg 2 (8k @ $C000) $F800: [IAAA AAAA] Sound Address (with auto-increment enable bit) (See Sound section for details) (mapper 019 only) PRG Setup: -------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | $E000 | $E800 | $F000 | { -1} | +-------+-------+-------+-------+ CHR Setup: -------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------+-------+-------+-------+-------+-------+-------+-------+ | $8000 | $8800 | $9000 | $9800 | $A000 | $A800 | $B000 | $B800 | +-------+-------+-------+-------+-------+-------+-------+-------+ Page numbers lower than $E0 will select CHR-ROM. Page numbers greater than or equal to $E0 will select CHR-RAM (RAM page N - $E0) *unless* CHR-RAM for the region is disabled via the appropriate bit in $E800. $E800.6, when set, disables RAM selection for $0xxx ($8000-9800 will always select ROM) $E800.7, when set, disables RAM selection for $1xxx ($A000-B800 will always select ROM) CHR-RAM disable allows games to utilize all 256k of CHR-ROM. When CHR-RAM is enabled, only 224k can be accessed. Mirroring: -------------------------- This section applies to mapper 019 only. 210 has hardwired mirroring [ $C000 ][ $C800 ] [ $D000 ][ $D800 ] Values less than $E0 select a CHR-ROM page for a NT. Values $E0 and up use NES's internal nametables (low bit selects which). Typical Examples: $C000 $C800 $D000 $D800 ----------------------- Horz: $E0 $E0 $E1 $E1 Vert: $E0 $E1 $E0 $E1 IRQ Operation: -------------------------- IRQs are driven by a 15-bit CPU cycle up-counter. $5000 and $5800 are *direct* access to the IRQ counter (they are not a reload value). Games can also read back the real-time state of the IRQ counter by reading those regs. When IRQs are enabled, the following occurs every CPU cycle: - If IRQ Counter = $7FFF a) Trip IRQ - otherwise... a) Increment IRQ counter by 1 Reading/Writing $5000 or $5800 will acknowledge the pending IRQ. Sources on the behavior of this IRQ counter vary. Some say that the IRQ counter wraps from $7FFF to $0000, and trips an IRQ only when it wraps -- however Sangokushi 2 polls $5800, and emulating IRQs that way results in the game locking up shortly after it starts (once it sees that $5800 is not what it expects, it resets the IRQ counter and loops) Emulating the IRQ counter as above seems to work for every game out there -- although it probably isn't 100% accurate. Sound: -------------------------- Sound applies to mapper 019 only. Mapper 210 has no extra sound. N106 has some pretty sweet expansion sound. And it's used in several games to boot! (More than any other expansion except for FDS) The N106 has up to 8 additional sound channels, each which plays back a configurable waveform of variable length, as well as having full volume control for each channel. There are 128 bytes of Sound RAM inside the N106 which is used to hold the waveform data, as well as sound registers. This RAM is accessed by setting the desired address by writing to $F800, then writing the desired data to $4800. $4800 is also readable. $F800: [IAAA AAAA] I = Auto-increment flag A = Sound RAM Address If the auto-increment flag is set, the Sound RAM address will increment (wrapping $7F->00) after every $4800 read/write. Sound Channel registers (inside Sound RAM): regs: "A" "B" "C" "D" "E" ------------------------------ Ch 0 - $40 $42 $44 $46 $47 Ch 1 - $48 $4A $4C $4E $4F Ch 2 - $50 $52 $54 $56 $57 Ch 3 - $58 $5A $5C $5E $5F Ch 4 - $60 $62 $64 $66 $67 Ch 5 - $68 $6A $6C $6E $6F Ch 6 - $70 $72 $74 $76 $77 Ch 7 - $78 $7A $7C $7E $7F "A": [FFFF FFFF] Low 8 freq bits "B": [FFFF FFFF] Mid 8 freq bits "C": [...L LLFF] F = High 2 freq bits L = Instrument Length (4 * (8-L)) "D": [AAAA AAAA] Instrument address "E": [.... VVVV] Volume Special Reg $7F: [.EEE VVVV] E = Number of Enabled channels (E+1) V = Channel 7's volume control Instruments: Instruments are in 4-bit samples. Each byte in sound RAM represents two samples (low 4 bits being the first sample, high 4 bits being the second sample). Each channel has an address which it uses to look for the instrument ('A' bits in reg "D"), as well as a length indicating how many samples are in the instrument ('L' bits in reg "C"). The instrument address is in 4-bit samples. IE: When the instrument address is $20, the instrument starts at the low 4 bits of byte address $10. A instrument address of $41 would be the high 4 bits of byte address $20. Instrument Length is 4 * (8-L) 4-bit samples. Therefore if L=3, the instrument is 20 4-bit samples long. Samples are unsigned: '0' is low, 'F' is high. For an example waveform... given the following instrument: $A8 DC EE FF FF EF DE AC 58 23 11 00 00 10 21 53 (length of 32 ... L=0) The following waveform (a pseudo-sine wave) would be produced: F - ***** E - ** ** D - * * C - * * B - A - * * 9 - 8 - * * 7 - 6 - 5 - * * 4 - 3 - * * 2 - * * 1 - ** ** 0 - ***** __________________________________ The waveform would continually loop this pattern Channel Disabling: Reg $7F controls the number of enabled channels. As little as 1 or as many as all 8 channels can be enabled. When not all channels are enabled, the high channels are the ones being used. That is, if only 3 channels are enabled, channels 5, 6, and 7 are the ones enabled, and the others are disabled. Disabling channels frees up more Sound RAM space for instruments (since the lower channels' registers are unused when disabled). Also, since there are fewer channels to clock, the enabled channels are clocked more quickly, resulting in higher quality sound and potentially higher tones (see frequency calculation) Frequency Calculation: The generated tone of each channel can be calculated with the following formula: F * CPU_CLOCK Hz = -------------------------- $F0000 * (E+1) * (8-L)*4 where: F = the 18-bit Freq value CPU_CLOCK = CPU clock rate (1789772.727272 on NTSC) E = Enabled Channels (bits as written to reg $7F) L = Instrument Length (bits as written) Or... you can figure it as the number of CPU cycles that have to pass before the channel takes the next step through its instrument: $F0000 * (E+1) Cycs = ------------------ F When F is 0, the channel is essentially "frozen" at it's current position and does not update (and thus, becomes silent). ================================================ FILE: docs/mapper/021.txt ================================================ ======================== = Mapper 021 = = + 023 = = + 025 = ======================== aka -------------------------- VRC4 Example Games: -------------------------- Wai Wai World 2 (021) Ganbare Goemon Gaiden 2 (021) Boku Dracula-kun (023) Tiny Toon Adventures (J) (023) Gradius 2 (J) (025) Bio Miracle Bokutte Upa (025) Multiple numbers, just one mapper: -------------------------- These three mapper numbers (021, 023, 025) collectively represent different wiring variations of the same mapper: VRC4. Each variation operates exactly the same, only the registers used are different because they all use different address lines. Some lines are even reversed from the norm. variant lines registers Mapper Number ================================================================= VRC4a: A1, A2 $x000, $x002, $x004, $x006 021 VRC4b: A1, A0 $x000, $x002, $x001, $x003 025 VRC4c: A6, A7 $x000, $x040, $x080, $x0C0 021 VRC4d: A3, A2 $x000, $x008, $x004, $x00C 025 VRC4e: A2, A3 $x000, $x004, $x008, $x00C 023 VRC4f: A0, A1 $x000, $x001, $x002, $x003 023 * see below * This doc will use the 'VRC4a' registers (0,2,4,6) in all following register descriptions. For other variants, use the above chart to convert. I'm unsure whether or not 'VRC4f' really exists. It seems to use the same registers are mapper 023's VRC2 counterpart (see 022.txt) but also has IRQ functionality (appears to be used by Tiny Toon Adventures). Could it be that 023 is really VRC4 and not a VRC4+VRC2 mix? Registers: -------------------------- Some registers are mirrored across several addresses. For example, writing to $9004 has the same effect as writing to $9006. $8000-$8006: [...P PPPP] PRG Reg 0 $9000,$9002: [.... ..MM] Mirroring: %00 = Vert %01 = Horz %10 = 1ScA %11 = 1ScB $9004,$9006: [.... ..M.] PRG Swap Mode Select $A000-$A006: [...P PPPP] PRG Reg 1 $B000-$E006: [.... CCCC] CHR Regs (see CHR Setup) $F000+$F002: [.... IIII] IRQ Reload Value (see IRQ section) $F004 [.... .MEA] IRQ Control (see IRQ section) $F006 [.... ....] IRQ Acknowledge (see IRQ section) PRG Setup: -------------------------- There are two PRG modes, which can be seleted via $9004. $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ PRG Mode 0: | $8000 | $A000 | { -2} | { -1} | +-------+-------+-------+-------+ PRG Mode 1: | { -2} | $A000 | $8000 | { -1} | +-------+-------+-------+-------+ CHR Setup: -------------------------- The VRC4 only has 5 data pins. To compensate, two CHR regs are combined to form a single page number. One reg contains the high 5 bits and the other reg contains the low 4 bits (allowing for 9-bit page numbers) Example: $B000+$B002 select 1k CHR page @ $0000 if $B000=$03 and $B002=$01 then use page $13 $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------+-------+-------+-------+-------+-------+-------+-------+ |$B000+2|$B004+6|$C000+2|$C004+6|$D000+2|$D004+6|$E000+2|$E004+6| +-------+-------+-------+-------+-------+-------+-------+-------+ -------------------------------------------------- -------------------------------------------------- VRC IRQs: -------------------------- VRC IRQ logic is shared by VRC4, VRC6, and VRC7. IRQs for all of those mappers operate exactly the same way. Therefore, this section applies to all of those mappers (other docs refer here). One thing in paticular to note with VRC4 that is different from VRC6, VRC7 is that the reload register is split in two just as CHR regs are. $F000 specifies the low 4 bits of the reload value, and $F002 specfies the high 4 bits. This only happens in VRC4. VRC6 and VRC7 have a single 8-bit register to specify the reload value. The rest of this doc will refer to this reload value as a single register. Notes: -------------------------- VRC IRQs are unique in that they simulate a scanline counter, without actually counting scanlines. The IRQ counter is actually a CPU cycle counter, with a prescaler that divides clocks by ~113.666667 CPU cycles (one NTSC scanline). This results in the IRQ counter being clocked once per scanline -- however unlike true scanline counters, it will be clocked even when the PPU is inactive, and even during VBlank! Registers: -------------------------- There are 3 registers relevant to VRC IRQs. See respective mapper doc for which register corresponds to which address: IRQ Reload: [IIII IIII] This register specifies the counter reload value. It does not affect the counter itself. IRQ Control: [.... .MEA] M = IRQ Mode (0=scanline mode, 1=CPU cycle mode) E = Enable (0=disabled, 1=enabled) A = Enable-on-acknowledge (see below) - If 'E' is written as set, the IRQ counter will be immediately reloaded with the reload value, and the prescaler will be reset. IRQs will also be enabled. - If 'E' is written as clear, the IRQ counter and prescaler are not changed, and IRQs are disabled. - Any write to this register will acknowledge the IRQ. IRQ Acknowledge: [.... ....] Any write to this register will acknowledge the IRQ. In addition, the 'A' control bit is copied to the 'E' control bit (enabling or disabling IRQs). No write to this register will change the state of the IRQ counter or prescaler. Operation: -------------------------- When in scanline mode ('M' control bit clear), a prescaler divides the passing CPU cycles by 114, 114, then 113 (and then repeats that pattern). This averages 113 + 2/3 CPU cycles (1 NTSC scanline). When the prescaler is reset, the sequence is reset, and it will be 114 CPU cycles until the next IRQ counter clock. A simple way to emulate prescaler behavior is to have it reset to 341, and subtract 3 every CPU cycle. When it drops to or below 0, increment it by 341 and clock the IRQ counter once. This will produce the 114,114,113 repeating pattern. When in cycle mode ('M' control bit set), the prescaler is effectively bypassed, and the IRQ counter gets clocked every CPU cycle. In this mode, the prescaler remains unchanged by passing CPU cycles. If IRQs are disabled, neither the prescaler nor IRQ counter get clocked. When the IRQ counter is clocked: - If IRQ counter = $FF... a) reload IRQ counter with reload value b) trip IRQ - otherwise... a) increment IRQ counter by 1 ================================================ FILE: docs/mapper/022.txt ================================================ ======================== = Mapper 022 = = + 023 = = + 025 = ======================== aka -------------------------- VRC2 Example Games: -------------------------- Ganbare Pennant Race (022) TwinBee 3 (022) Wai Wai World (023) Ganbare Goemon Gaiden (025) Multiple numbers, just one mapper: -------------------------- These mapper numbers (022, 023) represent 2 wiring variations of the same mapper: VRC2. Each variation operates the same, only the registers used are different because their lines are reversed from each other: variant lines registers Mapper Number ================================================================= VRC2a: A1, A0 $x000, $x002, $x001, $x003 022 * divides CHR bank select by two VRC2b: A0, A1 $x000, $x001, $x002, $x003 023 VRC2c: A1, A0 $x000, $x002, $x001, $x003 025 * does NOT divide CHR bank select by two This doc will use the 'VRC2b' registers (0,1,2,3) in all following register descriptions. For 'VRC2a', simply reverse $x001 and $x002 registers. VRC2a CHR: --------------------------- Important note! On VRC2a (mapper 022) only the high 7 bits of the CHR regs are used -- the low bit is ignored. Therefore, you effectively have to right-shift the CHR page by 1 to get the actual page number. For example... both $06 and $07 would both indicate page $03 This applies to VRC2a only. VRC2b (mapper 023) behaves normally. VRC2 vs. VRC4: -------------------------- VRC2 is strikingly similar to VRC4 (see mapper 021). The differences are: 1) VRC4 has IRQs, VRC2 does not 2) VRC4 has 5 bits for PRG regs, VRC2 only has 4 bits 3) VRC4 has 2 PRG modes, VRC2 does not. 4) VRC4 has 9 bit CHR banks, VRC2 only has 8 5) VRC4 internally supports external RAM, VRC2 does not Those differences aside -- they act exactly the same. Registers: -------------------------- Some registers are mirrored across several addresses. For example, writing to $8003 has the same effect as writing to $8000. $8000-$8003: [.... PPPP] PRG Reg 0 (select 8k @ $8000) $9000-$9003: [.... ..MM] Mirroring: %00 = Vert %01 = Horz %10 = 1ScA %11 = 1ScB $A000-$A003: [.... PPPP] PRG Reg 1 (select 8k @ $A000) $B000-$E003: [.... CCCC] CHR Regs (see CHR Setup) PRG Setup: -------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | $8000 | $A000 | { -2} | { -1} | +-------+-------+-------+-------+ CHR Setup: -------------------------- The VRC2 only has 4 data pins for CHR Regs. To compensate, two CHR regs are combined to form a single page number. One reg contains the high 4 bits and the other reg contains the low 4 bits (allowing for 8-bit page numbers) Example: $B000+$B001 select 1k CHR page @ $0000 if $B000=$03 and $B001=$01 then use page $13 (VRC2b) or page $09 (VRC2a -- see notes above) $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------+-------+-------+-------+-------+-------+-------+-------+ |$B000+1|$B002+3|$C000+1|$C002+3|$D000+1|$D002+3|$E000+1|$E002+3| +-------+-------+-------+-------+-------+-------+-------+-------+ ================================================ FILE: docs/mapper/023.txt ================================================ ======================== = Mapper 023 = ======================== Mapper 023 includes both a variant of VRC4, and a variant of VRC2. Both of which are covered in other docs. For details see the following: mapper 021 (VRC4) mapper 022 (VRC2) ================================================ FILE: docs/mapper/024.txt ================================================ ======================== = Mapper 024 = = + 026 = ======================== aka -------------------------- VRC6 Example Games: -------------------------- Akumajou Densetsu (024) Madara (026) Esper Dream 2 (026) Multiple numbers, just one mapper: -------------------------- As is the VRC way... VRC6 comes in two varieties. Both variants operate exactly the same, only the reigster addresses are different: variant lines registers Mapper Number ================================================================= VRC6a: A0, A1 $x000, $x001, $x002, $x003 024 VRC6b: A1, A0 $x000, $x002, $x001, $x003 026 This doc will use the 'VRC6a' registers (0,1,2,3) in all following register descriptions. For 'VRC6b', simply reverse $x001 and $x002. Registers: -------------------------- Some registers are mirrored across several addresses. For example, writing to $8003 has the same effect as writing to $8000. $8000-$8003: [PPPP PPPP] PRG Reg 0 (Select 16k @ $8000) $9000-$9002: Sound, Pulse 1 (see sound section) $A000-$A002: Sound, Pulse 2 $B000-$B002: Sound, Sawtooth $B003: [.... MM..] Mirroring: %00 = Vert %01 = Horz %10 = 1ScA %11 = 1ScB $C000-$C003: [PPPP PPPP] PRG Reg 1 (Select 8k @ $C000) $D000-$E003: [CCCC CCCC] CHR regs (See CHR setup) $F000-$F002: IRQ regs (See IRQ section) PRG Setup: -------------------------- $8000 $A000 $C000 $E000 +---------------+-------+-------+ | $8000 | $C000 | { -1} | +---------------+-------+-------+ CHR Setup: -------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------+-------+-------+-------+-------+-------+-------+-------+ | $D000 | $D001 | $D002 | $D003 | $E000 | $E001 | $E002 | $E003 | +-------+-------+-------+-------+-------+-------+-------+-------+ IRQs: -------------------------- VRC6 use the "VRC IRQ" setup shared by several VRCs. It uses the following registers: $F000: [IIII IIII] IRQ Reload $F001: [.... .MEA] IRQ Control $F002: [.... ....] IRQ Acknowledge For info on how these IRQs work, see the "VRC IRQs" section in mapper 021 ========================== Sound: -------------------------- VRC6 has two additional pulse channels, and one sawtooth channel. Both operate very similarly to the NES's native channels. Pulse Channels: ------------------------ $9000, $A000: [MDDD VVVV] M = Mode (0=normal mode, 1=digitized mode) D = Duty cycle (duty cycle is (D+1)/16) V = Volume $9001, $A001: [FFFF FFFF] F = Low 8 bits of Frequency $9002, $A002: [E... FFFF] F = High 4 bits of Frequency E = Channel Enable (0=disabled, 1=enabled) Pulse 1 uses regs $900x Pulse 2 uses regs $A00x Just like the NES's own pulse channels, an internal counter is counted down each CPU cycle, and when it wraps, it's reloaded with the 'F' frequency value, and the duty cycle unit takes another step. VRC6's pulses can have a duty cycle anywhere between 1/16 and 8/16 depending on the given 'D' value. Channel output is either 0 or 'V', depending on the current state of the duty cycle unit (or digitized mode). When 'M' is set (digitized mode), the duty cycle is ignored, and 'V' is always output. In this mode, the channel essentially is no longer a Pulse wave, but rather $9000/$A000 acts like a 4-bit PCM streaming register (similar to $4011). When 'E' is clear (channel disabled), output of the channel is forced to '0' (silencing the channel). Generated tone in Hz can be calculated by the following: CPU_CLOCK Hz = ------------- (F+1) * 16 Sawtooth Channel: ------------------------ $B000: [..AA AAAA] A = Accum Rate $B001: [FFFF FFFF] F = Low 8 bits of frequency $B002: [E... FFFF] F = High 4 bits of frequency E = Channel Enable (0=disabled, 1=enabled) The sawtooth uses an 8-bit accumulation register. Every time it is clocked, 'A' is added until the 7th clock, at which point it is reset to 0. The high 5 bits of this accumulation reg are then used as the channel output. Strangely, though, the accumulation register seems to only be clocked once for every *two* times the frequency divider expires. This results in a tone that's an octave lower than you might expect. It's difficult to put in words, so here's an example using $0B as a value for the accum rate ('A'): Step Accum. Channel output ------------------------------- 0 $00 $00 1 $00 $00 odd steps do nothing 2 $0B $01 even steps.. add value of 'A' to accum 3 $0B $01 4 $16 $02 5 $16 $02 6 $21 $04 7 $21 $04 8 $2C $05 9 $2C $05 10 $37 $06 11 $37 $06 12 $42 $08 6th and final time 'A' is added 13 $42 $08 0 $00 $00 7th time, accum is reset instead ... and the process repeats Channel output is the high 5 bits of the accumulation reg (right shift reg by 3). If the accum rate is too high, the accum reg WILL wrap at 8 bits, causing ugly distortion. The highest accum rate you can use without wrapping is $2A. If 'E' is clear (channel disabled), channel output is forced to 0 (silencing the channel). Generated tone in Hz can be calculated by the following: CPU_CLOCK Hz = ------------- (F+1) * 14 ================================================ FILE: docs/mapper/025.txt ================================================ ======================== = Mapper 025 = ======================== Mapper 025 is a variant of VRC4, which is covered in another doc. For info, see mapper 021. ================================================ FILE: docs/mapper/026.txt ================================================ ======================== = Mapper 026 = ======================== Mapper 026 is a variant of VRC6, which is covered in another doc. For info, see mapper 024. ================================================ FILE: docs/mapper/032.txt ================================================ ======================== = Mapper 032 = ======================== Example Games: -------------------------- Image Fight Major League Kaiketsu Yanchamaru 2 Registers: -------------------------- Range,Mask: $8000-BFFF, $F007 $8000-$8007: [...P PPPP] PRG Reg 0 $9000-$9007: [.... ..PM] ** see footnote P = PRG Mode M = Mirroring (0=Vert, 1=Horz) $A000-$A007: [...P PPPP] PRG Reg 1 $B000-$B007: [CCCC CCCC] CHR Regs PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ PRG Mode 0: | $8000 | $A000 | { -2} | { -1} | +-------+-------+-------+-------+ PRG Mode 1: | { -2} | $A000 | $8000 | { -1} | +-------+-------+-------+-------+ CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------+-------+-------+-------+-------+-------+-------+-------+ | $B000 | $B001 | $B002 | $B003 | $B004 | $B005 | $B006 | $B007 | +-------+-------+-------+-------+-------+-------+-------+-------+ Footnote: -------------------------- Major League wants hardwired 1-screen mirroring. (CIRAM A10 is tied to +5V on this game). Additionally, the register at $9000 is entirely disabled: the game can only request "PRG mode 0". A NES 2.0 submapper has been assigned for this difference. Otherwise you'll have to use a hash check. ================================================ FILE: docs/mapper/033.txt ================================================ ======================== = Mapper 033 = ======================== Example Games: -------------------------- Akira Bakushou!! Jinsei Gekijou Don Doko Don Insector X Note: -------------------------- Most dumps of mapper 048 games floating around are erroneously labelled as mapper 033. Mapper 033 does not have IRQs, mapper 048 does, and mirroring on each is handled a bit differently. Apart from that, the two are very similar. Registers: -------------------------- Range,Mask: $8000-BFFF, $A003 $8000 [.MPP PPPP] M = Mirroring (0=Vert, 1=Horz) P = PRG Reg 0 (8k @ $8000) $8001 [..PP PPPP] PRG Reg 1 (8k @ $A000) $8002 [CCCC CCCC] CHR Reg 0 (2k @ $0000) $8003 [CCCC CCCC] CHR Reg 1 (2k @ $0800) $A000 [CCCC CCCC] CHR Reg 2 (1k @ $1000) $A001 [CCCC CCCC] CHR Reg 3 (1k @ $1400) $A002 [CCCC CCCC] CHR Reg 4 (1k @ $1800) $A003 [CCCC CCCC] CHR Reg 5 (1k @ $1C00) PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | $8000 | $8001 | { -2} | { -1} | +-------+-------+-------+-------+ CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------+---------------+-------+-------+-------+-------+ | $8002 | $8003 | $A000 | $A001 | $A002 | $A003 | +---------------+---------------+-------+-------+-------+-------+ ================================================ FILE: docs/mapper/034.txt ================================================ ======================== = Mapper 034 = ======================== aka -------------------------- BxROM NINA-001 Example Games: -------------------------- Darkseed (BxROM) Mashou (BxROM) Impossible Mission 2 (NINA-001) Notes: -------------------------- How these two seperate and completely imcompatible mappers got assigned the same mapper number is a mystery. BxROM and NINA-001 are both assigned mapper 034, however they both work totally differently. There is no reliable way to tell the difference between the two apart from a CRC or Hash check. ================================= BxROM ================================= BxROM has bus conflicts... however this mapper also covers some BxROM compatible boards that do not suffer from bus conflicts. Registers (**BUS CONFLICTS** sometimes): -------------------------- $8000-FFFF: Select 32k PRG @ $8000 Note on a real BxROM, only the low 2 bits are used (PRG capped at 128k). But since this is BxROM and compatible, emus should use all 8 bits PRG Setup: -------------------------- $8000 $A000 $C000 $E000 +-------------------------------+ | $8000 | +-------------------------------+ ================================= NINA-001 ================================= Registers: -------------------------- $7FFD: Select 32k PRG @ $8000 $7FFE: Select 4k CHR @ $0000 $7FFF: Select 4k CHR @ $1000 I'm not sure whether or not WRAM can also exist at $6000-7FFF PRG Setup: -------------------------- $8000 $A000 $C000 $E000 +-------------------------------+ | $7FFD | +-------------------------------+ CHR Setup: -------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------------------------------+-------------------------------+ | $7FFE | $7FFF | +-------------------------------+-------------------------------+ ================================================ FILE: docs/mapper/044.txt ================================================ ======================== = Mapper 044 = ======================== Example Game: -------------------------- Super Big 7-in-1 Notes: --------------------------- This mapper is an MMC3 based multicart. The multicart selects a block of PRG and CHR depending on the selected game, and the MMC3 regs act as they normally would within the given block. For info on MMC3, see mapper 004. Registers: --------------------------- Range,Mask: $8000-FFFF, $E001 All registers behave exactly like a normal MMC3, except for: $A001: [EW.. .BBB] E,W = Same as on typical MMC3 B = Block select Blocks: --------------------------- Selecting block 7 is the same as selecting block 6. All blocks have 128k PRG and CHR... except for block 6 which has 256k PRG and CHR. All MMC3 selected pages are chosen from the given block (including fixed pages). This can be accomplished by ANDing the MMC3 regs with a given value, and ORing them with a value based on the current block: Block PRG-AND PRG-OR CHR-AND CHR-OR --------------------------------------------- 0 $0F $00 $7F $000 1 $0F $10 $7F $080 2 $0F $20 $7F $100 3 $0F $30 $7F $180 4 $0F $40 $7F $200 5 $0F $50 $7F $280 6,7 $1F $60 $FF $300 Powerup: --------------------------- Block 0 must be selected at powerup (and possibly reset?) ================================================ FILE: docs/mapper/045.txt ================================================ ======================== = Mapper 045 = ======================== Example Games: -------------------------- Super 8-in-1 Super 4-in-1 Super 1000000-in-1 Notes: --------------------------- This mapper is another MMC3 multicart, only it works a bit strangely. The multicart selects PRG/CHR blocks independently through 4 internal registers (accessed via $6000-7FFF). MMC3 registers then operate normally within the current block. For info on MMC3, see mapper 004. Registers: --------------------------- $6000-7FFF: Multicart regs $8000-FFFF: Same as MMC3 for selected blocks When Multicart regs are locked, writes to $6000-7FFF proceed to PRG-RAM, as normal. Where the game writes in the $6000-7FFF range doesn't matter. An internal counter selects which reg gets written to. ie: LDA #$00 STA $6000 ; first write, goes to reg 0 STA $6000 ; second write, goes to reg 1 STA $6000 ; reg 2 STA $6000 ; reg 3 STA $6000 ; back to reg 0, etc Reg 0: [CCCC CCCC] Low 8 bits of CHR-OR Reg 1: [PPPP PPPP] PRG-OR Reg 2: [CCCC SSSS] S = CHR-AND block size C = High 4 bits of CHR-OR Reg 3: [.LAA AAAA] L = Lock Multicart regs (1=locked) A = Inverted PRG-AND Once multicart regs are locked, the only way to unlock is to Reset the system. CHR Setup: -------------------------- 'S' bits are somewhat strange. They seem to select the size of the CHR block to mask out: 'S' Block size CHR-AND ---------------------------- $F 256k $FF $E 128k $7F $D 64k $3F ... $8 2k $01 7-0 1k $00 An easy way to emulate this: chr_and = 0xFF >> ~S_bits; CHR-OR is straightforward PRG Setup: ------------------------- PRG-OR is straightforward. PRG-AND is inverted. XOR written value with $3F for actual PRG-AND. Odd game behavior: ------------------------- Games seem to set the multicart registers in a loop that runs 256 times. Why it does this isn't known, neither is whether or not it is actually necessary. Powerup and reset: ------------------------- Block 0 must be selected on powerup and reset. Regs must be unlocked, as well... and they must be reset so that the next write will write to reg 0. ================================================ FILE: docs/mapper/046.txt ================================================ ======================== = Mapper 046 = ======================== Example Game: -------------------------- Rumblestation 15-in-1 Bus Conflicts?: --------------------------- No idea whether or not this mapper suffers from bus conflicts. Use caution! Registers: --------------------------- Regs at $6000-7FFF means no PRG-RAM. $6000-7FFF: [CCCC PPPP] High CHR, PRG bits $8000-FFFF: [.CCC ...P] Low CHR, PRG bits 'C' selects 8k CHR @ $0000 'P' select 32k PRG @ $8000 Powerup: --------------------------- $6000 set to 0 on powerup. ================================================ FILE: docs/mapper/047.txt ================================================ ======================== = Mapper 047 = ======================== Example Game: -------------------------- Super Spike V'Ball + Nintendo World Cup Notes: --------------------------- Yet another MMC3 multicart. See mapper 004 for info on MMC3. There is no PRG-RAM. The multicart reg lies at $6000-7FFF, but is only writable when MMC3 PRG-RAM is enabled and writable (see $A001) Registers: --------------------------- $6000-7FFF: [.... ...B] Block select $8000-FFFF: Same as MMC3 for selected block Each block has 128k PRG and 128k CHR. ================================================ FILE: docs/mapper/048.txt ================================================ ======================== = Mapper 048 = ======================== Example Games: -------------------------- Bubble Bobble 2 (J) Don Doko Don 2 Captain Saver Flintstones, The (J) Notes: -------------------------- Most dumps of mapper 048 games floating around are erroneously labelled as mapper 033. Mapper 033 does not have IRQs, mapper 048 does, and mirroring on each is handled a bit differently. Apart from that, the two are very similar. This mapper is very similar to MMC3 in a lot of ways, including how the IRQ counter operates. Registers: -------------------------- Range,Mask: $8000-FFFF, $E003 $8000: PRG Reg 0 (8k @ $8000) $8001: PRG Reg 1 (8k @ $A000) $8002: CHR Reg 0 (2k @ $0000) $8003: CHR Reg 1 (2k @ $0800) $A000: CHR Reg 2 (1k @ $1000) $A001: CHR Reg 3 (1k @ $1400) $A002: CHR Reg 4 (1k @ $1800) $A003: CHR Reg 5 (1k @ $1C00) $C000: IRQ Reload $C001: IRQ Clear $C002: IRQ Enable $C003: IRQ Acknowledge $E000: [.M.. ....] Mirroring 0 = Vert 1 = Horz PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | $8000 | $8001 | { -2} | { -1} | +-------+-------+-------+-------+ CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------+---------------+-------+-------+-------+-------+ | $8002 | $8003 | $A000 | $A001 | $A002 | $A003 | +---------------+---------------+-------+-------+-------+-------+ IRQs: --------------------------- IRQs on this mapper seem to behave exactly like MMC3's IRQs, with 2 exceptions: 1) The written reload value is inverted. EG: Writing $06 to the reload reg on this mapper would be like writing $F9 on MMC3. You can simply XOR the writes with $FF and it will work just like MMC3. 2) The IRQ seems to trip a little later than it does on MMC3. It looks like about a 4 CPU cycle delay from the normal MMC3 IRQ time. Failure to put in this delay results in shaking and other graphical quirks in some games. The registers on this mapper corespond directly to regs on the MMC3: 048 - MMC3 --------------- $C000 $C000 (XOR written value with $FF) $C001 $C001 $C002 $E001 $C003 $E000 For details on MMC3 IRQ operation, see mapper 004. ================================================ FILE: docs/mapper/049.txt ================================================ ======================== = Mapper 049 = ======================== Example Game: -------------------------- Super HIK 4-in-1 Notes: --------------------------- Yet another MMC3 multicart. For info on MMC3, see mapper 004. There is no PRG-RAM. The multicart reg lies at $6000-7FFF, but is only writable when MMC3 PRG-RAM is enabled and writable (see $A001) Registers: --------------------------- $6000-7FFF: [BBPP ...O] Multicart reg B = Block P = 32k PRG Reg O = PRG Mode (0=32k mode) $8000-FFFF: Same as MMC3 for selected block Each block is 128k PRG and 128k CHR PRG Setup: --------------------------- When the 'O' mode bit is clear, ordinary MMC3 PRG regs are ignored, and instead, 32k PRG page 'P' is swapped in at $8000. When 'O' is set, 'P' is ignored, and MMC3 PRG regs work normally for the current block. Powerup: --------------------------- $6000 set to 0 on powerup. ================================================ FILE: docs/mapper/050.txt ================================================ ======================== = Mapper 050 = ======================== Example Game: -------------------------- Super Mario Bros. (JU) (Alt Levels) (SMB2j pirate cart) Notes: --------------------------- No PRG-RAM. PRG setup is bizarre, as is the scrambled PRG reg. Registers: --------------------------- Range,Mask: $4020-5FFF, $4120 $4020: [.... HLLM] L,M,H = Low, middle, and high bits of PRG Reg $4120: [.... ...E] IRQ Enable (0=Disabled, 1=Enabled) PRG Setup: --------------------------- $6000 $8000 $A000 $C000 $E000 +-------+-------+-------+-------+-------+ | {$0F} | { 8 } | { 9 } | $4020 | {$0B} | +-------+-------+-------+-------+-------+ IRQs: --------------------------- Writing to $4120 with E=0 will disable IRQs, acknowledge the pending IRQ, and reset the IRQ counter to 0. Writing with E=1 will just enable IRQs (but will not change anything else). When enabled, The IRQ counter will count up every CPU cycle. When it makes the transition from $0FFF->$1000, and IRQ is generated. The counter appears to be a full 16-bits (so it will not wrap until $FFFF) ================================================ FILE: docs/mapper/052.txt ================================================ ======================== = Mapper 052 = ======================== Example Game: -------------------------- Mario 7-in-1 Notes: --------------------------- Yet another MMC3 multicart. For info on MMC3, see mapper 004. Registers: --------------------------- $6000-7FFF: [.MHL SBPP] Multicart reg P = PRG Block (bits 0,1) B = CHR+PRG Block Select bit (PRG bit 2, CHR bit 1) S = PRG Block size (0=512k 1=256k) L = CHR Block low bit (bit 0) H = CHR Block high bit (bit 2) M = CHR Block size (0=256k 1=128k) $8000-FFFF: Same as MMC3 for selected block $6000 can only be written to once ... and only if PRG-RAM is enabled and writable (see $A001). Once $6000 has been written to, $6000-7FFF maps to PRG-RAM PRG Setup: --------------------------- 'S' PRG-AND PRG-OR ------------------------ 0 $1F %BP0 0000 1 $0F %BPP 0000 'B' and 'P' bits make a 3-bit value used as PRG-OR (left shift 4). When 'S' is clear, the low bit of that value is forced to 0. PRG swapping behaves just like a normal MMC3 within this selected block CHR Setup: --------------------------- 'M' CHR-AND CHR-OR ------------------------ 0 $FF %HB 0000 0000 1 $7F %HB L000 0000 'H', 'B' and 'L' bits make a 3-bit value used as CHR-OR (left shift 7). When 'M' is clear, the low bit of that value is forced to 0. CHR swapping behaves just like a normal MMC3 within this selected block Powerup and Reset: --------------------------- $6000 set to 0 on reset and powerup. ================================================ FILE: docs/mapper/057.txt ================================================ ======================== = Mapper 057 = ======================== Example Games: -------------------------- GK 47-in-1 6-in-1 (SuperGK) Registers: --------------------------- Range,Mask: $8000-FFFF, $8800 $8000: [.H.. .AAA] H = High bit of CHR reg (bit 4) A = Low 3 bits of CHR Reg (OR with 'B' bits) $8800: [PPPO MBBB] P = PRG Reg O = PRG Mode M = Mirroring (0=Vert, 1=Horz) B = Low 3 bits of CHR Reg (OR with 'A' bits) CHR Setup: --------------------------- 'A' and 'B' bits combine with an OR to get the low 3 bits of the desired page, and the 'H' bit is the high bit. This 4-bit value selects an 8k page @ $0000 PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ PRG Mode 0: | $8800 | $8800 | +-------------------------------+ PRG Mode 1: | <$8800> | +-------------------------------+ ================================================ FILE: docs/mapper/058.txt ================================================ ======================== = Mapper 058 = ======================== Example Games: -------------------------- 68-in-1 (Game Star) Study and Game 32-in-1 Registers: --------------------------- $8000-FFFF: A~[.... .... MOCC CPPP] P = PRG page select C = CHR page select (8k @ $0000) O = PRG Mode M = Mirroring (0=Vert, 1=Horz) PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------------------------------+ PRG Mode 0: | <$8000> | +-------------------------------+ PRG Mode 1: | $8000 | $8000 | +---------------+---------------+ ================================================ FILE: docs/mapper/060.txt ================================================ ======================== = Mapper 060 = ======================== Example Game: -------------------------- Reset Based 4-in-1 Notes: --------------------------- This mapper is very, very unique. It's a multicart that consists of four NROM games, each with 16k PRG (put at $8000 and $C000) and 8k CHR. The current block that is selected is determined by an internal register that can only be incremented by a soft reset! I would assume the register is 2 bits wide? Don't know for sure. ================================================ FILE: docs/mapper/061.txt ================================================ ======================== = Mapper 061 = ======================== Example Game: -------------------------- 20-in-1 Registers: --------------------------- $8000-FFFF: A~[.... .... M.LO HHHH] H = High 4 bits of PRG Reg L = Low bit of PRG Reg O = PRG Mode M = Mirroring (0=Vert, 1=Horz) PRG Setup: --------------------------- PRG Reg is 5 bits -- combination of 'H' and 'L' bits. $8000 $A000 $C000 $E000 +-------------------------------+ PRG Mode 0: | <$8000> | +-------------------------------+ PRG Mode 1: | $8000 | $8000 | +---------------+---------------+ ================================================ FILE: docs/mapper/062.txt ================================================ ======================== = Mapper 062 = ======================== Example Game: -------------------------- Super 700-in-1 Registers: --------------------------- $8000-FFFF: A~[..pp pppp MPOC CCCC] [.... ..cc] p = Low bits of PRG Reg P = High bit of PRG Reg c = Low bits of CHR Reg C = High bits of CHR Reg O = PRG Mode M = Mirroring (0=Vert, 1=Horz) PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------------------------------+ PRG Mode 0: | <$8000> | +-------------------------------+ PRG Mode 1: | $8000 | $8000 | +---------------+---------------+ CHR Setup: ---------------------------- 'C' and 'c' select an 8k page @ $0000 ================================================ FILE: docs/mapper/064.txt ================================================ ======================== = Mapper 064 = ======================== aka -------------------------- Tengen RAMBO-1 Example Games: -------------------------- Klax Skull and Crossbones Shinobi Notes: -------------------------- This mapper is very similar to MMC3. It uses a similar swapping system, but adds a little functionality. IRQs are set up similar as well... but have some major differences. This is one of those mappers that is a big pain to impliment in an emu -- especially since so few games use it. And the games that use it really blow hard. Registers: -------------------------- Range,Mask: $8000-FFFF, $E001 $8000: [CPK. AAAA] C = CHR mode select P = PRG mode select K = full 1k CHR mode select (see CHR setup) A = Address for use with $8001 $8001: [DDDD DDDD] Data port R:0 -> CHR reg 0 R:1 -> CHR reg 1 R:2 -> CHR reg 2 R:3 -> CHR reg 3 R:4 -> CHR reg 4 R:5 -> CHR reg 5 R:6 -> PRG reg 0 R:7 -> PRG reg 1 R:8 -> CHR reg 6 R:9 -> CHR reg 7 R:A - R:E not used R:F -> PRG reg 2 $A000: [.... ...M] Mirroring 0 = Vert 1 = Horz $C000: [IIII IIII] IRQ Reload value $C001: [.... ...M] IRQ Mode select and reset 0 = Scanline (A12) mode 1 = Cycle mode $E000: [.... ....] IRQ Acknowledge/Disable $E001: [.... ....] IRQ Enable PRG Setup: --------------------------- PRG mode is selected via $8000.6 $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ PRG Mode 0: | R:6 | R:7 | R:F | { -1} | +-------+-------+-------+-------+ PRG Mode 1: | R:F | R:6 | R:7 | { -1} | +-------+-------+-------+-------+ CHR Setup: --------------------------- $8000 has 2 bits to configure CHR modes. Therefore there are effectively 4 CHR modes. $8000: [CPK. AAAA] <--- C and K bits relevent to CHR $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------+---------------+-------+-------+-------+-------+ C=0, K=0 | | | R:2 | R:3 | R:4 | R:5 | +---------------+---------------+-------+-------+-------+-------+ C=0, K=1 | R:0 | R:8 | R:1 | R:9 | R:2 | R:3 | R:4 | R:5 | +-------+-------+-------+-------+---------------+---------------+ C=1, K=0 | R:2 | R:3 | R:4 | R:5 | | | +-------+-------+-------+-------+---------------+---------------+ C=1, K=1 | R:2 | R:3 | R:4 | R:5 | R:0 | R:8 | R:1 | R:9 | +-------+-------+-------+-------+-------+-------+-------+-------+ IRQs: --------------------------- There are two seperate IRQ modes. One uses A12 to count scanlines in a manner just like MMC3 does (see mapper 004 for details on how scanlines are counted and the restrictions involved). The other mode uses CPU cycles with a 4-step prescaler (so the IRQ counter gets clocked every 4 CPU cycles). Regardless of the mode used to clock the counter... every time the counter is clocked, the following actions occur: - If Reset reg ($C001) was written to after previous clock... a) reload IRQ counter with IRQ Reload value **PLUS ONE** - Otherwise... If IRQ Counter is 0... a) reload IRQ counter with IRQ Reload value - Otherwise... a) Decrement IRQ counter by 1 b) If IRQ counter is now 0 and IRQs are enabled, trigger IRQ Just like with MMC3, the counter is clocked and updated even when IRQs are disabled -- however IRQs will only be triggered when enabled. Note about the plus one: I'm not sure if 1 is really added or if there's simply an additional 1 clock delay before the IRQ counter is updated. From a software standpoint, it doesn't really matter -- adding the additional 1 works without any side-effects. Registers involved with IRQs: --------------------------- $C000: [IIII IIII] - IRQ Reload value $C001: [.... ...M] - IRQ Reset reg, mode select 0 = Scanline mode (A12) 1 = CPU Cycle mode (with prescaler) Any write to this register will make it so that the IRQ counter will reload with the reload value +1 on its next clock. Whether or not writing to this register clears the IRQ counter like it does with MMC3 isn't known... and doesn't matter, since it's reloaded later anyway. Also, any write to this register will reset the CPU cycle prescaler (so that it will be 4 CPU cycles until the next clock). $E000: [.... ....] - IRQ Acknowledge/Disable Any write to this register will acknowledge the pending IRQ, and disable IRQs $E001: [.... ....] - IRQ Enable Any write to this register will enable IRQs A note about IRQs: ------------------ Scanline IRQs seem to trip a little later than they do on the MMC3. It looks like about a 5 dot delay from the normal MMC3 IRQ time (265 instead of 260). Failure to put in this delay results in shaking and other graphical quirks in some games... notably Klax. This delay also seems to exist for CPU cycle driven IRQs (Skull & Crossbones will suffer without it). Perhaps the RAMBO-1's IRQ generating hardware is a little slower than usual? Apart from that timing difference, A12 clocks RAMBO-1's IRQ counter just exactly like it does MMC3, so all the notes about A12, $2006/7, etc from the mapper 004 documenation apply to this mapper as well. ================================================ FILE: docs/mapper/065.txt ================================================ ======================== = Mapper 065 = ======================== Example Games: -------------------------- Daiku no Gen San 2 Kaiketsu Yanchamaru 3 Spartan X 2 Registers: -------------------------- $8000: PRG Reg 0 (8k @ $8000) $A000: PRG Reg 1 (8k @ $A000) $C000: PRG Reg 2 (8k @ $C000) $B000-$B007: CHR regs $9001: [M... ....] Mirroring 0 = Vert 1 = Horz $9003: [E... ....] IRQ Enable (0=disabled, 1=enabled) $9004: [.... ....] Reload IRQ counter $9005: [IIII IIII] High 8 bits of IRQ Reload value $9006: [IIII IIII] Low 8 bits of IRQ Reload value On Powerup: --------------------------- On powerup, it appears as though PRG regs are inited to specific values: $8000 = $00 $A000 = $01 $C000 = $FE Games do rely on this and will crash otherwise. PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | $8000 | $A000 | $C000 | { -1} | +-------+-------+-------+-------+ CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------+-------+-------+-------+-------+-------+-------+-------+ | $B000 | $B001 | $B002 | $B003 | $B004 | $B005 | $B006 | $B007 | +-------+-------+-------+-------+-------+-------+-------+-------+ IRQs: --------------------------- This mapper's IRQ system is very simple. There's a 16-bit internal down counter which (when enabled), decrements by 1 every CPU cycle. When the counter reaches 0, an IRQ is fired. The counter stops at 0 -- it does not wrap and isn't automatically reloaded. Any write to $9003 or $9004 will acknowledge the pending IRQ. Any write to $9004 will copy the 16-bit reload value into the counter. $9006 and $9005 set the reload value, but do not have any effect on the actual counter. Note that $9005 is the HIGH bits, not the low bits. ================================================ FILE: docs/mapper/066.txt ================================================ ======================== = Mapper 066 = ======================== aka -------------------------- GxROM and compatible Example Games: -------------------------- Doraemon Dragon Power Gumshoe Thunder & Lightning Super Mario Bros. + Duck Hunt Notes: --------------------------- I do not know whether or not this mapper suffers from bus conflicts. Use caution! This mapper is INFAMOUS for having bad headers. Probably 80% or more of these ROMs floating around out there have the wrong mirroring mode set in the header. Some games are marked as mapper 066 that are really mapper 140. See mapper 140 for info. Registers: -------------------------- $8000-FFFF: [..PP ..CC] P = Selects 32k PRG @ $8000 C = Selects 8k CHR @ $0000 ================================================ FILE: docs/mapper/067.txt ================================================ ======================== = Mapper 067 = ======================== Example Games: -------------------------- Fantasy Zone 2 (J) Mito Koumon - Sekai Manyuu Ki Registers: --------------------------- Range,Mask: $8000-FFFF, $F800 $8800: CHR Reg 0 (2k @ $0000) $9800: CHR Reg 1 (2k @ $0800) $A800: CHR Reg 2 (2k @ $1000) $B800: CHR Reg 3 (2k @ $1800) $C800: IRQ Load (write twice) $D800: [...E ....] IRQ Enable (0=disabled, 1=enabled) $E800: [.... ..MM] Mirroring %00 = Vert %01 = Horz %10 = 1ScA %11 = 1ScB $F800: PRG Reg (16k @ $8000) CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------+---------------+---------------+---------------+ | $8800 | $9800 | $A800 | $B800 | +---------------+---------------+---------------+---------------+ PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | $F800 | { -1} | +---------------+---------------+ IRQ Operation: --------------------------- $C800 is a write-twice register (similar to $2005 and $2006). The first write sets the *high* 8 bits of the IRQ counter, and the second write sets the *low* 8 bits. This directly changes the actual IRQ counter -- not a reload value. Any write to $D800 will acknowledge the IRQ, and will also reset the toggle so that the next write to $C800 will be the first write. $D800, of course, also enables/disables IRQs (bit 4). The IRQ counter, when enabled, counts down every CPU cycle. When it wraps ($0000->FFFF), it disables itself and triggers an IRQ. ================================================ FILE: docs/mapper/068.txt ================================================ ======================== = Mapper 068 = ======================== Example Games: -------------------------- After Burner 2 Maharaja Registers: --------------------------- Range,Mask: $8000-FFFF, $F000 $8000: CHR Reg 0 (2k @ $0000) $9000: CHR Reg 1 (2k @ $0800) $A000: CHR Reg 2 (2k @ $1000) $B000: CHR Reg 3 (2k @ $1800) $C000: [.NNN NNNN] NT-ROM Reg 0 $D000: [.NNN NNNN] NT-ROM Reg 1 $E000: [...R ...M] Mirroring (see section below) $F000: PRG Reg (16k @ $8000) CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------+---------------+---------------+---------------+ | $8000 | $9000 | $A000 | $B000 | +---------------+---------------+---------------+---------------+ PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | $F000 | { -1} | +---------------+---------------+ Mirroring: --------------------------- The mirroring reg has two significant bits: $E000: [...R ...M] 'M' selects H/V: 0 = Vert 1 = Horz 'R' selects whether or not to use CHR-ROM as nametables. 0 = normal mirroring 1 = use CHR-ROM When 'R' is set, $C000 and $D000 are used to select 1k CHR-ROM pages to use as nametables. They are arranged in either Horz or Vert mirroring fashion depending on the 'M' bit ($C000 would be used in place of NTA, $D000 in place of NTB). R=1, M=0: [ $C000 ][ $D000 ] [ $C000 ][ $D000 ] R=1, M=1: [ $C000 ][ $C000 ] [ $D000 ][ $D000 ] Note that CHR-ROM for nametables is taken from the last 128k of CHR. This means you must effectively OR the value written to $C000/$D000 with $80. ================================================ FILE: docs/mapper/069.txt ================================================ ======================== = Mapper 069 = ======================== aka -------------------------- FME-7 Sunsoft 5B Example Games: -------------------------- Gimmick! Batman: Return of the Joker Hebereke Gremlins 2 (J) Notes: -------------------------- This mapper is FME-7 and compatible. Sunsoft 5B operates the same as FME-7, only it has additional sound hardware. For a long time, it was thought Gimmick! uses FME-7, so the expansion sound is labeled as FME-7 in various places -- however -- technically FME-7 has no extra sound. Gimmick! is the only known game to use the extra sound found on Sunsoft 5B Registers: -------------------------- Range,Mask: $8000-FFFF, $E000 $8000: [.... AAAA] Address for use with $A000 $A000: [DDDD DDDD] Data port R:0-7 -> CHR Regs R:8-B -> PRG Regs R:C -> Mirroring R=D-F -> IRQ Control $C000: [.... AAAA] Address for use with $E000 (sound) $E000: [DDDD DDDD] Data port (sound -- see sound section) PRG Setup: --------------------------- R:8 controls $6000-7FFF. It can map in PRG-RAM, PRG-ROM, or leave it unmapped (open bus), depending on the mode it sets: R:8: [ERPP PPPP] E = Enable RAM (0=disabled, 1=enabled) R = RAM/ROM select (0=ROM, 1=RAM) P = PRG page if E=0 and R=1, RAM is selected, but it's disabled, resulting in open bus. In case it's still unclear: R=0: ROM @ $6000-7FFF R=1, E=0: Open Bus @ $6000-7FFF R=1, E=1: RAM @ $6000-7FFF R:9 - R:B appear to be a full 8 bits: [PPPP PPPP], and select only ROM. $6000 $8000 $A000 $C000 $E000 +-------+-------+-------+-------+-------+ | R:8 | R:9 | R:A | R:B | { -1} | +-------+-------+-------+-------+-------+ No games seem to use more than 8k PRG-RAM, so I'm unsure whether or not it's swappable when selected. I don't see why it wouldn't be. CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------+-------+-------+-------+-------+-------+-------+-------+ | R:0 | R:1 | R:2 | R:3 | R:4 | R:5 | R:6 | R:7 | +-------+-------+-------+-------+-------+-------+-------+-------+ Mirroring: --------------------------- R:C: [.... ..MM] %00 = Vert %01 = Horz %10 = 1ScA %11 = 1ScB IRQs: --------------------------- This mapper has a 16-bit IRQ counter which decrements every CPU cycle. When it wraps from $0000->FFFF, an IRQ is tripped. reg R:E sets the low 8 bits of the counter reg R:F sets the high 8 bits Note the regs change the actual counter -- not a reload value. reg R:D is the IRQ control: [C... ...T] C = Enable countdown (0=disabled, 1=enabled) T = Enable IRQ triggering (0=disabled, 1=enabled) In order for IRQs to work as expected, both bits must be set. If either bit is cleared, an IRQ won't occur: C=0, T=1: IRQs are enabled, but the counter will never decrement C=1, T=0: Counter decrements, but IRQs are disabled Acknowledging IRQs can only be done by disabling them (T=0). Sound: --------------------------- Sunsoft 5B appears to be identical to the AY 3-8910 (or a similar chip -- possibly a different AY 3-891x or a YM2149). The only game to use the sound, Gimmick!, does not use the envelope or noise functionality that exists on the AY 3-891x, however, through testing it has been shown that such functionality does in fact exist. The sound info below is a simplified version of the behavior. Envelope and Noise are not covered (aside from the noise shift formula), and registers relating to those areas are not mentioned. However the information below is enough to satisfy Gimmick! If you want further information and full register descriptions, consult an AY 3-8910 datasheet or doc. Sunsoft 5B has 3 Square channels (no configurable duty cycle -- always play at 50% duty). Each operate similarly to the native NES sound channels. They output sound at 1 octave lower than what may be expected, though (see below). Sound Regs: --------------------------- $C000: [.... AAAA] Address for use with $E000 $E000: [DDDD DDDD] Data port: R:0 -> [FFFF FFFF] Chan 0, Low 8 bits of Freq R:1 -> [.... FFFF] Chan 0, High 4 bits of Freq R:2 -> [FFFF FFFF] Chan 1, Low 8 bits of Freq R:3 -> [.... FFFF] Chan 1, High 4 bits of Freq R:4 -> [FFFF FFFF] Chan 2, Low 8 bits of Freq R:5 -> [.... FFFF] Chan 2, High 4 bits of Freq R:7 -> [.... .CBA] Channel disable flags (0=enabled, 1=disabled) C = Disable Chan 2 B = Disable Chan 1 A = Disable Chan 0 R:8 -> [.... VVVV] Chan 0, Volume R:9 -> [.... VVVV] Chan 1, Volume R:A -> [.... VVVV] Chan 2, Volume Operation: --------------------------- For tone generation, a counter is counted up each CPU cycle. When it reaches the given 'F' value, it resets to zero, and another step through the duty cycle is taken. These squares' duty cycles are fixed at 50% (AY 3-8910 docs say 8/16, but see below). Emulating in this fashion, with a 16-step duty, these channels play 1 octave higher than they should! Therefore, either channels are only clocked every other CPU cycle... or (what I find to be easiest to emulate) the duty is actually 16/32 instead of 8/16, or something else is going on. I do not know which is actually happening. The generated tone in Hz can be calculated with the following: CPU_CLOCK Hz = ------------- (F+1) * 32 When the duty cycle outputs high, 'V' is output, otherwise 0 is output. When the channel is disabled (see R:7), 0 is forced as output for the channel. Non-linear volume: --------------------------- Output volume is non-linear... increasing in steps of 3 dB. Output can be calculated with the following pseudo-code: vol = 1.0; for(i = 0; i < 0x10; ++i) { sunsoft_out[i] = vol * base; vol *= step; } Where 'base' can be adjusted to match your native NES sound channel levels, and 'step' is "10^(dB/20)". For 3 dB, 'step' would be ~1.4125 Noise Formula: --------------------------- >> >> +-->[nnnn nnnn nnnn nnnn]->output | | | | | ++ | | | | v v +-------------------XOR - 16-bit right-shift reg - bits 0,3 (before shift) XOR to create new input bit - bit 0 is shifted to output - initial feed is 1 ================================================ FILE: docs/mapper/070.txt ================================================ ======================== = Mapper 070 = ======================== Example Games: -------------------------- Family Trainer - Manhattan Police Family Trainer - Meiro Daisakusen Kamen Rider Club Space Shadow Notes: --------------------------- I do not know whether or not this mapper suffers from bus conflicts. Use caution! Many of these games use the family trainer mat as an input device. Registers: -------------------------- $8000-FFFF: [PPPP CCCC] P = Selects 16k PRG @ $8000 C = Selects 8k CHR @ $0000 PRG Setup: -------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | $8000 | { -1} | +---------------+---------------+ ================================================ FILE: docs/mapper/071.txt ================================================ ======================== = Mapper 071 = ======================== Example Games: -------------------------- MiG 29 - Soviet Fighter Fire Hawk The Fantastic Adventures of Dizzy Bee 52 Notes: -------------------------- This mapper covers several Camerica/Codemasters boards. One in paticular that needs to be noted is the board used by Fire Hawk -- which has mapper controlled 1-screen mirroring. On other boards, mirroring is hardwired! This is yet another one of those terrific mapper number incompatibilities. Some of these games are EXTREMELY DIFFICULT to emulate. Not because the mapper is complicated (it's actually very simple), but because the games are picky about timing and use some seldom used aspects of the NES. In paticular: - Bee 52 uses the sprite overflow flag ($2002.5) - MiG 29 uses DMC IRQs, and is VERY PICKY about their timing. If your DMC IRQ timing isn't spot on (or at least really freaking close), this game will glitch like hell. This mapper also involves a custom lockout defeat circuit which is mostly unimportant for emulation purposes. Details will not be mentioned here, but are outlined in Kevtris' Camerica Mappers documentation. Fire Hawk does some strange timing code when changing the mirroring mode. It is unknown whether or not any special timing is required. Registers: -------------------------- $8000-9FFF: [...M ....] Mirroring (for Fire Hawk only!) 0 = 1ScA 1 = 1ScB $C000-FFFF: PRG Select (16k @ $8000) PRG Setup: -------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | $C000 | { -1} | +---------------+---------------+ ================================================ FILE: docs/mapper/072.txt ================================================ ======================== = Mapper 072 = ======================== Example Games: -------------------------- Pinball Quest (J) Moero!! Pro Tennis Moero!! Juudou Warriors Registers (**BUS CONFLICTS**): --------------------------- $8000-FFFF: [PCRS DDDD] P = When a 1 is written after a 0 was previously written, the bottom three bits of the data bus are copied to the PRG bank select C = When a 1 is written after a 0 was previously written, the bottom four bits of the data bus are copied to the CHR bank select R = For games that have add-on sound, while 0, the ADPCM playback IC is held in reset and unable to make sound S = For games that have add-on sound, when the value written here changes (direction unknown because the datasheet contradicts itself), the sound specified by the bottom 5 bits of the address bus is played. Leaving the value at 0 will probably result in erratic audio playback. D = the three- or four- bit bank number to switch to, as appropriate. Notes: --------------------------- Commands pass through a latch. Rather than writing to the regs directly, you write the desired page number and command to the latch, then send another command that readies it for the next time. Commands (PC bits together): %00 = Do nothing (prepare for next write) %01 = Set CHR Page %10 = Set PRG page %11 = Set both simultaneously Example: If a game wanted to select CHR page 3, it would first write $43, then $03. The $43 fills the latch with command bits $4, which instruct bank $3 to be used for CHR; then the write of $03 prepares for the next write by resetting the command bits to $0. The $03 should be able to be any value from $00 to $0F, because the command bits are what is crucial. No current theory explains why games go to any effort to put the bank's nybble in the second byte, although perhaps it has to do with not disturbing the bank registers while the logic propagates. CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------------------------------------------------------+ | CHR Reg | +---------------------------------------------------------------+ PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | PRG Reg | { -1} | +---------------+---------------+ ================================================ FILE: docs/mapper/073.txt ================================================ ======================== = Mapper 073 = ======================== aka -------------------------- VRC3 Example Games: -------------------------- Salamander Registers: -------------------------- Range,Mask: $8000-FFFF, $F000 $8000: [.... IIII] Bits 0- 3 of IRQ reload value $9000: [.... IIII] Bits 4- 7 of IRQ reload value $A000: [.... IIII] Bits 8-11 of IRQ reload value $B000: [.... IIII] Bits 12-15 of IRQ reload value $C000: [.... .MEA] IRQ Control M = IRQ Mode (0=16-bit mode, 1=8-bit mode) E = IRQ Enable (0=disabled, 1=enabled) A = Enable-on-Acknowledge (see IRQ section) $D000: [.... ....] IRQ Acknowledge (see IRQ section) $F000: [.... PPPP] PRG Select (16k @ $8000) PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | $F000 | { -1} | +---------------+---------------+ IRQs: --------------------------- VRC3 IRQs operate differently from other VRCs. The counter is 16 bits instead of 8 bits, and there is no scanline mode -- only CPU cycle mode. Other aspects, however, are very similar. $8000-B000 set the 16-bit reload value (not the actual IRQ counter). When $C000 is written to with the 'E' bit set, the reload value is copied into the actual IRQ counter. When enabled, the IRQ counter will increment by 1 every CPU cycle until it wraps, at which point the IRQ counter is reloaded with the reload value (relevent bits only! see Modes below) and an IRQ is tripped. Any write to $C000 or $D000 will acknowledge the IRQ. Any write to $D000 will also copy the 'A' control bit to the 'E' control bit... enabling or disabling IRQs. This does not change the contents of the IRQ counter. Modes: --------------------------- There are 8-bit and 16-bit modes for the IRQ counter, as controlled by the 'M' bit in $C000. In 16-bit mode (M=0): - Counter is a full 16-bits. - IRQ is triggered when IRQ counter is incremented from $FFFF In 8-bit mode (M=1): - Only the low 8-bit bits of counter are used - IRQ is triggered when low 8 bits of IRQ counter are incremented from $FF - Incrementing the low bits *never* alters the high bits of the counter - When low 8 bits wrap, only the low 8 bits are copied from the reload value... high bits remain unchanged - Reloading via $C000 write will still reload all 16 bits. ================================================ FILE: docs/mapper/074.txt ================================================ ======================== = Mapper 074 = ======================== aka: -------------------------- Pirate MMC3 variant Example Games: -------------------------- Di 4 Ci - Ji Qi Ren Dai Zhan Ji Jia Zhan Shi Notes: -------------------------- This mapper is a modified MMC3 (or is based on MMC3?). In addition to any CHR-ROM present, there is also an additional 2k of CHR-RAM which is selectable. CHR pages $08 and $09 select CHR-RAM, other pages select CHR-ROM Apart from that, this mapper behaves exactly like your typical MMC3. See mapper 004 for details. ================================================ FILE: docs/mapper/075.txt ================================================ ======================== = Mapper 075 = ======================== aka: -------------------------- VRC1 Example Games: -------------------------- Tetsuwan Atom Ganbare Goemon! - Karakuri Douchuu Registers: -------------------------- Range,Mask: $8000-FFFF, $F000 $8000: [.... PPPP] PRG Reg 0 (8k @ $8000) $A000: [.... PPPP] PRG Reg 1 (8k @ $A000) $C000: [.... PPPP] PRG Reg 2 (8k @ $C000) $9000: [.... .BAM] Mirroring, CHR reg high bits M = Mirroring (0=Vert, 1=Horz) A = High bit of CHR Reg 0 B = High bit of CHR Reg 1 $E000: [.... CCCC] Low 4 bits of CHR Reg 0 (4k @ $0000) $F000: [.... CCCC] Low 4 bits of CHR Reg 1 (4k @ $1000) PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | $8000 | $A000 | $C000 | { -1} | +-------+-------+-------+-------+ CHR Setup: --------------------------- CHR regs are 5 bits wide. The low 4 bits of each reg are set by $E000 and $F000, and the high bit is taken from the appropriate bits of $9000. $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------------------------------+-------------------------------+ | $9000.1 + $E000 | $9000.2 + $F000 | +-------------------------------+-------------------------------+ ================================================ FILE: docs/mapper/076.txt ================================================ ======================== = Mapper 076 = ======================== Example Games: -------------------------- Digital Devil Story - Megami Tensei Notes: --------------------------- This mapper is a rewire of the Namcot 108 mapper IC to increase CHR to 128k. The trade off is coarser CHR banking. Registers: --------------------------- Range,Mask: $8000-FFFF, $8001 $8000: [.... .AAA] A = Address for use with $8001 $8001: [..DD DDDD] Data port: R:2 -> CHR reg 0 (2k @ $0000) R:3 -> CHR reg 1 (2k @ $0800) R:4 -> CHR reg 2 (2k @ $1000) R:5 -> CHR reg 3 (2k @ $1800) R:6 -> PRG reg 0 (8k @ $8000) R:7 -> PRG reg 1 (8k @ $a000) CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------+---------------+---------------+---------------+ | R:2 | R:3 | R:4 | R:5 | +---------------+---------------+---------------+---------------+ PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | R:6 | R:7 | { -2} | { -1} | +-------+-------+-------+-------+ ================================================ FILE: docs/mapper/077.txt ================================================ ======================== = Mapper 077 = ======================== Example Game: -------------------------- Napoleon Senki Notes: --------------------------- This mapper uses an 8 KiB SRAM to provide both 6 KiB of CHR-RAM and four-screen mirroring. Registers: (** BUS CONFLICTS **) --------------------------- $8000-FFFF: [CCCC PPPP] C = CHR Reg (2k @ $0000) P = PRG Reg (32k @ $8000) CHR Setup: --------------------------- CHR-RAM is fixed at $0800-$1FFF. CHR-ROM is swappable at $0000: $0000-$0400 $0800-$0C00 $1000-$1400 $1800-$1C00 $2000-$2400 $2800-$2C00 +-----------+-----------+-----------+-----------+-----------+-------------+ | $8000,ROM | {1},RAM | {2},RAM | {3},RAM | {0},RAM |Internal VRAM| +-----------+-----------+-----------+-----------+-----------+-------------+ When making an emulator, you do not need to care about the specific order of the CHR-RAM banks: just provide 10KiB from $0800-$2FFF. ================================================ FILE: docs/mapper/078.txt ================================================ ======================== = Mapper 078 = ======================== Example Games: -------------------------- Holy Diver Uchuusen - Cosmo Carrier Notes: --------------------------- This mapper number covers two seperate mappers which are *almost* identical... however the mirroring control on each is different (making them incompatible). You'll probably have to do a CRC or Hash check to figure out which mirroring setup to use. I think some emus might also look at the mirroring bit in the iNES header to determine which setup to use -- however the ROMs I have do not seem to have the mirroring bit set differently, so I don't know how well that would work (not to mention it's probably not a good idea anyway). Registers: (** BUS CONFLICTS **) --------------------------- $8000-FFFF: [CCCC MPPP] C = CHR Reg (8k @ $0000) P = PRG Reg (16k @ $8000) M = Mirroring: --For Uchuusen - Cosmo Carrier-- 0 = 1ScA 1 = 1ScB --For Holy Diver-- 0 = Horz 1 = Vert PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | $8000 | { -1} | +---------------+---------------+ ================================================ FILE: docs/mapper/079.txt ================================================ ======================== = Mapper 079 = ======================== Example Games: -------------------------- Blackjack Dudes with Attitude F-15 City War Krazy Kreatures Registers: --------------------------- Range,Mask: $4100-5FFF, $4100 be sure to make note of the mask -- $4200 does not map to the register, but $4300 does. $4100: [.CPP PCCC] C = CHR Reg (8k @ $0000) P = PRG Reg (32k @ $8000) Note the high bit of the CHR Reg. ================================================ FILE: docs/mapper/080.txt ================================================ ======================== = Mapper 080 = ======================== Example Games: -------------------------- Kyonshiizu 2 Minelvaton Saga Taito Grand Prix - Eikou heno License Notes: --------------------------- Regs appear at $7EFx, I'm unsure whether or not PRG-RAM can exist at $6000-7EFF Fudou Myouou Den is often marked to use this mapper -- however it uses mapper 207. Registers: --------------------------- $7EF0-7EF5: CHR Regs $7EF6,7EF7: [.... ...M] Mirroring 0 = Horz 1 = Vert $7EF8,7EF9: Internal RAM permission ($A3 enables reads/writes; any other value disables) $7EFA,7EFB: PRG Reg 0 (8k @ $8000) $7EFC,7EFD: PRG Reg 1 (8k @ $A000) $7EFE,7EFF: PRG Reg 2 (8k @ $C000) $7F00-7FFF: 128 Bytes of RAM, mirrored once. CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------+---------------+-------+-------+-------+-------+ | <$7EF0> | <$7EF1> | $7EF2 | $7EF3 | $7EF4 | $7EF5 | +---------------+---------------+-------+-------+-------+-------+ PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | $7EFA | $7EFC | $7EFE | { -1} | +-------+-------+-------+-------+ ================================================ FILE: docs/mapper/082.txt ================================================ ======================== = Mapper 082 = ======================== Example Games: -------------------------- SD Keiji - Blader Kyuukyoku Harikiri Stadium Notes: --------------------------- Regs appear at $7EFx, I'm unsure whether or not PRG-RAM can exist at $6000-7FFF Registers: --------------------------- $7EF0-7EF5: CHR Regs $7EF6: [.... ..CM] CHR Mode/Mirroring C = CHR Mode select M = Mirroring: 0 = Horz 1 = Vert $7EFA: [PPPP PP..] PRG Reg 0 (8k @ $8000) $7EFB: [PPPP PP..] PRG Reg 1 (8k @ $A000) $7EFC: [PPPP PP..] PRG Reg 2 (8k @ $C000) CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------+---------------+-------+-------+-------+-------+ CHR Mode 0: | <$7EF0> | <$7EF1> | $7EF2 | $7EF3 | $7EF4 | $7EF5 | +---------------+---------------+---------------+---------------+ CHR Mode 1: | $7EF2 | $7EF3 | $7EF4 | $7EF5 | <$7EF0> | <$7EF1> | +-------+-------+-------+-------+---------------+---------------+ PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | $7EFA | $7EFB | $7EFC | { -1} | +-------+-------+-------+-------+ Note: remember that the low 2 bits are not used (right-shift written values by 2) ================================================ FILE: docs/mapper/085.txt ================================================ ======================== = Mapper 085 = ======================== aka -------------------------- VRC7 Example Games: -------------------------- Lagrange Point Tiny Toon Adventures 2 (J) VRC7a vs. VRC7b -------------------------- Lagrange Point ('VRC7a') and Tiny Toon Adventures 2 ('VRC7b') both operate exactly the same, but are wired a bit differently. VRC7a uses $x010 for regs, and VRC7b uses $x008. Registers below are listed as they exist on VRC7a. For VRC7b, make the appropriate adjustments Also, only Lagrange Point seems to use the extra sound. It's unknown whether or not the sound hardware exists on VRC7b, as Tiny Toon doesn't use it. CHR-RAM note: -------------------------- Lagrange Point, for some reason I still don't understand, swaps its 8k CHR-RAM around. How this offers any functionality is beyond me, but the game does it, so your emu must support it. Registers: -------------------------- $8000: PRG Reg 0 (8k @ $8000) $8010: PRG Reg 1 (8k @ $A000) $9000: PRG Reg 2 (8k @ $C000) $9010: Sound Address Reg (see below) $9030: Sound Data Port (see below) $A000-$D010: CHR Regs $E000: [.... ..MM] Mirroring: %00 = Vert %01 = Horz %10 = 1ScA %11 = 1ScB $E010: [IIII IIII] IRQ Reload value $F000: [.... .MEA] IRQ Control $F010: [.... ....] IRQ Acknowledge PRG Setup: -------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | $8000 | $8010 | $9000 | { -1} | +-------+-------+-------+-------+ CHR Setup: -------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------+-------+-------+-------+-------+-------+-------+-------+ | $A000 | $A010 | $B000 | $B010 | $C000 | $C010 | $D000 | $D010 | +-------+-------+-------+-------+-------+-------+-------+-------+ IRQs: -------------------------- VRC7 uses the "VRC IRQ" setup shared by several VRCs. It uses the following registers: $E010: [IIII IIII] IRQ Reload $F000: [.... .MEA] IRQ Control $F010: [.... ....] IRQ Acknowledge For info on how these IRQs work, see the "VRC IRQs" section in mapper 021 ---------------------------------------------------------------------------- ---------------------------------------------------------------------------- ----- VRC7 Sound ------------------------------------------------- ---------------------------------------------------------------------------- ---------------------------------------------------------------------------- VRC7 has additional sound channels! It is a slightly dumbed down version of the YM2413 (aka OPLL). There are only 6 harmony channels and no rhythmic channels. Strap yourself in. FM-Synth is a beast. --------------------------------------------- Disclaimers: --------------------------------------------- Information here is pieced together from the Yamaha YM2413 Application Manual ("YM2413.pdf"), and Mitsutaka Okazaki's "emu2413.c" emulator. Anyone whose looked at those sources know they are not the easiest things to comprehend without prior experience with FM synth, so here I attempt to explain things in a more traditional form. I don't really care about YM2413 (I hate FM synth... I find it extremely ugly), so I only cover items on the VRC7 here (ie: no rhythmic information). If you want details about a full YM2413, you'll have to look elsewhere. I am NOT confident about this information being 100% accurate. I made every effort to be as accurate as possible, and my implementation based on the below info sounds *very close* to recordings of the real thing, but I do hear some subtle differences. I graciously welcome any corrections anyone can offer. Bitwidths of various counters are kind of an educated guess. With the exception of the phase accumulator, which is the only counter whose size is hinted at in the documentation... so I'm fairly certain it is in fact 18 bits wide. I mention the use of various lookup tables. I do not know if these lookup tables actually exist on the hardware, or if the values are calculated at runtime. Likewise the actual size of these lookup tables is entirely unknown to me. You can choose your own size in your implementation. --------------------------------------------- FM-Synth basics & other fundamental concepts: --------------------------------------------- The basic idea of FM-Synth is you have 2 sine waves (aka, "slots"), a "modulator" and a "carrier". The output of the carrier is what you actually hear. The output of the modulator alters the frequency of the carrier, effectively acting like a supersonic vibrato. This bends and twists the carrier's waveform into a myriad of different shapes, producing all kinds of different sounds. Each of the 6 channels have 2 slots (a Carrier and a Modulator). Each slot behaves independently and has its own settings and counters. Note that I will refer to "slots" often in these docs. Do not confuse a slot for the whole channel. "ADSR" stands for Attack/Decay/Sustain/Release. These represent 4 phases of amplitude (volume) changes in synthesized audio. This is a common technique in all synth audio (not just FM-Synth). - Attack is when the tone begins, and you have a rapid increase in volume, increasing to *above* the desired output level. - Decay is when attack has reached its maximum, and the volume starts to decline to the desired output level. - Sustain is when the volume has reached the desired level. It holds the volume at that level for as long as the tone is to be played. Although sometimes the volume might slowly drop. - Release is when the tone is done, and volume gradually decreases until it's completely silent. "Key on" / "Key off" represents the entry and exit into ADSR. You can think of it like a piano or a keyboard... when you "key on", you are pressing a key, and when you "key off" you are releasing a key. Effectively, this means that when you key on, you enter "Attack", and when you key off, you enter "Release". --------------------------------------------- Volume and Attenuation: --------------------------------------------- VRC7 doesn't really have a concept of an output volume. Instead, it does everything with "attenuation", which is basically the opposite of volume. Attenuation is like a forced reduction -- so high attenuation means low output. Zero attenuation means the output is as high as possible. All attenuation levels are expressed in decibels (dB), which is a logarithmic (non-linear) scale. VRC7's threshhold or maximum attenuation is 48 dB. This means that at 48 dB, output is zero. Note that even though dB are non-linear, you can still work with them as if they were linear. That is, 10dB + 10dB is still 20dB. The only thing is that when converted to linear units, 20dB is MUCH MUCH more than 2x 10dB. Since VRC7 handles all its output levels in terms of dB, this means you will only need to convert from dB to linear units in exactly one place: when determining the linear output of the "slot". Converting dB <-> Linear can be accomplished with the below formulas: dB = -20 * log10( Linear ) * scale (if Linear = 0, dB = +inf) Linear = 10 ^ (dB / -20 / scale) 'scale' is an optional factor you can use to scale up dB so that they're in an easier to use base. I recommend using (1<<23)/48 for a scale (this would mean that 1<<23 would represent 48 dB). This will make envelope calculations much easier (see Envelope Generation section for details). Remember the threshhold is 48 dB. So if you have 48 dB or higher, Linear=0. --------------------------------------------- Clock rate: --------------------------------------------- VRC7 has its own oscillator to drive the clock rate. It's clocked at 3.6 MHz (exactly 2x the NTSC NES CPU clock rate), but those clocks are divided by 72, effectively making the rate at which each individual unit is clocked 49715.90909 Hz. I find it very likely that clocking each individual unit is done serially across the 72 cycles, but the effect that detail has on the generated audio is tiny to the point of being insignificant. To think of this in terms of CPU cycles, you could say that all units are clocked once every 36 CPU cycles on NTSC. However, this is techncially inaccurate, as the NES clock does not drive the VRC7. And on PAL systems, the clock rate doesn't sync up like that. --------------------------------------------- Registers: --------------------------------------------- Register descriptions to follow. Details as to what each field actually does will not be covered here but will be explained in future sections. $9010: [..AA AAAA] A = Address for use with $9030 $9030: [DDDD DDDD] -- data port R:00-R:07 -> Custom instrument settings (see below) R:1x: [FFFF FFFF] (where x=0-5, selecting the channel) F = low 8 bits of F-Num (frequency control) R:2x: [..SK BBBF] (where x=0-5, selecting the channel) F = high bit of F-Num B = Block select (or octave) K = Key on (1=key on, 0=key off) S = Sustain On (poorly named, has no impact on Sustain mode -- actually affects Release) R:3x: [IIII VVVV] I = Instrument select V = 'Volume' (poorly named, it's more like "Carrier Base Attenuation Level") (regs R:1x, 2x, and 3x apply to both Carrier and Modulator regs R:0x apply differently to each ) R:00: [AFPK MMMM] (applies to Modulator) R:01: [AFPK MMMM] (applies to Carrier) A = Enable Amplitude Modulation (AM) F = Enable Frequency Modulation (FM) P = Disable Percussive Mode (0=percussive, 1=normal) K = Key Scale Rate (KSR) M = 'MULTI' Freqency multiplier R:02: [KKLL LLLL] K = Modulator Key Scale Level (KSL) L = Modulator base attenuation level R:03: [KK.C MFFF] K = Carrier Key Scale Level (KSL) C = Carrier rectify sine wave (0=full sine wave, 1=half sine wave) M = Modulator rectify sine wave F = Modulator Feedback level R:04: [AAAA DDDD] (Modulator) R:05: [AAAA DDDD] (Carrier) A = Attack Rate D = Decay Rate R:06: [SSSS RRRR] (Modulator) R:07: [SSSS RRRR] (Carrier) S = Sustain Level R = Release Rate There are 16 selectable instruments (selected via R:3x). Instrument 0 is configurable via regs R:00 through R:07. The other instruments are fixed at the below values: 0x03,0x21,0x04,0x06,0x8D,0xF2,0x42,0x17 // instrument 1 0x13,0x41,0x05,0x0E,0x99,0x96,0x63,0x12 // instrument 2 0x31,0x11,0x10,0x0A,0xF0,0x9C,0x32,0x02 // instrument 3 0x21,0x61,0x1D,0x07,0x9F,0x64,0x20,0x27 // instrument 4 0x22,0x21,0x1E,0x06,0xF0,0x76,0x08,0x28 // instrument 5 0x02,0x01,0x06,0x00,0xF0,0xF2,0x03,0x95 // instrument 6 0x21,0x61,0x1C,0x07,0x82,0x81,0x16,0x07 // instrument 7 0x23,0x21,0x1A,0x17,0xEF,0x82,0x25,0x15 // instrument 8 0x25,0x11,0x1F,0x00,0x86,0x41,0x20,0x11 // instrument 9 0x85,0x01,0x1F,0x0F,0xE4,0xA2,0x11,0x12 // instrument A 0x07,0xC1,0x2B,0x45,0xB4,0xF1,0x24,0xF4 // instrument B 0x61,0x23,0x11,0x06,0x96,0x96,0x13,0x16 // instrument C 0x01,0x02,0xD3,0x05,0x82,0xA2,0x31,0x51 // instrument D 0x61,0x22,0x0D,0x02,0xC3,0x7F,0x24,0x05 // instrument E 0x21,0x62,0x0E,0x00,0xA1,0xA0,0x44,0x17 // instrument F **** SIDE NOTE **** Writing to Regs R:00 through R:07 do NOT seem to have an immediate effect on channels using instrument 0. Lagrange Point (Track 2 of the NSF) will write to these regs while a channel using instrument 0 is still keyed on and audible, resulting in an ugly and very noticable "blurp" noise at the end of a note. This is not heard on the real hardware, so instrument data must be cached somehow. Perhaps it only takes effect when the channel is keyed on, or when R:3x is written to? Don't know exactly. --------------------------------------------- Phase / Frequency Calculation: --------------------------------------------- Each slot has an 18-bit up counter which determines the current phase (position in the sine wave). Each clock, this counter is incremented: phase += F * (1 << B) * M * V / 2 where: F = 9-bit F-num of the channel B = 3-bit Block of the channel M = see below V = vibrato (FM) output R:00 or R:01 specify a 4 bit 'MULTI' value. That MULTI value is run through the below LUT to get 'M': MULTI: 0 1 2 3 4 5 6 7 8 9 A B C D E F (hex) M: 1 2 4 6 8 10 12 14 16 18 20 20 24 24 30 30 (dec) If FM is enabled for the slot (see R:00 or R:01 for the enable bit), 'V' is the output of the FM unit. See AM/FM section for details. If FM is disabled, 'V' = 1 Another 'phase_secondary' value is used to actually generate the phase: phase_secondary = phase + adj For the Carrier: adj = the output of the Modulator. Note that slot output is 20-bits wide, but the phase is only 18 bits wide... this means that the high 2 bits of the modulator output are effectively dropped. For the Modulator: R:03 has a 3-bit 'F' value specifying the feedback level. if F=0: adj = 0 otherwise: adj = previous_output_of_modulator >> (8 - F) The bits of the phase_secondary value are extracted and used to generate the sine wave: phase_secondary: [RI IIII III. .... ....] R: rectification bit I: index to half-sine lookup table ** 'I' may be more or less bits depending on how big your half-sine lookup table is ** 'R' determines what to do with output after it's been converted to a linear level. See next section for details of this bit, and details of the half-sine table. --------------------------------------------- Attenuation / Output calculation: --------------------------------------------- The attenuation level determines the slot output on each clock. Attenuation level is determined as follows: TOTAL = half_sine_table[I] + base + key_scale + envelope + AM half_sine_table[I]: -------------- The half-sine table mentioned in the previous section does not actually hold the output of the sine function. Rather, it holds the attenuation level of the sine function. Example: sin(pi/2) = 1 ~~~> I='0100 0000' ~~~> half_sine_table[ I ] = 0 dB sin(0) = 0 ~~~> I='0000 0000' ~~~> half_sine_table[ I ] = +inf dB This table is effectively: half_sine_table[I] = Convert_Linear_To_dB( sin( pi * I / (1 << bitwidth_of_I) ) ) base: -------------- For Modulator: base = (0.75 * L), where L is the 6-bit base level (see register R:02) For Carrier: base = (3.00 * L), where L is the 4-bit 'volume' (see register R:3x) key_scale: -------------- Key Scale Level, 'K', is a 2-bit value (see regs R:02, R:03) that adds attenuation as the pitch of the tone increases (ie: higher pitches = quieter). If K=0: key_scale=0 Otherwise: F = high 4 bits of the current F-Num B = 3-bit Block (Octave) A = table[ F ] - 6 * (7-B) if A < 0: key_scale = 0 otherwise: key_scale = A >> (3-K) table: F: $0 $1 $2 $3 $4 $5 $6 $7 $8 $9 $A $B $C $D $E $F A: 0.00 18.00 24.00 27.75 30.00 32.25 33.75 35.25 36.00 37.50 38.25 39.00 39.75 40.50 41.25 42.00 envelope: -------------- Output of the envelope generator. See Envelope Generation section for details. AM: -------------- If Amplitude modulation is enabled for the slot (see R:00, R:01), AM is the output of the amplitude modulation unit. Otherwise, AM=0. See AM/FM section for details. Finally... after all that, we have our 'TOTAL'. This is the total attenuation for the slot. 1) This attenutation is then converted to linear units to get the preliminary output. This is scaled up to a 20-bit value 2) If the high bit ('R') of the 18-bit 'phase_secondary' value (see previous section) is set, this means we are in the negative portion of the sine wave, which means output needs to be negated. However, if we are rectifying to a half sine wave (see R:03), output is zero'd instead. 3) Output is then run through a filter which averages this output with the previous clock's output 4) The result is the FINAL, actual output. Pseudo-code to clarify: total = half_sine_table[I] + base + key_scale + envelope + AM prevoutput = output // 1) output = convert_dB_to_Linear( total ) * (1<<20) // 2) if R: if halfsine: output = 0 else: output = -output // 3) FINAL = (output + prevoutput) / 2 'FINAL' is what the slot actually outputs. This is a 20-bit value. The modulator's output will be sent to the carrier, and the carrier's output will be audible (though you will want to scale it down... 20-bit audio is crazy loud when ouputting 16-bit samples). 'FINAL' is also the value used when calculating the modulator's feedback (see prev section). --------------------------------------------- Envelope Generation: --------------------------------------------- Each slot has a 23-bit up counter (hereon 'EGC') for envelope generation, very similar to the 18-bit phase counter. It determines the output of the envelope generator... which adds attenuation to the output (see previous section). The envelope generator operates as an ADSR unit. When the channel is keyed on, both the Carrier and the Modulator enter the Attack Phase. When keyed off, they enter Release phase. When the ADSR unit completes a full ADSR cycle, it enters a 5th 'Idle' phase. EGC is incremented every clock. The value by which it's incremented depends on which phase of ADSR we're in. Those rates are then adjusted by a 'Key Scale Rate' factor (see R:00, R:01). EGC also serves as the direct output of the envelope generator (except in the Attack phase). When EGC=0, output is 0 dB, and whdn EGC=(1<<23), output is 48 dB. Because of this, I recommend scaling all units in your emulator to work with dB in this (1<<23)/48 base. Doing so results in minimal unit conversion. Formula for determining the rate to increase EGC: BF = (3-bit Channel Block << 1) + high bit of F-Num... forming a 4-bit value K = Key Scale Rate bit (see R:00, R:01) if K: KB = BF otherwise: KB = BF >> 2 R = base rate (see subsections below) RKS = R*4 + KB RH = RKS >> 2 (if RH > 15, use RH=15) RL = RKS & 3 The subsections below will provide a value for R, then will use RH and RL to determine the rate by which EGC is incremented. Note that if R=0, then EGC is not incremented at all. Attack: ------- R = slot attack rate (4-bits as written to R:04, R:05) EGC += (12 * (RL+4)) << RH Once EGC wraps, reset EGC to zero and enter Decay phase Decay: ------- R = slot decay rate (4-bits as written to R:04, R:05) EGC += (RL+4) << (RH-1) Once output level reaches the slot sustain level (see R:06, R:07), set EGC to the sustain level (do not reset it to 0!), and enter Sustain phase. The sustain level is (3 dB * L), where L is the 4-bit value written to the register. This means you enter Sustain when EGC >= (3 * L * (1<<23) / 48) Sustain: ------- If slot is percussive (see R:00, R:01): R = slot RELEASE rate (R:06, R:07, low bits) otherwise: R = 0 EGC += (RL+4) << (RH-1) When EGC reaches (1<<23), output is fixed at 48 dB and enter Idle phase Release: ------- If channel has "Sustain On" set (see R:2x), R = 5 otherwise, if slot is percussive: R = slot release rate (R:06, R:07) otherwise: R = 7 EGC += (RL+4) << (RH-1) When EGC reaches (1<<23), output is fixed at 48 dB and enter Idle phase Idle: ------- R=0 EGC not incremented Output fixed at 48 dB As previously mentioned, the output of the envelope generator is EGC, except in Attack phase. In Attack, the actual rate of attack is logarithmic (it also decreases attenuation, rather than increasing it). attack_output = 48 dB - (48 dB * ln(EGC) / ln(1<<23)) (ln = natural log) --------------------------------------------- Key On / Key Off: --------------------------------------------- R:2x has the Key On bit for the channel. This bit only has an impact when its state transitions. Upon transition, do the following for both Carrier and Modulator: When being set (0->1): (key on) - Reset EGC to zero - Reset 18-bit phase counter to zero - Enter Attack phase When being clear (1->0): (key off) - If currently in attack, EGC must be set to the current output level - Enter Release phase --------------------------------------------- AM/FM: --------------------------------------------- There is one AM unit and one FM unit. The output of these units are shared across all slots. Both units have a 20-bit counter that is increased by 'rate' every clock. sinx = sin(2 * pi * counter / (1<<20)) AM unit: 'rate' = 78 AM_output = (1.0 + sinx) * 0.6 dB (emu2413 uses 1.2 dB instead of 0.6, but that sounds way too steep to me) See the "Attenuation / Output calculation" section for how this output is applied FM unit: 'rate' = 105 FM_output = 2 ^ (13.75 / 1200 * sinx) (note: '^' is exponent, not xor) See the "Phase / Frequency Calculation" section for how this output is applied ================================================ FILE: docs/mapper/086.txt ================================================ ======================== = Mapper 086 = ======================== Example Games: -------------------------- Moero!! Pro Yakyuu (Black) Moero!! Pro Yakyuu (Red) Notes: --------------------------- Regs are at $6000-7FFF, so these games have no SRAM. Registers: -------------------------- $6000-6FFF: [.CPP ..CC] P = Selects 32k PRG @ $8000 C = Selects 8k CHR @ $0000 $7000-7FFF: [..SS IIII] Sound control **not sure about this** S = Sound Start/Stop: %10 = start sound effect? anything else = stop sound effect? I = Sound effect ID Sound: -------------------------- This mapper has some sort of sample playback mechanism. Writing to $7xxx starts/stops the currently playing sample. How this playback works (apart from the sketchy notes above) is a complete mystery to me. The sound effects themselves are NOT PART of the .nes file. Therefore the only real way to support them currently would be to load external .wav files or something and play them back when they're triggered. ================================================ FILE: docs/mapper/087.txt ================================================ ======================== = Mapper 087 = ======================== Example Games: -------------------------- Argus (J) City Connection (J) Ninja Jajamaru Kun Notes: --------------------------- Regs are at $6000-7FFF, so these games have no SRAM. Registers: -------------------------- $6000-7FFF: [.... ..AB] B = High CHR Bit A = Low CHR Bit This reg selects 8k CHR @ $0000. Note the reversed bit orders. Most games using this mapper only have 16k CHR, so the 'B' bit is usually unused. ================================================ FILE: docs/mapper/088.txt ================================================ ======================== = Mapper 088 = ======================== Example Games: -------------------------- Quinty (J) Namcot Mahjong 3 Dragon Spirit - Aratanaru Densetsu Registers: --------------------------- Range,Mask: $8000-FFFF, $8001 $8000: [.... .AAA] Address for use with $8001 $8001: [DDDD DDDD] Data port: R:0 -> CHR reg 0 R:1 -> CHR reg 1 R:2 -> CHR reg 2 R:3 -> CHR reg 3 R:4 -> CHR reg 4 R:5 -> CHR reg 5 R:6 -> PRG reg 0 R:7 -> PRG reg 1 CHR Setup: --------------------------- CHR is split into two halves. $0xxx can only have CHR from the first 64k, $1xxx can only have CHR from the second 64k. $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------+---------------+-------+-------+-------+-------+ | | | R:2 | R:3 | R:4 | R:5 | +---------------+---------------+-------+-------+-------+-------+ | | | | AND written values with $3F | OR written values with $40 | PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | R:6 | R:7 | { -2} | { -1} | +-------+-------+-------+-------+ ================================================ FILE: docs/mapper/089.txt ================================================ ======================== = Mapper 089 = ======================== Example Games: -------------------------- Mito Koumon Registers: (**BUS CONFLICTS**) -------------------------- $8000-FFFF: [CPPP MCCC] C = Select 8k CHR @ $0000 P = Select 16k PRG @ $8000 M = Mirroring: 0 = 1ScA 1 = 1ScB PRG Setup: -------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | $8000 | { -1} | +---------------+---------------+ ================================================ FILE: docs/mapper/090.txt ================================================ ======================== = Mapper 090 = = + 209 = ======================== aka -------------------------- Tekken 2 Pirate Cart a big fat pile of ass Example Games: -------------------------- Tekken 2 (090) Mortal Kombat 2 (090) Super Contra 3 (090) Super Mario World (090) Shin Samurai Spirits 2 (209) Rants: --------------------------- This mapper is such a big pain in the ass. Not only is it overly complicated in every possible way, but every single game that uses it SUCKS. Plus the composers for these horrible pirate games must have been tone-deaf, because the music is always out of key. I freaking hate this mapper with a passion (can you tell?). 090 vs. 209 --------------------------- 209 split from 090 somewhere along the line... but at some time, 090 was shared by both. Therefore you may come across ROMs mislabelled as 090 that are actually 209. The two mappers are exactly the same. The only difference is a jumper setting which controls the extended nametable control. 090 has extended NT control permanently disabled, 209 has it enabled. Why this is in a jumper setting I don't know... since the game can already freaking enable/disable the mode through software! Regardless... two mappers are needed because some games that don't use the NT control don't disable it through software (they rely on the jumper setting disabling it). This doc, as a whole, applies to both 090 and 209 -- with the exception of the Mirroring section, which draws the distinctions between the two. Notes: --------------------------- This mapper has no PRG-RAM. As suprising as that is. In addition to the above mentioned jumper setting that controls mirroring, there are 2 other dipswitch settings which can be read back by the game via reg $5000. Changing the dipswitch can change the game being played in some ROMs (really, it's more or less the same game, just with slight differences). This document's organization: --------------------------- Since there are so many registers for this mapper, registers will be listed and outlined as the features are explained... and the overall registers section will be extremely brief -- serving primarily as a very quick reference or checklist. PRG Setup: --------------------------- $8000-8003 are PRG regs. $8004-8007 are mirrors of them. $8000-$8003: [.PPP PPPP] $D000 is the PRG mode select (among other things): $D000: [SRNC CPPP] R,N = Relate to Mirroring (see mirroring section for details) C = Relate to CHR Setup (see chr setup for details) S = Put PRG @ $6000-7FFF P = PRG Mode Select If 'S' is clear, $6000-7FFF is always open bus. It is only when 'S' is set, that $6000 reflects the page indicated in the setup chart below. Notice that page numbers are "actual" pages. Some modes are bit reversed (as marked below). This means that the PRG registers are to be interpretted backwards: [.ABC DEFG] normal order [.GFE DCBA] bit reversed order $6000 $8000 $A000 $C000 $E000 +-----------------+-------------------------------+ PRG Mode %000 | ($8003 * 4)+3 | { -1} | +-----------------+-------------------------------+ PRG Mode %001 | ($8003 * 2)+1 | $8001 | { -1} | +-----------------+---------------+---------------+ PRG Mode %010 | $8003 | $8000 | $8001 | $8002 | { -1} | +-----------------+-------+-------+-------+-------+ PRG Mode %011 | $8003 | $8000 | $8001 | $8002 | { -1} | *BIT REVERSE* +-----------------+-------------------------------+ PRG Mode %100 | ($8003 * 4)+3 | $8003 | +-----------------+-------------------------------+ PRG Mode %101 | ($8003 * 2)+1 | $8001 | $8003 | +-----------------+---------------+---------------+ PRG Mode %110 | $8003 | $8000 | $8001 | $8002 | $8003 | +-----------------+-------+-------+-------+-------+ PRG Mode %111 | $8003 | $8000 | $8001 | $8002 | $8003 | *BIT REVERSE* +-----------------+-------+-------+-------+-------+ In case you don't see the patterns: - PRG modes %1xx are the same as %0xx, only $8003 is used for the last page instead of {-1} - $6000 is always swapped to the last 8k in the block specified by $8003. In %1xx modes, this means $6000 will always mirror $E000. CHR Setup: --------------------------- $9000-9007 are CHR regs -- each specifies the low 8 bits of the CHR page $A000-A007 -- specifies the high 8 bits of the CHR page (work with above regs) The rest of this section refers to above regs as $900x only -- but note that it all includes $900x and $A00x. CHR Mode is set by the following: $D000: [SRNC CPPP] R,N = Relate to Mirroring (see mirroring section for details) S,P = Relate to PRG (see prg setup for details) C = CHR Mode $D003: [M.BH HHHH] M = Mirror CHR (very strange, see below) B = CHR Block mode (0=enabled, 1=disabled) H = CHR Block (when in block mode) In CHR Block mode ('B' clear), $A00x is ignored, and instead, the H bits selects a 256k block for all CHR. $9000-9007 select a page within that block. In normal mode ('B' set), $9000-9007 select a page from the entire CHR. Mirror CHR mode ('M' set), only takes effect when in 1k or 2k mode ('C' = %10 or %11). In this mode, $0800-$0FFF always mirrors $0000-07FF. ($1800 is unaffected, however). This is relatively easily emulatable by using $9000+$9001 in place of $9002+$9003 in the chart below. Note that page numbers are in "actual" pages. $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------------------------------------------------------+ CHR Mode %00: | $9000 | +---------------------------------------------------------------+ CHR Mode %01: | $9000 | $9004 | +-------------------------------+-------------------------------+ CHR Mode %10: | $9000 | $9002 | $9004 | $9006 | +---------------+---------------+---------------+---------------+ CHR Mode %11: | $9000 | $9001 | $9002 | $9003 | $9004 | $9005 | $9006 | $9007 | +-------+-------+-------+-------+-------+-------+-------+-------+ Mirroring: --------------------------- At first glance... mirroring appears simple: $D001: [.... ..MM] %00 = Vert %01 = Horz %10 = 1ScA %11 = 1ScB However there is a special setting to complicate this, of course. Note! For mapper 090, the above is it! None of the below special mirroring stuff applies. The below mirroring info applies *only* to mapper 209. $D000: [SRNC CPPP] S,P = Relate to PRG (see prg setup for details) C = Relates to CHR (see chr setup for details) N = Enable advanced NT control (0=disabled, 1=enabled) R = disable NT RAM (0=NT can be RAM or ROM, 1=NT ROM only) When 'N' is clear, $D001 controls mirroring, and all other mirroring regs are ignored (including 'R' bit of $D000). When 'N' is set, $D001 is ignored, and the below regs control mirroring. $D002: [A... ....] NT RAM select bit $B000-B003 NT Regs (low 8 bits) $B004-B007 NT Regs (high 8 bits) Just like with normal CHR Regs, NT CHR regs are 16-bits... $B000-B003 specify the low bits, and $B004-B007 specify the high bit. They are arranged in the following: [ $B000 ][ $B001 ] [ $B002 ][ $B003 ] When 'R' is set, $D002 is ignored, and CHR-ROM is always used as NT (with page selected by appropriate reg). When 'R' is clear... CHR-ROM is only used if bit 7 of the NT Reg does not match the 'A' bit of $D002. If the bits match, then NES internal NT RAM is used instead (either NTA or NTB, depending on bit 0 of the NT reg) IRQs --------------------------- IRQs on this mapper are 100% completely insane. They decided to do everything possible in order to make IRQs as obfuscated and ridiculous as possible. IRQs are triggered by any one of 4 sources: 1) CPU Cycles 2) A12 Rises 3) PPU Reads (wtf, I know, but it's true) 4) CPU Writes (wtf, I know, but it's true) I *think* the only method used by any games is the A12. CPU Cycles may also be used... and I really doubt the other two are used anywhere. A12 rises operate just like they do for MMC3 (mapper 004 -- see that doc for details). One key difference: Unlike the MMC3, nearby rises are not ignored. This means that under "normal" conditions, this IRQ counter is clocked 8 times per scanline (not just once). Clocks are first run through a prescaler, which divides the clocks by either 256 or 8 (prescaling by 8 is useful with A12 mode). Also.. the counter can be configured to count up, or count down! Among other oddities. Related regs are as follows: $C001: [DU.. FPSS] D = Count-down mode (0=disabled, 1=enabled) U = Count-up mode (0=disabled, 1=enabled) F = Funky mode (0=disabled, 1=enabled) -- see below P = Prescaler size (0=256, 1=8) S = IRQ source: %00 = CPU Cycles %01 = PPU A12 rising edges %10 = PPU Reads %11 = CPU Writes $C002: [.... ....] Any write here will acknowledge and disable IRQs $C003: [.... ....] Any write here will enable IRQs $C000: [.... ...E] Alternate method: writing to this reg with E=0: same as writing to $C002 writing to this reg with E=1: same as writing to $C003 $C004: [PPPP PPPP] Prescaler. Any write here will set the prescaler to 'P' XOR $C006 $C005: [IIII IIII] IRQ Counter. Any write here will set the IRQ counter to 'I' XOR $C006 $C006: [XXXX XXXX] This value is used as a XOR when writing to $C004/5 $C007: Funky Mode Reg $C004 and $C005 directly change the IRQ counter/prescaler. They do not change a reload value. When Count-up and count-down mode are both enabled, or both disabled, the IRQ counter will stand still. Only one can be enabled for IRQs to work. When the prescaler is in 3-bit mode (divide by 8), the high 5 bits of the prescaler remain unchanged when clocked and only the low 3 bits are used. When the low 3 bits wrap, the IRQ counter is clocked. 8-bit mode (divide by 256) works as you'd expect. When the IRQ counter wraps (either $FF->00 or $00->FF, depending on whether it's incrementing or decrementing), an IRQ is tripped (if enabled). Disabling IRQs does not stop the counter or prescaler from counting, it simply stops the IRQ from being generated. Funky Mode: When 'F' in $C001 is clear, $C007 is ignored. When set, exact operation is unknown. It appears to funkify the prescaler. $C007 containing any value other than $FF will result in the IRQ counter not being clocked at all... and $FF will result in the prescaler dividing by strange amounts (sometimes 8? sometimes 12? sometimes 257?). Details are unknown. Fortunately, no games use this funky mode. Other crap: --------------------------- $5000: [DD.. ....] Dipswitch settings (readable only) These bits can be read back as any value depending on dipswitch settings on the cart. The high bit, in paticular, has an effect in some games. $5800, $5801: 8*8->16 multiplication reg. (read+write) These are similar to MMC5's multiplication reg. You write two values you want multiplied to $5801 and $5800, then the 16-bit product can be read back ($5800 has low 8 bits, $5801 has high 8 bits). Mulitplication is unsigned. Multiplication appears to need some processing time. After writing values, wait 8 CPU cycles before reading. $5803: a single byte of RAM (read+write) $5804-$5807 may also be RAM -- it's unknown. Registers: --------------------------- Registers were all covered in detail in previous sections. This section is just an overall reference/checklist. Range, Mask: $5000-FFFF, $F007 $5000: Dipswitch (read only) $5800-5801: 8*8->16 multiplier (read+write) $5803: RAM (read+write) $5804-5807: ??? (possibly RAM) $8000-8003: PRG Regs $8004-8007: Mirror of PRG Regs $9000-9007: CHR Regs (low bits) $A000-A007: CHR Regs (high bits) $B000-B003: NT Regs (low bits) $B004-B007: NT Regs (high bits) $C000-C007: IRQ Regs $D000-D003: Control/Mode Regs $D004-D007: mirror $D000-D003 ================================================ FILE: docs/mapper/091.txt ================================================ ======================== = Mapper 091 = ======================== Example Games: -------------------------- Street Fighter III Super Mario & Sonic 2 Notes: --------------------------- Regs exist at $6000-7FFF, so this mapper has no SRAM. Registers: --------------------------- Range,Mask: $6000-7FFF, $7003 $6000-6003: CHR Regs $7000-7001: [.... PPPP] PRG Regs $7002 [.... ....] IRQ Stop $7003 [.... ....] IRQ Start CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------+---------------+---------------+---------------+ | $6000 | $6001 | $6002 | $6003 | +---------------+---------------+---------------+---------------+ PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | $7000 | $7001 | { -2} | { -1} | +-------+-------+-------+-------+ IRQs: --------------------------- IRQs on this mapper seem to behave exactly like MMC3 -- except it's fixed so that it will only fire after 8 scanlines. This is easily emulatable by using MMC3 logic. Write to $7002/$7003 can translate directly to write(s) to the following MMC3 registers: on $7002 write: a) write to $E000 on $7003 write: a) write $07 to $C000 b) write to $C001 c) write to $E001 For details on MMC3 IRQ operation, see mapper 004 ================================================ FILE: docs/mapper/092.txt ================================================ ======================== = Mapper 092 = ======================== Example Games: -------------------------- Moero!! Pro Soccer Moero!! Pro Yakyuu '88 - Ketteiban Notes: --------------------------- This mapper is identical to mapper 072 except for the different PRG Setup. See mapper 072 for details. PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | { 0 } | PRG Reg | +---------------+---------------+ ================================================ FILE: docs/mapper/093.txt ================================================ ======================== = Mapper 093 = ======================== Example Games: -------------------------- Fantasy Zone (J) Registers: (**BUS CONFLICTS**) -------------------------- $8000-FFFF: [PPPP ...M] P = PRG Reg (16k @ $8000) M = Mirroring: 0 = Vert 1 = Horz PRG Setup: -------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | $8000 | { -1} | +---------------+---------------+ ================================================ FILE: docs/mapper/094.txt ================================================ ======================== = Mapper 094 = ======================== Example Games: -------------------------- Senjou no Ookami Registers: (**BUS CONFLICTS**) -------------------------- $8000-FFFF: [...P PP..] PRG Reg (16k @ $8000) PRG Setup: -------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | $8000 | { -1} | +---------------+---------------+ ================================================ FILE: docs/mapper/095.txt ================================================ ======================== = Mapper 095 = ======================== aka -------------------------- MMC3 (modified) Example Game: -------------------------- Dragon Buster (J) Notes: --------------------------- This mapper is a modified MMC3. It behaves exactly like your normal MMC3, only mirroring is handled differently. For details on MMC3, refer to mapper 004. Regs: --------------------------- $8000: [CP.. .AAA] C = CHR Mode P = PRG Mode A = Address for $8001 This register operates exactly like it does on your normal MMC3. It is mentioned here because the 'C' bit has another usage for mirroring. The normal mirroring reg ($A000) is totally ignored, and the CHR regs select nametables: When 'C' is set: [ R:2 ][ R:3 ] [ R:4 ][ R:5 ] When 'C' is clear: [ R:0 ][ R:0 ] [ R:1 ][ R:1 ] For mirroring, only bit 5 of the CHR regs is significant. Bit 5 of the appropriate reg selects either NTA or NTB. ================================================ FILE: docs/mapper/096.txt ================================================ ======================== = Mapper 096 = ======================== Example Games: -------------------------- Oeka Kids - Anpanman no Hiragana Daisuki Oeka Kids - Anpanman to Oekaki Shiyou!! Notes: --------------------------- These games use the Oeka kids tablet -- so you'll need to add support for that if you really want to test these. These games use 32k of CHR-RAM, which is swappable in a very unique fashion. Be sure to read the CHR Setup section in detail. Registers: --------------------------- I'm unsure whether or not this mapper suffers from bus conflicts. Use caution! $8000-FFFF: [.... .CPP] C = CHR Block select (see CHR Setup) P = PRG Page select (32k @ $8000) CHR Setup: --------------------------- This mapper is tricky!!! Firstly, this mapper divides the 32k CHR-RAM into two 16k blocks (above 'C' bit selects which block is used). The selected pages (including the fixed page) are taken from only the currently selected 16k block. $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------------------------------+-------------------------------+ | **See below** | { 3 } | +-------------------------------+-------------------------------+ But that's the easy part. This mapper does a very, very cool trick which watches the PPU address lines to effectively "split" the nametable into 4 smaller sections -- thereby assigning a different CHR-RAM page to each section. This allows **every single tile in the NT** to have a unique tile graphic! Long story short: A nametable spans from $2000-$23BF ($23C0-$23FF are the attribute table). The mapper breaks the NT up like so: $2000-20FF = use CHR page 0 $2100-21FF = use CHR page 1 $2200-22FF = use CHR page 2 $2300-23BF = use CHR page 3 the other nametables at $2400, $2800, $2C00 are broken up in the same fashion. Long story long: PPU Address lines are modified as the PPU fetches tiles, and also when the game manually changes the PPU address (via the second write to $2006 --- or by the increment after read/writing $2007). The mapper monitors every change to the PPU Address lines, and when it lies within a certain range, it swaps the appropriate CHR page in. It will only swap CHR when the address falls between $2000-2FFF (or mirrored regions like $6000-6FFF, $A000-AFFF, $E000-EFFF). $3xxx will not trigger a swap. When in that range, it checks to make sure the address is not attribute tables ((Addr AND $03FF) < $03C0). Note I'm not 100% sure if the mapper really does this or not. It's very possible that attribute fetches will also swap CHR... this would not really disrupt anything other than making the game be more careful about its PPU writes. When all that checks out, bits 8 and 9 (Addr AND $0300) select the 4k CHR page to swap in to $0000. Note that the mapper does not distinguish between PPU driven line changes and game driven line changes. This means that games can manually swap the CHR page by doing specific writes to $2006: LDA #$20 STA $2006 STA $2006 ; Addr set to $20xx -- CHR page 0 selected LDA #$21 STA $2006 STA $2006 ; Addr set to $21xx -- CHR page 1 selected And in fact, games would HAVE to do that to select CHR, since that's the only way to fill CHR RAM with the desired data. So make sure your emu supports this. ================================================ FILE: docs/mapper/097.txt ================================================ ======================== = Mapper 097 = ======================== Example Game: -------------------------- Kaiketsu Yanchamaru Registers: -------------------------- I'm not sure whether or not this mapper suffers from bus conflicts. Use caution! $8000-FFFF: [MM.. PPPP] P = PRG Reg (16k @ $C000) M = Mirroring: %00 = 1ScA %01 = Horz %10 = Vert %11 = 1ScB PRG Setup: -------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | { -1} | $8000 | +---------------+---------------+ ================================================ FILE: docs/mapper/105.txt ================================================ ======================== = Mapper 105 = ======================== aka -------------------------- NES-EVENT Example Game: -------------------------- Nintendo World Championships 1990 Notes: --------------------------- This mapper is an MMC1 with crazy wiring and a huge 30-bit CPU cycle driven IRQ counter. Registers are all internal and not directly accessable -- and the latch must be written to 1 bit at a time -- just like on a normal MMC1. For details on how regs are written to, see mapper 001. This mapper has 8k CHR-RAM, and it is not swappable. Registers: --------------------------- Note that like a normal MMC1, registers are internal and not accessed directly. $8000-9FFF: [.... PSMM] Same as MMC1 (but CHR mode bit isn't used) $A000-BFFF: [...I OAA.] I = IRQ control / initialization toggle O = PRG Mode/Chip select A = PRG Reg 'A' $C000-DFFF: [.... ....] Unused $E000-FFFF: [...W BBBB] W = WRAM disable (same as MMC1) B = PRG Reg 'B' Powerup / Reset / Initialization: --------------------------- On powerup and reset, the first 32k of PRG (from the first PRG chip) is selected at $8000 *no matter what*. PRG cannot be swapped until the mapper has been "initialized" by setting the 'I' bit to 0, then to '1'. This toggling will "unlock" PRG swapping on the mapper. Note 'I' also controls the IRQ counter (see below) PRG Setup: --------------------------- There are 2 PRG chips, each 128k. The 'O' bit selects between the chips, and also determines which PRG Reg is used to select the page. O=0: Use first PRG chip (first 128k), use 'A' PRG Reg, 32k swap O=1: Use second PRG chip (second 128k), use 'B' PRG Reg, MMC1 style swap In addition, if the mapper has not been "unlocked", the first 32k of the first chip is always selected regardless (as if $A000 contained $00). Modes as listed below: $8000 $A000 $C000 $E000 +-------------------------------+ Uninitialized: | { 0 } | <-- use first 128k +-------------------------------+ O=0: | $A000 | <-- use first 128k +-------------------------------+ O=1, P=0: | <$E000> | <-- use second 128k +-------------------------------+ O=1, P=1, S=0: | { 0 } | $E000 | <-- use second 128k +---------------+---------------+ O=1, P=1, S=1: | $E000 | {$07} | <-- use second 128k +---------------+---------------+ IRQ Counter: --------------------------- The 'I' bit in $A000 controls the IRQ counter. When cleared, the IRQ counter counts up every cycle. When set, the IRQ counter is reset to 0 and stays there (does not count), and the pending IRQ is acknowledged. The cart has 4 dipswitches which control how high the counter must reach for an IRQ to be generated. The IRQ counter is 30 bits wide.. when it reaches the following value, an IRQ is fired: [1D CBAx xxxx xxxx xxxx xxxx xxxx xxxx] ^ ^^^ | ||| either 0 or 1, depending on the corresponding dipswitch. So if all dipswitches are open (use '0' above), the counter must reach $20000000. If all dipswitches are closed (use '1' above), the counter must reach $3E000000. etc In the official tournament, 'C' was closed, and the others were open, so the counter had to reach $2800000. ================================================ FILE: docs/mapper/107.txt ================================================ ======================== = Mapper 107 = ======================== Example Game: -------------------------- Magic Dragon Registers: --------------------------- I do not know whether or not this mapper suffers from bus conflicts. Use caution! $8000-FFFF: [PPPP PPP.] [CCCC CCCC] P = Selects 32k PRG @ $8000 C = Selects 8k CHR @ $0000 This is very strange. Bits 1-7 seem to be used by both CHR and PRG. ================================================ FILE: docs/mapper/112.txt ================================================ ======================== = Mapper 112 = ======================== Example Games: -------------------------- Huang Di San Guo Zhi - Qun Xiong Zheng Ba Registers: --------------------------- Range,Mask: $8000-FFFF, $E001 $8000: [.... .AAA] A = Address for use with $A000 $A000: [DDDD DDDD] Data port: R:0 -> PRG reg 0 R:1 -> PRG reg 1 R:2 -> CHR reg 0 R:3 -> CHR reg 1 R:4 -> CHR reg 2 R:5 -> CHR reg 3 R:6 -> CHR reg 4 R:7 -> CHR reg 5 $E000: [.... ...M] Mirroring: 0=Vert 1=Horz CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------+---------------+-------+-------+-------+-------+ | | | R:4 | R:5 | R:6 | R:7 | +---------------+---------------+-------+-------+-------+-------+ PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | R:0 | R:1 | { -2} | { -1} | +-------+-------+-------+-------+ ================================================ FILE: docs/mapper/113.txt ================================================ ======================== = Mapper 113 = ======================== Example Games: -------------------------- HES 6-in-1 Mind Blower Pak Total Funpak Registers: --------------------------- Range,Mask: $4100-5FFF, $4100 be sure to make note of the mask -- $4200 does not map to the register, but $4300 does. $4100: [MCPP PCCC] C = CHR Reg (8k @ $0000) P = PRG Reg (32k @ $8000) M = Mirroring: 0 = Horz 1 = Vert Note the high bit of the CHR Reg. ================================================ FILE: docs/mapper/115.txt ================================================ ======================== = Mapper 115 = ======================== Example Game: -------------------------- Yuu Yuu Hakusho Final - Makai Saikyou Retsuden Notes: --------------------------- MMC3 variant. For info on MMC3, see mapper 004. Regs at $6000-7FFF means no PRG-RAM Registers: --------------------------- Range,Mask: $6000-7FFF, $6001 $6000: [O... PPPP] O = PRG Mode P = 16k PRG Page $6001: [.... ...C] C = CHR Block select $8000-FFFF: Same as MMC3 CHR Setup: --------------------------- 'C' selects a 256k CHR block for all the CHR selected by the MMC3. You can think of this as a CHR-OR of $000 or $100 depending on 'C'. PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------------------------------+ O=0: | MMC3 | +-------------------------------+ O=1: | $6000 | MMC3 | +---------------+---------------+ Normal MMC3 PRG setup applies. If the O mode bit is set, then $8000-BFFF no longer reflects the typical MMC3 setup, and instead has a 16k page selected by $6000. ================================================ FILE: docs/mapper/118.txt ================================================ ======================== = Mapper 118 = ======================== aka -------------------------- MMC3 (modified) Example Games: -------------------------- Armadillo Pro Sport Hockey Notes: --------------------------- This mapper is a modified MMC3. It behaves exactly like your normal MMC3, only mirroring is handled differently. For details on MMC3, refer to mapper 004. Regs: --------------------------- $8000: [CP.. .AAA] C = CHR Mode P = PRG Mode A = Address for $8001 This register operates exactly like it does on your normal MMC3. It is mentioned here because the 'C' bit has another usage for mirroring. The normal mirroring reg ($A000) is totally ignored, and the CHR regs select nametables: When 'C' is set: [ R:2 ][ R:3 ] [ R:4 ][ R:5 ] When 'C' is clear: [ R:0 ][ R:0 ] [ R:1 ][ R:1 ] For mirroring, only bit 7 of the CHR regs is significant. Bit 7 of the appropriate reg selects either NTA or NTB. ================================================ FILE: docs/mapper/119.txt ================================================ ======================== = Mapper 119 = ======================== aka -------------------------- TQROM MMC3 (alternate) Example Games: -------------------------- High Speed Pinbot Notes: --------------------------- In addition to any CHR-ROM present, this mapper has 8k CHR-RAM. CHR-RAM is selectable just like ROM. Bit 6 of each CHR Reg (R:0 - R:5) indicates whether ROM or RAM is selected (1=use RAM). Other than that, this mapper is your plain, vanilla MMC3. See mapper 004 for details. ================================================ FILE: docs/mapper/140.txt ================================================ ======================== = Mapper 140 = ======================== Example Game: -------------------------- Bio Senshi Dan - Increaser Tono Tatakai Notes: --------------------------- Regs lie at $6000-7FFF, so there's no SRAM Registers: -------------------------- $6000-7FFF: [..PP CCCC] P = Selects 32k PRG @ $8000 C = Selects 8k CHR @ $0000 ================================================ FILE: docs/mapper/152.txt ================================================ ======================== = Mapper 152 = ======================== Example Games: -------------------------- Arkanoid 2 (J) Gegege no Kitarou 2 Registers: (**BUS CONFLICTS**) -------------------------- $8000-FFFF: [MPPP CCCC] M = Mirroring: 0 = 1ScA 1 = 1ScB P = PRG Reg (16k @ $8000) C = CHR Reg (8k @ $0000) PRG Setup: -------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | $8000 | { -1} | +---------------+---------------+ ================================================ FILE: docs/mapper/154.txt ================================================ ======================== = Mapper 154 = ======================== Example Game: -------------------------- Devil Man Registers: --------------------------- Range,Mask: $8000-FFFF, $8001 $8000: [.M.. .AAA] M = Mirroring 0 = 1ScA 1 = 1ScB A = Address for use with $8001 $8001: [DDDD DDDD] Data port: R:0 -> CHR reg 0 R:1 -> CHR reg 1 R:2 -> CHR reg 2 R:3 -> CHR reg 3 R:4 -> CHR reg 4 R:5 -> CHR reg 5 R:6 -> PRG reg 0 (8k @ $8000) R:7 -> PRG reg 1 (8k @ $A000) CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------+---------------+-------+-------+-------+-------+ | | | R:2 | R:3 | R:4 | R:5 | +---------------+---------------+-------+-------+-------+-------+ R:0,R:1 select CHR from the first 64k block. R:2-R:5 select CHR from the second 64k block. Therefore, you must effectively AND the written values to R:0,R:1 with $3F, and OR the written values to R:2-R:5 with $40. PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | R:6 | R:7 | { -2} | { -1} | +-------+-------+-------+-------+ ================================================ FILE: docs/mapper/159.txt ================================================ ======================== = Mapper 159 = ======================== This mapper is covered in full in mapper 016's doc. See that doc for details. ================================================ FILE: docs/mapper/164.txt ================================================ ======================== = Mapper 164 = ======================== Example Game: -------------------------- Final Fantasy V Registers: --------------------------- Range,Mask: $5000-FFFF, $F300 $5000, $D000: PRG reg (32k @ $8000) $6000-7FFF may have SRAM (not sure) On Reset --------------------------- Reg seems to contain $FF on powerup/reset Notes: --------------------------- Swapping is really simple -- the thing that is funky is the register range/mask. $5000 and $D000 will access the register, however $5100, $5200, etc will not. ================================================ FILE: docs/mapper/165.txt ================================================ ======================== = Mapper 165 = ======================== Example Games: -------------------------- Fire Emblem (Unl) (some weird ?Chinese? pirate version) Notes: --------------------------- This mapper is a strange MMC2+MMC3 hybrid. Register style, PRG, mirroring, ?and even IRQs? of MMC3, with the CHR swapping and CHR latch functionality of MMC2. There is 4k CHR-RAM in addition to any CHR-ROM present. For details on MMC3, see mapper 004. For details on MMC2, see mapper 009. Both will be referenced heavily in this doc. Operation: --------------------------- Register layout, PRG Setup, SRAM enabling, Mirroring, all function as they do on your vanilla MMC3. The CHR Regs (R:0 - R:5) are used in MMC2 style: CHR Setup: $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------------------------------+-------------------------------+ | <> or <> | <> or <> | +-------------------------------+-------------------------------+ The same latches that exist on MMC2 exist on this mapper as well, and determine the appropriate reg. CHR page 0 is CHR-RAM, other pages are CHR-ROM. Notes: --------------------------- This game specifically will read ppu$xFD0 or ppu$xFE0 via $2007 to manually toggle the latch (specifically, to swap in the CHR-RAM page). Failure to emulate this method of MMC2 latch toggling will result in garbled graphics. ================================================ FILE: docs/mapper/180.txt ================================================ ======================== = Mapper 180 = ======================== Example Game: -------------------------- Crazy Climber (J) Notes: --------------------------- This game uses a special input device (the crazy climber controller), so you'll need to emulate that in order to really test this mapper. Registers: (*** BUS CONFLICTS ***) -------------------------- $8000-FFFF: [.... .PPP] PRG Reg (16k @ $C000) PRG Setup: -------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | { 0 } | $8000 | +---------------+---------------+ Powerup: -------------------------- The register will probably contain 0 on cold powerup, but this is not guaranteed. The contents will be unchanged on reboot. ================================================ FILE: docs/mapper/182.txt ================================================ ======================== = Mapper 182 = ======================== Example Games: -------------------------- Pocahontas Super Donkey Kong Registers: --------------------------- This mapper is an MMC3 with its registers all scrambled to hell. Rather than a typical register outline, this section will "translate" mapper 182 registers to their coresponding "normal" MMC3 counterpart. For MMC3 details, see mapper 004. Range, Mask: $8000-FFFF, $E001 Mapper 182 MMC3 -------------------- $8000 - $8001 $A000 $A000 $8000 (addresses further scrambled, see below) $A001 - $C000 $8001 $C001 $C000+$C001 * $E000 $E000 $E001 $E001 A write to $C001 would be like a write to both $C000 and $C001 on a normal MMC3 (sets reload value, and clears the IRQ counter). The Address/Data port registers are further scrambled: Mapper 182 MMC3 -------------------- R:0 R:0 R:1 R:3 R:2 R:1 R:3 R:5 R:4 R:6 R:5 R:7 R:6 R:2 R:7 R:4 Other than this scrambling mess, the mapper operates exactly like a normal MMC3. ================================================ FILE: docs/mapper/184.txt ================================================ ======================== = Mapper 184 = ======================== Example Games: -------------------------- Atlantis no Nazo The Wing of Madoola Registers: -------------------------- $6000-7FFF: [.HHH .LLL] H = Selects 4k CHR @ $1000 L = Selects 4k CHR @ $0000 Regs at $6000-7FFF means no SRAM The most significant bit of H is always set in hardware. (i.e. its range is 4 to 7) ================================================ FILE: docs/mapper/185.txt ================================================ ======================== = Mapper 185 = ======================== Example Games: -------------------------- Spy Vs. Spy (J) Mighty Bomb Jack (J) Registers: (**BUS CONFLICTS**) --------------------------- $8000-FFFF: [..CC ..CC] CHR Reg Notes: --------------------------- This mapper is retarded. These games only have 8k of CHR, and they attempt to disable CHR by writing a specific value to the CHR Reg, then VERIFY that garbage is read back, then they swap back to the actual CHR. If they don't get the expected garbage, they lock up. Perhaps this was some sort of copy protection? Each game has their own value that enables/disables CHR. Rather than failing to attempt to list all the exact values used here (I don't know what all of them are), I can provide some logic: if C AND $0F is nonzero, and if C does not equal $13: CHR is enabled otherwise CHR is disabled When CHR is disabled, the pattern tables are open bus. Theoretically, this should return the LSB of the address read, but real-world behavior may vary. ================================================ FILE: docs/mapper/189.txt ================================================ ======================== = Mapper 189 = ======================== Example Game: -------------------------- Thunder Warrior Notes: --------------------------- This mapper is a modified MMC3. Everything operates just as it does on the MMC3, only the normal PRG regs (R:6,R:7) are ignored, and a new PRG Reg is used instead. For details on MMC3, see mapper 004 Registers: --------------------------- Regs at $6000-7FFF means no SRAM $4120-7FFF: [AAAA BBBB] A,B: PRG Reg $8000-FFFF: Same as on MMC3 PRG Setup: -------------------------- 'A' and 'B' bits of the $4120 reg seem to be effectively OR'd. That is... $30, $03, and $21 will all select page 3 $8000 $A000 $C000 $E000 +-------------------------------+ | $4120 | +-------------------------------+ ================================================ FILE: docs/mapper/191.txt ================================================ ======================== = Mapper 191 = ======================== aka: -------------------------- Pirate MMC3 variant Example Game: -------------------------- Sugoro Quest - Dice no Senshitachi (As) Notes: -------------------------- This mapper is a modified MMC3 (or is based on MMC3?). In addition to any CHR-ROM present, there is also an additional 2k of CHR-RAM which is selectable. Bit 7 of each CHR reg selects RAM or ROM (1=RAM, 0=ROM) Apart from that, this mapper behaves exactly like your typical MMC3. See mapper 004 for details. ================================================ FILE: docs/mapper/192.txt ================================================ ======================== = Mapper 192 = ======================== aka: -------------------------- Pirate MMC3 variant Example Game: -------------------------- Ying Lie Qun Xia Zhuan Notes: -------------------------- This mapper is a modified MMC3 (or is based on MMC3?). In addition to any CHR-ROM present, there is also an additional 4k of CHR-RAM which is selectable. CHR Pages $08-$0B are CHR-RAM, other pages are CHR-ROM. Apart from that, this mapper behaves exactly like your typical MMC3. See mapper 004 for details. ================================================ FILE: docs/mapper/193.txt ================================================ ======================== = Mapper 193 = ======================== Example Game: -------------------------- Fighting Hero (Unl) Registers: --------------------------- Regs at $6000-7FFF = no SRAM Range,Mask: $6000-7FFF, $6003 $6000: CHR Reg 0 $6001: CHR Reg 1 $6002: CHR Reg 2 $6003: PRG Reg CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------------------------------+---------------+---------------+ | <<$6000>> | <$6001> | <$6002> | +-------------------------------+---------------+---------------+ PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | $6003 | { -3} | { -2} | { -1} | +-------+-------+-------+-------+ ================================================ FILE: docs/mapper/194.txt ================================================ ======================== = Mapper 194 = ======================== aka: -------------------------- Pirate MMC3 variant Example Game: -------------------------- Dai-2-Ji - Super Robot Taisen (As) Notes: -------------------------- This mapper is a modified MMC3 (or is based on MMC3?). In addition to any CHR-ROM present, there is also an additional 2k of CHR-RAM which is selectable. CHR Pages $00-$01 are CHR-RAM, other pages are CHR-ROM. Apart from that, this mapper behaves exactly like your typical MMC3. See mapper 004 for details. ================================================ FILE: docs/mapper/200.txt ================================================ ======================== = Mapper 200 = ======================== Example Games: -------------------------- 1200-in-1 36-in-1 Registers: --------------------------- $8000-FFFF: A~[.... .... .... MRRR] M = Mirroring (0=Vert, 1=Horz) R = PRG/CHR Reg CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------------------------------------------------------+ | $8000 | +---------------------------------------------------------------+ PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | $8000 | $8000 | +---------------+---------------+ ================================================ FILE: docs/mapper/201.txt ================================================ ======================== = Mapper 201 = ======================== Example Games: -------------------------- 8-in-1 21-in-1 (2006-CA) (Unl) Registers: --------------------------- $8000-FFFF: A~[.... .... RRRR RRRR] R = PRG/CHR Reg CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------------------------------------------------------+ | $8000 | +---------------------------------------------------------------+ PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------------------------------+ | $8000 | +-------------------------------+ ================================================ FILE: docs/mapper/203.txt ================================================ ======================== = Mapper 203 = ======================== Example Games: -------------------------- 35-in-1 Registers: --------------------------- $8000-FFFF: [PPPP PPCC] P = PRG Reg C = CHR Reg CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------------------------------------------------------+ | $8000 | +---------------------------------------------------------------+ PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ | $8000 | $8000 | +---------------+---------------+ ================================================ FILE: docs/mapper/205.txt ================================================ ======================== = Mapper 205 = ======================== Example Games: -------------------------- 15-in-1 3-in-1 Registers: --------------------------- Regs at $6000-7FFF means no SRAM $6000-7FFF: [.... ..MM] Game Mode / Block $8000-FFFF: MMC3 Notes: --------------------------- These multicarts select the game mode by writing to $6000-7FFF, the individual games then use traditional MMC3 style regs at $8000-FFFF. The MMC3 regs only swap to pages *within* the block specifed by the game mode. This can be easily emulated by ANDing the page numbers written to MMC3 with certain values, and then ORing them with other values, based on the selected block. Chart below to illustrate: Block PRG-AND PRG-OR CHR-AND CHR-OR ------------------------------------------------- 0 $1F $00 $FF $000 1 $1F $10 $FF $080 2 $0F $20 $7F $100 3 $0F $30 $7F $180 For details on MMC3, see mapper 004 ================================================ FILE: docs/mapper/207.txt ================================================ ======================== = Mapper 207 = ======================== Example Game: -------------------------- Fudou Myouou Den Notes: --------------------------- Regs appear at $7EFx, I'm unsure whether or not PRG-RAM can exist at $6000-7FFF This mapper is just like mapper 080, with mirroring handled differently. For details, refer to mapper 080. Registers: --------------------------- $7EF0: [MCCC CCCC] M = Mirroring 0 C = CHR Reg 0 $7EF1: [MCCC CCCC] M = Mirroring 1 C = CHR Reg 1 $7EF2-7EF5: CHR Regs 2-5 $7EFA,7EFB: PRG Reg 0 (8k @ $8000) $7EFC,7EFD: PRG Reg 1 (8k @ $A000) $7EFE,7EFF: PRG Reg 2 (8k @ $C000) CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------+---------------+-------+-------+-------+-------+ | <$7EF0> | <$7EF1> | $7EF2 | $7EF3 | $7EF4 | $7EF5 | +---------------+---------------+-------+-------+-------+-------+ PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | $7EFA | $7EFC | $7EFE | { -1} | +-------+-------+-------+-------+ Mirroring: --------------------------- [ $7EF0 ][ $7EF0 ] [ $7EF1 ][ $7EF1 ] Mirroring bit of appropriate reg selects NTA or NTB ================================================ FILE: docs/mapper/209.txt ================================================ ======================== = Mapper 209 = ======================== This mapper is covered in full in mapper 090's doc. See that doc for details. ================================================ FILE: docs/mapper/210.txt ================================================ ======================== = Mapper 210 = ======================== This mapper is covered in full in mapper 019's doc. See that doc for details. ================================================ FILE: docs/mapper/225.txt ================================================ ======================== = Mapper 225 = ======================== Example Games: -------------------------- 52 Games 58-in-1 64-in-1 Registers: --------------------------- $5800-5803: [.... RRRR] RAM (readable/writable) (16 bits of RAM -- 4 bits in each of the 4 regs) $5804-5FFF: mirrors $5800-5803 $8000-FFFF: A~[.HMO PPPP PPCC CCCC] H = High bit (acts as bit 7 for PRG and CHR regs) M = Mirroring (0=Vert, 1=Horz) O = PRG Mode P = PRG Reg C = CHR Reg CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------------------------------------------------------+ CHR Mode 0: | $8000 | +---------------------------------------------------------------+ PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------------------------------+ PRG Mode 0: | <$8000> | +-------------------------------+ PRG Mode 1: | $8000 | $8000 | +---------------+---------------+ ================================================ FILE: docs/mapper/226.txt ================================================ ======================== = Mapper 226 = ======================== Example Games: -------------------------- 76-in-1 Super 42-in-1 Registers: --------------------------- Range, Mask: $8000-FFFF, $8001 $8000: [PMOP PPPP] P = Low 6 bits of PRG Reg M = Mirroring (0=Horz, 1=Vert) O = PRG Mode $8001: [.... ...H] H = high bit of PRG PRG Setup: --------------------------- Low 6 bits of the PRG Reg come from $8000, high bit comes from $8001 $8000 $A000 $C000 $E000 +-------------------------------+ PRG Mode 0: | | +-------------------------------+ PRG Mode 1: | Reg | Reg | +---------------+---------------+ ================================================ FILE: docs/mapper/227.txt ================================================ ======================== = Mapper 227 = ======================== Example Game: -------------------------- 1200-in-1 Notes: --------------------------- This mapper has 8k CHR-RAM, and also has the ability to write protect it's CHR-RAM! Registers: --------------------------- $8000-FFFF: A~[.... ..LP OPPP PPMS] L = Last PRG Page Mode P = PRG Reg O = Mode M = Mirroring (0=Vert, 1=Horz) S = PRG Size Setup: --------------------------- When 'O' is set, CHR-RAM is write protected (writes have no effect). 'O' also changes the PRG mode. Note there is funky ANDs and ORs going on below depending on the modes: $8000 $A000 $C000 $E000 +---------------+---------------+ O=1, S=0: | P | P | +-------------------------------+ O=1, S=1: | < P > | +-------------------------------+ O=0, S=0, L=0: | P | P AND $38 | +---------------+---------------+ O=0, S=1, L=0: | P AND $3E | P AND $38 | +---------------+---------------+ O=0, S=0, L=1: | P | P OR $07 | +---------------+---------------+ O=0, S=1, L=1: | P AND $3E | P OR $07 | +---------------+---------------+ ================================================ FILE: docs/mapper/228.txt ================================================ ======================== = Mapper 228 = ======================== Example Games: -------------------------- Action 52 Cheetah Men II Notes: --------------------------- Cheetah Men II is infamous for how freaking terrible it is. Action 52 is none better. These games are SO bad, it's hilarious. Action 52's PRG size is weird (not a power of 2 value). This is because there are 3 seperate 512k PRG chips. PRG Setup section will cover details. Powerup and Reset: --------------------------- Apparently the games expect $00 to be written to $8000 on powerup/reset. Registers: --------------------------- $4020-4023: [.... RRRR] RAM (readable/writable) (16 bits of RAM -- 4 bits in each of the 4 regs) $4024-5FFF: mirrors $4020-4023 $8000-FFFF: [.... ..CC] Low 2 bits of CHR A~[..MH HPPP PPO. CCCC] M = Mirroring (0=Vert, 1=Horz) H = PRG Chip Select P = PRG Page Select O = PRG Mode C = High 4 bits of CHR CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------------------------------------------------------+ | $8000 | +---------------------------------------------------------------+ PRG Setup: --------------------------- 'H' bits select the PRG chip. Each chip is 512k in size. Chip 2 does not exist, and when selected, will result in open bus. The Action 52 .nes ROM file contains chips 0, 1, and 3: chip 0: offset 0x000010 chip 1: offset 0x080010 chip 2: -- non existant -- chip 3: offset 0x100010 'P' selects the PRG page on the currently selected chip. $8000 $A000 $C000 $E000 +-------------------------------+ PRG Mode 0: | <$8000> | +-------------------------------+ PRG Mode 1: | $8000 | $8000 | +---------------+---------------+ ================================================ FILE: docs/mapper/230.txt ================================================ ======================== = Mapper 230 = ======================== Example Game: -------------------------- 22-in-1 Reset Driven: --------------------------- The mapper has 2 main modes: Contra mode, and multicart mode. Performing a Soft Reset switches between them. Notes: --------------------------- This multicart has an odd PRG size (not power of 2). This is because there are 2 PRG chips. The first is 128k and contains Contra, the other is 512k and contains the multicart. A soft reset changes which chip is used as well as other stuff relating to the mode Registers: --------------------------- Contra Mode $8000-FFFF: [.... .PPP] Multicart Mode $8000-FFFF: [.MOP PPPP] M = Mirroring (0=Horz, 1=Vert) O = PRG Mode P = PRG Page Note: Mirroring is always Vert in Contra Mode. PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +---------------+---------------+ Contra Mode: | $8000 | { 7 } | <--- use chip 0 +-------------------------------+ Multi Mode 0: | | <--- use chip 1 +-------------------------------+ Multi Mode 1: | Reg | Reg | <--- use chip 1 +---------------+---------------+ chip 0 = 128k PRG (offset 0x00010-0x2000F) chip 1 = 512k PRG (offset 0x20010-0xA000F) ================================================ FILE: docs/mapper/231.txt ================================================ ======================== = Mapper 231 = ======================== Example Game: -------------------------- 20-in-1 Registers: --------------------------- $8000-FFFF: A~[.... .... M.LP PPP.] M = Mirroring (0=Vert, 1=Horz) L = Low bit of PRG P = High bits of PRG PRG Setup: --------------------------- Note that 'L' and 'P' bits make up the PRG reg, and the 'L' is the low bit. $8000 $A000 $C000 $E000 +---------------+---------------+ | $8000 AND $1E | $8000 | +---------------+---------------+ ================================================ FILE: docs/mapper/232.txt ================================================ ======================== = Mapper 232 = ======================== Example Games: -------------------------- Quattro Adventure Quattro Sports Quattro Arcade Notes: -------------------------- This is another Camerica/Codemasters mapper like 071. Like 071, this mapper also involves a custom lockout defeat circuit which is mostly unimportant for emulation purposes. Details will not be mentioned here, but are outlined in Kevtris' Camerica Mappers documentation. Registers: --------------------------- $8000-BFFF: [...B B...] PRG Block Select $C000-FFFF: [.... ..PP] PRG Page Select PRG Setup: --------------------------- Pages are taken from the 64k block currently selected by $8000. $8000 $A000 $C000 $E000 +---------------+---------------+ | $C000 | { 3 } | +---------------+---------------+ ================================================ FILE: docs/mapper/233.txt ================================================ ======================== = Mapper 233 = ======================== Example Game: -------------------------- ???? "42-in-1" ???? Notes: --------------------------- Sources report this mapper as "42-in-1" with description layed out below. I did not test this, since I could not find a copy of the ROM in question. The only ROM I have that's marked as 233 is "Unknown Multicart 1", and it does *not* follow the description in this doc at all. There is a "Super 42-in-1"... but that is mapper 226. 226, by the way, is strikingly similar to the below description. I wonder if below description really applies to 233? Registers: --------------------------- $8000-FFFF: [MMOP PPPP] M = Mirroring O = PRG Mode P = PRG Page PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------------------------------+ PRG Mode 0: | <$8000> | +-------------------------------+ PRG Mode 1: | $8000 | $8000 | +---------------+---------------+ Mirroring: --------------------------- 'M' mirroring bits: %00 = See below %01 = Vert %10 = Horz %11 = 1ScB Mode %00 (almost, but not quite 1ScA): [ NTA ][ NTA ] [ NTA ][ NTB ] ----------------------------------------------- -------------------------- "Unknown Multi Cart 1" -------------------------- ----------------------------------------------- Notes: -------------------------- This ROM is a mystery. It consists of 32 individual mapper 0 games, each 16k PRG + 8k CHR. Games do not use compatible mirroring modes -- the ROM includes Ice Climber (which uses H mirroring) and Excitebike (V mirroring). No games seem to be modified to make any kind of writes to $8000-FFFF, so I didn't find any clue as to what mapper registers are being written to (if any). There does not appear to be *ANY* game selection screen for the multicart. I have paged through every 16k of PRG and they all run the individual games straight up. So unless the mapper aligns to 8k (or smaller) pages on startup and there's some crap hidden in there, I don't see how these games are held together. Could it be that this ROM dump is bad or incomplete? The *only* other thing I can think of is that this cart might be reset based (a la mapper 060) -- but that is complicated by the fact that games use conflicting mirroring modes. So perhaps the mapper sets the mirroring mode based on what game is selected? More weirdness: games seem to have mismatching PRG/CHR pages. Ice Climber's PRG is on page $1F, but its CHR is on page $17. It seems that you need to subtract 8 from the PRG page to get the matching CHR page (and wrap where appropriate) Even more weirdness: despite each game only having 16k PRG, some of the games in the package normally have 32k! I don't know if the games were just hacked and had a bunch of stuff removed for what. It's possible this ROM dump is bogus and/or bad. It might even be assigned the wrong mapper number. At any rate... here are the games. Game name PRG page CHR Page Mirroring ---------------------------------------------------------- Galaxian $00 $18 Horz 10 Yard Fight $01 $19 Horz DKJ Math $02 $1A Vert Antarctic Adventure $03 $1B Horz Balloon Fight $04 $1C Horz Baseball $05 $1D Horz Battle City $06 $1E Horz Binary Land $07 $1F Horz Burger Time $08 $00 Horz Chack 'n Pop $09 $01 Horz Clu Clu Land $0A $02 Horz Lode Runner $0B $03 Vert ?? Go / Othello ?? $0C $04 ???? Field Combat $0D $05 Horz ** really 32k PRG? ** Devil World $0E $06 Horz Dig Dug $0F $07 Horz Donkey Kong $10 $08 Horz Donkey Kong Jr. $11 $09 Horz Donkey Kong 3 $12 $0A Vert Door Door $13 $0B Vert Duck Hunt $14 $0C Vert Excitebike $15 $0D Vert Exerion $16 $0E Horz F1-Race $17 $0F Vert Formation Z $18 $10 Horz Front Line $19 $11 Horz Galaga $1A $12 Horz ** really 32k PRG? ** Golf $1B $13 Vert Hogan's Alley $1C $14 Vert Hyper Olympic $1D $15 Vert ?? some gun game ?? $1E $16 ???? Ice Climber $1F $17 Horz ================================================ FILE: docs/mapper/234.txt ================================================ ======================== = Mapper 234 = ======================== Example Game: -------------------------- Maxi 15 Notes: -------------------------- Typical for the Atari 2600, but strange for the NES: Registers lie at $FF80-$FFFF but bankswitching happens on reads, as well as writes. Bus conflicts are thus avoided by storing the library of desired bankswitch values in ROM. Example: LDA $FF80 ; where $FF80 contains $62 would (ignoring bus conflicts) have the same effect on the mapper as: LDA #$62 STA $FF80 Registers: --------------------------- Range,Mask: $FF80-FFFF, $FFF8 $FF80, $FF88, $FF90, $FF98: [MOQq BBBb] Reg 0 M = Mirroring (0=Vert, 1=Horz) O = Mode (0=CNROM, 1=NINA-03) B,b = Block selection q = ROMs 3+4 /Enable (0=normal, 1=disable ROM further from cartridge edge) Since the cartridge was distributed with only ROMs 1+2 populated, this is irrelevant. This bit seems to have been intended to have been an extra address line for ROMs 3+4, enabling a total of 1.5M/1.5M in the cartridge, but a mistake prevents it from working. Q = ROM switch (0=enable ROMs 1+2, 1=enable ROMs 3+4) i.e. 0 for normal operation $FFC0, $FFC8, $FFD0, $FFD8: [.... ..LL] Reg 1 L = Lockout defeat (charge pump drive) $FFE8, $FFF0: [.cCC ...P] Reg 2 C,c = CHR page P = PRG page Once the bottom 6 bits of Reg 0 contain a non-zero value, Reg 0 and Reg 1 are locked and cannot be changed until the system is reset. Reg 2 is never locked. CHR Setup: --------------------------- 8k page @ $0000 selected by the following: 'O' CHR page --------------------- 0 %BB BbCC 1 %BB BcCC PRG Setup: --------------------------- 32k page @ $8000 selected by the following: 'O' PRG page --------------------- 0 %BBBb 1 %BBBP On Powerup/Reset: --------------------------- Regs all filled with 0 and unlocked. ================================================ FILE: docs/mapper/240.txt ================================================ ======================== = Mapper 240 = ======================== Example Games: -------------------------- Jing Ke Xin Zhuan Sheng Huo Lie Zhuan Registers: -------------------------- $4020-5FFF: [PPPP CCCC] P = Selects 32k PRG @ $8000 C = Selects 8k CHR @ $0000 ================================================ FILE: docs/mapper/242.txt ================================================ ======================== = Mapper 242 = ======================== Example Game: -------------------------- Wai Xing Zhan Shi Registers: --------------------------- $8000-FFFF: A~[.... .... .PPP P.M.] P = PRG Reg (32k @ $8000) M = Mirroring (0=Vert, 1=Horz) Powerup/Reset: --------------------------- Register set to 0 on powerup/reset. ================================================ FILE: docs/mapper/243.txt ================================================ ======================== = Mapper 243 = ======================== Example Games: -------------------------- Honey Poker III 5-in-1 Registers: --------------------------- Range,Mask: $4020-4FFF, $4101 $4100: [.... .AAA] Address for use with $4101 $4101: Data port R:2 -> [.... ...H] High bit of CHR reg R:4 -> [.... ...L] Low bit of CHR reg R:5 -> [.... .PPP] PRG reg (32k @ $8000) R:6 -> [.... ..DD] Middle bits of CHR reg R:7 -> [.... .MM.] Mirroring %00 = Horz %01 = Vert %10 = See below %11 = 1ScB Mirroring: --------------------------- Mirroing mode %10 is not quite 1ScB: [ NTA ][ NTB ] [ NTB ][ NTB ] CHR Setup: --------------------------- 8k CHR page @ $0000 is selected by the given 4 bit CHR page number ('HDDL') ================================================ FILE: docs/mapper/245.txt ================================================ ======================== = Mapper 245 = ======================== Example Games: -------------------------- Chu Han Zheng Ba - The War Between Chu & Han Xing Ji Wu Shi - Super Fighter Yin He Shi Dai Yong Zhe Dou e Long - Dragon Quest VII (As) Dong Fang de Chuan Shuo - The Hyrule Fantasy Notes: --------------------------- Another ?Chinese? MMC3 clone. Very similar to your typical MMC3. For MMC3 info, see mapper 004. Register layout is identical to a typical MMC3. CHR Setup: --------------------------- CHR-RAM is not swappable. When there is no CHR-ROM present, 8k CHR-RAM is fixed. However the CHR Mode bit ($8000.7) can still "flip" the left/right pattern tables. Example: $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +-------------------------------+-------------------------------+ CHR-RAM, Mode 0: | { 0 } | { 1 } | +-------------------------------+-------------------------------+ CHR-RAM, Mode 1: | { 1 } | { 0 } | +---------------------------------------------------------------+ CHR-ROM: | Typical MMC3 | +---------------------------------------------------------------+ PRG Setup: --------------------------- PRG Setup is the same as a normal MMC3, although there's a PRG-AND of $3F, and games select a 512k Block with bit 1 of R:0. Pretty simple really: R:0: [.... ..P.] 'P' PRG-AND PRG-OR -------------------------- 0 $3F $00 1 $3F $40 R:0 remains the normal MMC3 CHR reg, as well. Although the game that uses it as a PRG block selector ("DQ7") uses CHR-RAM, so it is normally ignored. ================================================ FILE: docs/mapper/246.txt ================================================ ======================== = Mapper 246 = ======================== Example Game: -------------------------- Fong Shen Bang - Zhu Lu Zhi Zhan Notes: -------------------------- Regs lie at $6000-67FF, but SRAM exists at $6800-7FFF. Don't know if there's only 6k of SRAM, or if there's 8k, but the first 2k is inaccessable. I find the latter more likely. Registers: --------------------------- Range,Mask: $6000-67FF, $6007 $6000-6003: PRG Regs $6004-6007: CHR Regs CHR Setup: --------------------------- $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------+---------------+---------------+---------------+ | $6004 | $6005 | $6006 | $6007 | +---------------+---------------+---------------+---------------+ PRG Setup: --------------------------- $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | $6000 | $6001 | $6002 | $6003 | +-------+-------+-------+-------+ Powerup/Reset: --------------------------- $6003 set to $FF on powerup (and probably reset, but not sure). ================================================ FILE: docs/mapper/__ READ THIS FIRST __.txt ================================================ ****************************************** * iNES Mappers by Mapper Number * * v0.6.2 * * by Disch * * * * modifications made by NESDEV wiki * * members, commited by Bregalad * ****************************************** Read this doc -------------------------- The mapper pages use charts and symbols and abbreviations and stuff which aren't clarified in each individual doc, but are covered here. You could probably get away with just skimming this doc and/or only coming back to it if something in a specific mapper doc seems unclear. RAM Names -------------------------- "WRAM", "SRAM", and "PRG-RAM" are used synonymously and inconsistently in these docs. Kind of sloppy of me, I know. All three terms refer to on-cartridge RAM. Mirroring -------------------------- The NES only has two physical nametables. These nametables are referred to as "NTA" and "NTB". There are 4 "slots" for nametables to be accessed: $2000 (upper-left), $2400 (upper-right), $2800 (lower-left), and $2C00 (lower-right) Mappers which can customize the nametable layout may have a chart like the below to illustrate which nametable goes to which slot: [ $2000 ][ $2400 ] [ $2800 ][ $2C00 ] Most mappers which control mirroring usually pick from 2 to 4 standard mirroring configurations: Horizontal ("Horz"), Vertical ("Vert"), 1-Screen A ("1ScA"), and 1-Screen B ("1ScB"). These arrange the nametables like so: Vert Horz -------------- -------------- [ NTA ][ NTB ] [ NTA ][ NTA ] [ NTA ][ NTB ] [ NTB ][ NTB ] 1ScA 1ScB -------------- -------------- [ NTA ][ NTA ] [ NTB ][ NTB ] [ NTA ][ NTA ] [ NTB ][ NTB ] A few mappers also support 4-screen mirroring, which uses 4 full nametables so that each slot has its own unique nametable. Since the NES only has 2k for nametables, for a game to have 4-screen mirroring, additional VRAM must be present on the cartridge. I only know of a grand total of three games which use 4-screen mirroring, and they will be mentioned in their respective docs. Swap Charts ---------------------------- PRG/CHR swapping schemes are generally outlined in a chart. A PRG chart might look like so: $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | $7EFA | $7EFC | $7EFE | { -1} | +-------+-------+-------+-------+ This indicates which register is used to select a PRG page for which region. In this example, the register at $7EFA selects an 8k page for $8000-9FFF. Numbers surrounded by {curly braces} mean the page is fixed. Here, $E000-FFFF is fixed to page -1. Negative pages indicate the last pages are used. IE: "-1" means to use the last page of PRG, "-2" would be the second last, etc. CHR charts work similarly: $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------+---------------+-------+-------+-------+-------+ | <$7EF0> | <$7EF1> | $7EF2 | $7EF3 | $7EF4 | $7EF5 | +---------------+---------------+-------+-------+-------+-------+ Here, the register at $7EF3 selects a 1k CHR page for $1400-17FF, while $7EF0 selects a 2k CHR page for $0000-07FF. Numbers surrounded by <> symbols indicate the low bits of the given page number are ignored. This is typical where a mapper deals with several different page sizes. For example, $7EF0 selects a 2k page, but its low bit is ignored (effectively, you must right-shift its value by 1 for the actual page number). Example: if $7EF0=$05, 2k page $02 would be selected ($05 right shift 1 = $02) Double <>'s (example: "<<$7EF0>>") would mean the low 2 bits are ignored (right shift the value by 2). Numbers without <> symbols are referred to as "actual" page numbers. Charts may have multiple rows if there are multiple swapping modes. Erroneous noob swapping ---------------------------- Some newbies tend to make an understandable, but incorrect assumption about how swapping works. Given the following CHR chart: $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 +---------------+---------------+-------+-------+-------+-------+ CHR Mode 0: | | | R:2 | R:3 | R:4 | R:5 | +---------------+---------------+---------------+---------------+ CHR Mode 1: | R:2 | R:3 | R:4 | R:5 | | | +-------+-------+-------+-------+---------------+---------------+ A newbie might think that they can cleverly manipulate modes to select 1k pages across the board, rather than having those two 2k chunks in there. IE: They think that they can set R:2-R:5 in mode 0, then switch to mode 1, set R:2-R:5 again... and that would select each 1k page individually. This, of course, is not now it works. "Swapping" isn't actually swapping. What's actually happening is when the NES reads from a certain address the high bits of the address are being replaced by the contents of a mapper register. Because of this, mapper registers (and swapping modes) are accessed constantly at runtime... not just when the value is written. For example... setting R:2 in mode 0, then switching to mode 1 will have the exact same effect as switching to mode 1 first, then setting R:2. Both methods end up in mode 1, and both set only R:2... meaning the end result is selecting a 1k page at $0000. You might say a second "swap" occurs when the mode is changed. That is... if a game were to change modes, they would see the pattern tables "flip", even though they didn't swap anything. Register bit layouts ---------------------------- Registers often have different bits of the written value do different things. Or sometimes only some bits are significant and others are ignored. In these situations, bitfields are indicated by a pair of brackets. Example: $8000: [CP.. .AAA] The above shows 3 seperate things ('A', 'P', and 'C') that the register controls, and which bits are assigned to those things. Bits marked as '.' are irrelevent and unused. These bits are listed high bit first (here, 'C' would be bit 7) Some mappers (usually multicarts) also take bits from the address written to -- not just the value written. These instances will be marked with brackets with "A~" before them. A good example of this: $8000-FFFF: [.... ..CC] A~[..MH HPPP PPO. CCCC] The first bracket represents the value written, and the second bracket (with the A~) represents the address written to. Address/Data ports ---------------------------- Many mappers have several registers which are accessed by writing an address to one area, then writing the data you want to write to the reg to another area. The most common example of this is MMC3 (mapper 004). $8000 is the address port, and $8001 is the data port. Since $8001 actually accesses 8 different registers, $8001 can't appear in charts and descriptions and stuff. So for address/data ports like this, the accessed registers are referred to as "R:#" (where # is the hex address by which they're accessed). For example MMC3's 8 regs would be "R:0" through "R:7". For example, if a game wanted to change R:4, it would do the following: LDA #$04 STA $8000 ; set address to $04 LDA whatever STA $8001 ; since address is $04, this sets R:4 Timing / Dots ---------------------------- When discussing the timing of PPU triggered IRQs, I refer to 'dots'. IE: "The IRQ will fire on dot 260 of the scanline". 'Dots' are otherwise known as PPU Cycles. Each scanline consists of 341 dots -- and on NTSC, there are 3 dots to every 1 CPU cycle. Bus Conflicts ----------------------------- Some simple mappers suffer from bus conflicts. This means that when registers share CPU space with PRG, the value you write to the address must match what is read from that address or bad things will happen! Many games do this by having a LUT of common values somewhere and indexing it: Swap_LUT: .db $00, $01, $02, $03, $04, $05, $06, $07 PRG_Swap: ; assume A is the desired page to swap to (00-07) TAX STA Swap_LUT,X RTS This ABSOLUTELY NEEDS TO BE DONE for these mappers! Do not try to shortcut this! You will break your ROM! I'm sure I missed some mappers that have bus conflicts -- but I tried to mark all the ones I know do, and suspect might. When a bus conflicting write occurs, the result is usually an AND of the two potential values -- but such behavior should not be relied on. Register Masking / Ranges ------------------------------ Many times, a single register can be accessed by several addresses. For example when you see something like: $8000-FFFF: PRG Reg That means a write to anywhere between $8000-FFFF will access the PRG Reg. In that same vein, sometimes not all address lines are used when decoding which register is to be accessed. That is, some bits of the address don't matter. This creates a masking effect where registers are mirrored in a semi-weird fashion accross an address range. This would be marked in docs with something like: Range,Mask: $8000-FFFF, $E001 This would mean that within the range $8000-FFFF, you'd use $E001 as a mask for determining which register to use. IE: $D3F7 would mirror $C001, because $D3F7 AND $E001 = $C001. PRG/CHR Masking ------------------------------ When a game selects a page higher than there is ROM for, the page number would be masked to select an appropriate page. For example... if a game only has $08 pages of CHR, and it selects page $0A, then it would actually select page $02 (because $0A AND ($08-1) = $02). In that same vein... fixed "last pages" {-1}, {-2}, etc are really pages $FFFF, $FFFE, etc -- and the mask happens to make that select the last or second last page. This is why PRG/CHR sizes must always be a power of 2, except in extremely rare cases where there's an odd number of chips (and those cases are handled specially by the mapper). Powerup/Reset ----------------------------- Do not assume the state of anything at startup. Mapper registers, like RAM, contain pseudo-random garbage on system powerup, except in special cases, which will be noted in the appropriate docs. If no such note is made, you cannot assume anything. PRG-AND, PRG-OR, Blocks, etc ----------------------------- Multicarts (and even some single game carts) employ a type of block system which lets the game choose a block, and then will only swap to pages within that block. In these docs I often illustrate this with PRG-OR and PRG-AND values. For an example, let's say you have a game with the following PRG pages selected: $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | $02 | $16 | { -2} | { -1} | +-------+-------+-------+-------+ And let's say the PRG-AND is $0F, and the PRG-OR is $20. This would result in the following pages being selected: $8000 $A000 $C000 $E000 +-------+-------+-------+-------+ | $22 | $26 | $2E | $2F | +-------+-------+-------+-------+ simply, it's "(desiredpage AND PRGAND) OR PRGOR". Note that even the fixed pages are affected by this. CHR-AND and CHR-OR operate the same way, but with CHR pages. Always apply these values *before* any downshifting caused by <> symbols. Bregalad's notice ----------------- I imported changes that were made from Disch's original documents and compiled them into a new version. Disch himself clarified that changes to his own work were welcome in a NESDEV forums post : http://forums.nesdev.com/viewtopic.php?f=3&t=10275 He also said he keeps his distance to emulation scene and is probably not going to ever update this himself, so that is the reason for me to update the doccuments instead. I did not do any of these changes, rather, they were made by NESDEV wiki members such as lidnariq, Rainwarrior, and tepples (among possibly others). The details are in the changelog. I just reviewed them and integrated them here in order to keep the documents up to date. Finally I'd want to say a big thank to disch and congratulations for doing such a huge work of documenting so many mappers at the same time, while keeping it coherent and very comprehensive. ================================================ FILE: docs/mapper/changes.txt ================================================ v0.7 (Bregalad) ================================ - Imported changes from NESDEV wiki: - mapper 005 : - Correct info for mode B CHR switching in 8k mode - Add info about Bandits king of Ancient China's emulation - Add info for sound - Fixes misspelling - mapper 021 : A fixes about IRQ operation - mapper 022 : Combined with mapper 025 - mapper 032 : A game requires an NES2.0 sumbapper, not all registers uses all bits - mapper 072 : Major changes in explanation how the command/data works - mapper 076 : Major changes in PRG banking explanation - mapper 077 : Major changes in how CHR-RAM and NT-RAM works - mapper 080 : Added info about 128 bytes internal RAM - mapper 085 : Added extended information about sound (also by Disch). - mapper 091 : One more example game - mapper 113 : Changed example games - mapper 184 : High bit of 4 is always set in hardware - mapper 185 : Add note about open bus pattern table - mapper 207 : Is mapper 080 with mirroring handled differently - mapper 234 : Better wording of explanations of the strange way the regs are accessed - Mappers 072, 077, 078, 089, 093, 094, 152, 180 and 185 are confirmed to have bus conflicts - Did some spelling and corrects two errors in mapper 001 v0.6.1 ================================== - Fixed minor goof in 189 - made 4x4 bit RAM more clear in 225,228 - fixed various spelling goofs - added info about vertical scrolling in 005's split screen mode - added some info about a common noob error in the main readme - added technical correction in 005 about PRG-RAM chip select - fixed some mapper 034 stuff (BxROM and compatible) - fixed mapper 067 IRQ info. Had first/second writes backwards. - fixed mapper 073 IRQ info. Had modes backwards. - fixed 049 -- had PRG modes mixed up - fixed error in 090 -- had CHR block modes backwards - fixed 203 - additional notes and crap for 233 - alternative freq formula for N106 audio was wrong. Fixed it. - clarified 230 - changed D:# naming convention to R:# to avoid confusion (d is often used for bit positions) Still 111 mapper numbers (more coming, I swear!) v0.6 ================================== First real release (111 mapper numbers) ================================================ FILE: docs/mapper/mmc3_irq_tests_readme.txt ================================================ NTSC NES MMC3 IRQ Counter Test ROMs ----------------------------------- These ROMs test much of MMC3 IRQ counter behavior on an NTSC NES PPU. They have been tested on an actual NES with on several MMC3 cartridges and all give a passing result. Many tests are written specifically to catch likely errors in an emulator. Each ROM runs several tests and reports the result on screen and by beeping a number of times. Failure codes for each ROM are listed below. It's best to run the tests in order, because some earlier ROMs test things that later ones assume will work properly. The ROMs mainly test behavior by manually clocking the MMC3's IRQ counter by writing to $2006 to change the current VRAM address. The last two ROMs test different revisions of the MMC3, so at most only one will pass on a particular emulator. All the asm source is included, and most tests are clearly divided into sections. The code runs on a custom devcart and assembler so it will require some effort to assemble. Contact me if you'd like assistance porting them to your setup. MMC3 Operation -------------- I have fairly thoroughly tested MMC3 IRQ counter operation and found the following behaviors that differ as described in kevtris's (draft?) MMC3 documentation: - The counter can be clocked manually via bit 12 of the VRAM address even when $2000 = $00 (bg and sprites both use tiles from $0xxx). - The IRQ flag is not set when the counter is cleared by writing to $C001. - I uncovered some pathological behavior that isn't covered by the test ROMs. If $C001 is written, the counter clocked, then $C001 written again, on the next counter clock the counter will be ORed with $80 (revision B)/frozen (revision A) and neither decremented nor reloaded. If $C001 is written again at this point, on the next counter clock it will be reloaded normally. I put a check in my emulator and none of the several games I tested ever caused this situation to occur, so it's probably not a good idea to implement this. The MMC3 in Crystalis (referred to here as revision A) worked as described in kevtris's document, with the above changes. The MMC3 in Super Mario Bros. 3 and Mega Man 3 (I think revision B, but I don't have the special screw driver) further differed when $C000 was written with 0: - Writing 0 to $C000 works no differently than any other value written; it will cause the counter to be reloaded every time it is clocked (once it reaches zero). - When the counter is clocked, if it's not zero, it is decremented, otherwise it is reloaded with the last value written to $C000. *After* decrementing/reloading, if the counter is zero and IRQ is enabled via $E001, the IRQ flag is set. 1.Clocking ---------- Tests counter operation. Requires support for clocking via manual toggling of VRAM address. 2) Counter/IRQ/A12 clocking isn't working at all 3) Should decrement when A12 is toggled via $2006 4) Writing to $C000 shouldn't cause reload 5) Writing to $C001 shouldn't cause immediate reload 6) Should reload (no decrement) on first clock after clear 7) IRQ should be set when counter is decremented to 0 8) IRQ should never be set when disabled 9) Should reload when clocked when counter is 0 2.Details --------- Tests counter details. 2) Counter isn't working when reloaded with 255 3) Counter should run even when IRQ is disabled 4) Counter should run even after IRQ flag has been set 5) IRQ should not be set when counter reloads with non-zero 6) IRQ should not be set when counter is cleared via $C001 7) Counter should be clocked 241 times in PPU frame 3.A12 Clocking -------------- Tests clocking via bit 12 of VRAM address. 2) Shouldn't be clocked when A12 doesn't change 3) Shouldn't be clocked when A12 changes to 0 4) Should be clocked when A12 changes to 1 via $2006 write 5) Should be clocked when A12 changes to 1 via $2007 read 6) Should be clocked when A12 changes to 1 via $2007 write 4.Scanline Timing ----------------- Tests basic timing for scanlines 0, 1, and 240. 2) Scanline 0 time is too soon 3) Scanline 0 time is too late 4) Scanline 1 time is too soon 5) Scanline 1 time is too late 6) Scanline 239 time is too soon 7) Scanline 239 time is too late 5.MMC3 Rev A ------------ Tests MMC3 revision A differences (tested with Crystalis board). 2) IRQ should be set when reloading to 0 after clear 3) IRQ shouldn't occur when reloading after counter normally reaches 0 6.MMC3 Rev B ------------ Tests MMC3 revision B differences (tested with Super Mario Bros. 3 and Mega Man 3 boards). 2) Should reload and set IRQ every clock when reload is 0 3) IRQ should be set when counter is 0 after reloading -- Shay Green (swap to e-mail) ================================================ FILE: docs/mapper/mmc3_test_readme.txt ================================================ NES MMC3 Tests -------------- These tests verify a small part of MMC3 (and some MMC6) behavior, mostly related to the scanline counter and IRQ. They should be run in order. The ROMs mainly test behavior by manually clocking the MMC3's IRQ counter by writing to $2006 to change the current VRAM address. The last two ROMs test behavior that differs among MMC3 chips. MMC3 Operation -------------- I have fairly thoroughly tested MMC3 IRQ counter operation and found the following behaviors that differ as described in kevtris's (draft?) MMC3 documentation: - The counter can be clocked manually via bit 12 of the VRAM address even when $2000 = $00 (bg and sprites both use tiles from $0xxx). - The IRQ flag is not set when the counter is cleared by writing to $C001. - I uncovered some pathological behavior that isn't covered by the test ROMs. If $C001 is written, the counter clocked, then $C001 written again, on the next counter clock the counter will be ORed with $80 (revision B)/frozen (revision A) and neither decremented nor reloaded. If $C001 is written again at this point, on the next counter clock it will be reloaded normally. I put a check in my emulator and none of the several games I tested ever caused this situation to occur, so it's probably not a good idea to implement this. The MMC3 in Crystalis (referred to here as revision A) worked as described in kevtris's document, with the above changes. The MMC3 in Super Mario Bros. 3 and Mega Man 3 (I think revision B, but I don't have the special screw driver) further differed when $C000 was written with 0: - Writing 0 to $C000 works no differently than any other value written; it will cause the counter to be reloaded every time it is clocked (once it reaches zero). - When the counter is clocked, if it's not zero, it is decremented, otherwise it is reloaded with the last value written to $C000. *After* decrementing/reloading, if the counter is zero and IRQ is enabled via $E001, the IRQ flag is set. Multi-tests ----------- The NES/NSF builds in the main directory consist of multiple sub-tests. When run, they list the subtests as they are run. The final result code refers to the first sub-test that failed. For more information about any failed subtests, run them individually from rom_singles/ and nsf_singles/. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. -- Shay Green ================================================ FILE: docs/memory_mapping.txt ================================================ ************************************** ** ** ** NES Memory Mapping ** ** version 1.0 ** ** ** ** by Disch! ** ************************************** =================================== Introduction =================================== There are a bunch of docs around which cover 6502 and asm hacking in NES games. They do a good job of showing how you can modify existing game code to make it do something new. Some even cover how to insert your own code and jump to it, allowing you to add new stuff to your hack. But one related area which is crucial to really understanding how to do all this is either only briefly touched on with little explanation, or is not touched on at all. Leaving the reader with many unanswered questions. I'm talking, of course, about mappers. People hear terms like "mapper", and "PRG swapping" thrown around... but not everyone understands what they do or how they work. If you're one of these people, hopefully this doc will shed some light on the subject. This doc primarily focuses on the PRG/CHR swapping aspect of mappers. Other aspects of mappers (such as IRQ generation, mirroring control, sound, etc) are not covered in this document. =================================== What should you know? =================================== This doc does not cover 6502. It doesn't assume you know any 6502, however I doubt much of the information here will be very useful without it. This doc will assume you know nothing about NES architecture, and will cover some basic ares that relate to NES<->Mapper interaction. However... despite it not relying on you having much previous experience, the information gained will probably be useless unless you've been around the block a few times. =================================== ROM offsets vs. CPU Addresses =================================== ROM offsets and CPU Addresses are two distinctly different things. ROM offsets indicate a position the .nes file -- and CPU addresses indicate an area in the NES CPU memory map. Mappers are the key to translating between the two. In this doc... ROM offsets will always be prefixed with '0x', and CPU addresses will always be prefixed with '$'. Tangent note: some docs and tutorials erroneously call CPU addresses "RAM offsets" (and in fact, it says this in FCEUXD as well). I want to punch these authors, as that term is false and misleading (you're not always dealing with RAM, and it doesn't really offset anything). I urge you to refrain from using these terms, and to just say "address" for CPU addresses and "offset" for ROM offsets. It may not seem like a big deal, but it may ultimately lead to confusion when you really do need to differentiate between what's RAM and what isn't (and when you actually do have to deal with real RAM offsets). =================================== A look at the NES Memory Map =================================== The first step to understanding the mapper's primary function (memory mapping) is to examine the basic NES memory Map. You may have seen similar charts in other NES docs, but they usually clump the cartridge and NES together: NES CPU Memory Map: +-------------+----------------------------+ | Address | Mapped to | +-------------+----------------------------+ |$0000 - $1FFF| System RAM | |$2000 - $3FFF| PPU Registers | |$4000 - $401F| CPU/pAPU/Joypad Registers | |$4020 - $FFFF| Cartridge | +-------------+----------------------------+ The NES cannot address anything higher than $FFFF Typical Cartridge memory map: +-------------+----------------------------------+ | Address | Mapped to | +-------------+----------------------------------+ |$4020 - $5FFF| Unused | |$6000 - $7FFF| PRG-RAM (aka SRAM) | |$8000 - $FFFF| PRG-ROM (ie: the actual game) | +-------------+----------------------------------+ This is how the vast majority of NES games look. The NES CPU Memory Map as listed above is *always* the same. However, the cartridge memory map can vary wildly according to the cartridge layout and the mapper. What the hell does this mean? Well this means that any time the game reads or writes to an address below $4020, it's doing something local to the NES. However when it reads/writes to an address above $4020, it's accessing something on the cartridge and is subject to mapper intervention: LDA $0300 ; reading from NES system RAM LDA $8123 ; reading from cartridge (typically PRG-ROM) =================================== A look at .nes ROM files =================================== The next step to understanding how mappers map cartridge information is understand what we'll be mapping. This is actually pretty straightforward... the .nes file contains 3 main parts: 1) iNES Header 2) PRG-ROM 3) CHR-ROM (assuming the game has any) PRG and CHR ROM sizes are indicated by the header. The iNES header is $10 bytes in size, which means that PRG-ROM starts at offset 0x00010 in the .nes file. =================================== Why a mapper is needed =================================== For our first example... let's take a look at a small, simple game like Super Mario Bros. This ROM has 32K PRG. You can see this in FCEUXD by loading the ROM, then going to Help | Message Log (it will say "2 x 16KiB") 32K is $8000 bytes. This fits PERFECTLY into the typical cartridge memory map. For this paticular situation... a mapper isn't needed, because all of the PRG can fit into the allotted space at address range $8000-$FFFF. This means that when the game is reading from address $8000, it's reading offset 0x00010 (the first byte of PRG-ROM). Note, however... that this is ALL of the space in the CPU memory map that is designated for PRG-ROM. So how on Earth are games able to fit in more PRG? This is accomplished by a technique performed by mappers called PRG swapping. But before we get into PRG swapping... let's start with something a little easier to visualize.... =================================== CHR Swapping =================================== CHR Swapping is the same concept as PRG swapping, only it deals with CHR instead of PRG. This makes it a bit easier to visualize. A good game to use as an example is Super Mario Bros. 2. Load up this ROM in FCEUXD, start the first level, and open up the PPU Viewer (Tools | PPU Viewer). At the same tile, load up the ROM in your favorite tile editor and jump to offset 0x26010 Notice how in FCEUXD, the tiles on the bottom of the right-hand pattern table are animating. By looking at how the tiles are layed out in your tile editor, you should be able to see that the game is just cycling through a list of tiles. This actually is a great demonstration of what swapping is and how it works. Instead of looking at the pattern tables and seeing a bunch of tiles... try to look at it and see "slots": +----------------------------++----------------------------+ | Slot 0 || Slot 4 | | || | +----------------------------++----------------------------+ | Slot 1 || Slot 5 | | || | +----------------------------++----------------------------+ | Slot 2 || Slot 6 | | || | +----------------------------++----------------------------+ | Slot 3 || Slot 7 | | || | +----------------------------++----------------------------+ Each slot holds 4 rows of tiles (64 tiles)... this represents 1k ($400 bytes) of CHR. Now the NES cannot "see" more than 8k at a time (the above 8 slots). However... games can have more graphics by putting different CHR "pages" in those slots. When it wants to change which page is in the slot, it "swaps in" a new page. The page that was previously visible is removed, and the new page becomes visible. SMB2 is constantly switching which CHR page is put in slots 6 and 7. By constantly swapping in new pages, it appears as though the tiles are animating. This is a common animation technique used by NES games. The pages themselves can be seen in a tile editor. SMB2's CHR-ROM starts at offset 0x20010. Which means: page $00 = 0x20010 - 0x2040F (the first 64 tiles) page $01 = 0x20410 - 0x2080F (the next 64 tiles) page $02 = 0x20810 - 0x20C0F ... page $7F = 0x3FC10 - 0x4000F (the last 64 tiles) (the above assumes 1k pages) Note about CHR-RAM: Some games use CHR-RAM instead of CHR-ROM (these games don't have any CHR-ROM, and all their graphics data is intermingled in the PRG. Castlevania, Final Fantasy are examples of these types of games). For these games, the above doesn't apply. CHR-RAM is handled differently from CHR-ROM and is beyond the scope of this document (it's more related to PPU registers than mapper stuff) =================================== PRG Swapping =================================== Some people can pick up the concept of CHR swapping pretty quick, but then struggle with PRG swapping. Which is a little strange, because it's the EXACT same concept. Rather than looking at the $8000-$FFFF area in CPU addressing space as a big chunk of PRG... look at it as a series of slots. For another visual of a typical NES CPU memory map: $0000 +-----------------+ $8000 +-----------------+ | | | | | System RAM | | PRG Slot 0 | | | | | $2000 +-----------------+ $A000 +-----------------+ | | | | | PPU Registers | | PRG Slot 1 | | | | | $4000 +-----------------+ $C000 +-----------------+ | CPU/etc Regs | | | $5000 +-----------------+ | PRG Slot 2 | | Unused | | | $6000 +-----------------+ $E000 +-----------------+ | | | | | SRAM | | PRG Slot 3 | | | | | +-----------------+ +-----------------+ Just like how in CHR swapping, slots are filled with pages of tiles... with PRG swapping, slots are filled by pages of PRG. So while a game can only "see" 32k of PRG at any one time... it can still access much more than 32k by swapping out different pages. Just as CHR pages are stored in order in the .nes file, so are PRG pages: page $00 = 0x00010 - 0x0200F page $01 = 0x02010 - 0x0400F page $02 = 0x04010 - 0x0600F ... etc (the above assumes 8k pages) =================================== Converting ROM Offsets and CPU Addresses =================================== 6502 code and NES games all deal with CPU addresses. None of it deals with ROM offsets. This is why NES pointers appear so funky to newcomers. People sometimes come across a pointer such as "63 91", and get confused when they find that it actually points to offset 0x15173. But when you consider everything mentioned above.. it really does make perfect sense: - "63 91" = $9163 (CPU address) - $9163 is in "Slot 0" ($8000-9FFF) - $9163 is $1163 bytes into the slot (slot starts at $8000) - for this to point to 0x15173... page $0A (assuming 8k pages) must be swapped in slot 0: $0A * $2000 = 0x14000 (page# * page_size = start_of_page) 0x14000 + $1163 = 0x15163 (start_of_page + bytes_into_slot = offset) 0x15163 + 0x10 = 0x15173 (adjust for iNES header) That same pointer, however, could point to 0x0D173 or 0x05173 or 0x11173. It all depends on which page is swapped in. So how do you find out which page is swapped in? Well it's quite easy in FCEUXD. Simply open up the hex editor (Tools | Hex Editor), go to View | NES Memory, go to the desired CPU address, right click on it, and select "Go Here In ROM File". The hex editor will flip over to ROM offset mode and will give you the offset to the desired PRG. Note, however, that games are CONSTANTLY swapping PRG pages. It's not uncommon for games to swap pages multiple times every frame. Therefore it's recommended you snap the debugger at the appropriate time before finding which pages are swapped in. And remember that just because a page is swapped in now, doesn't mean it will be swapped in later. =================================== Variable slot sizes, swappable slots, and hardwired slots =================================== Up until now, I've been using 8k pages as examples. This is because the most common mapper (MMC3, mapper 4) uses 8k PRG pages. However, other mappers use different sizes. Some mappers even let the game PICK the size! Different size pages operate just like you may expect. 16k pages ($4000 bytes) would be stored in the ROM as: page $00 = 0x00010 - 0x0400F page $01 = 0x04010 - 0x0800F ... etc And 32k pages ($8000 bytes -- yes, some mappers use pages this big) would be stored as: page $00 = 0x00010 - 0x0800F page $01 = 0x08010 - 0x1000F ... etc Generally slots are arranged like so: 8k 16k 32k $8000 +--------+ +--------+ +--------+ | | | | | | $A000 +--------+ | | | | | | | | | | $C000 +--------+ +--------+ | | | | | | | | $E000 +--------+ | | | | | | | | | | +--------+ +--------+ +--------+ Mappers don't always let the game select a page for every slot. Some slots are fixed so that they always contain a specific page. These slots are often called "hardwired" (even though that term is technically inaccurate... "fixed" is more appropriate). Usually, the last slot is fixed to the last page of PRG, and cannot be swapped out. This all depends on the mapper being used and possibly the settings of that mapper. =================================== Nitty Gritty specific mapper details =================================== Everything mentioned so far as been intentionally generic. The concepts are sound and apply to every mapper out there. However each individual mapper does vary slightly in slot arrangement, PRG/CHR swapping ability, swappable/fixed slots, and even additional features not mentioned in this doc. And while the info mentioned above is all sound... you're eventually going to need to know the specifics. So here is a short list of some common mappers and their arrangement. This is by no means a complete list. I have no intention of making a complete list. There are sources for mapper details elsewhere -- if the mapper your game uses is not listed here, you'll have to find a different resource. This list also does not go into details of mapper registers. If you want to know how to manually swap out pages or do other things with the mapper, you'd be best suited to find a technical doc on said mapper. Finding out which mapper your game uses is pretty easy. Many emulators will tell you this information after the ROM is loaded. In FCEUXD, once you load the ROM, go to Help | Message Log, and it will tell you the mapper number. ************************ * Mapper 0 -- NROM * ************************ Mapper 0 is no mapper. There is no PRG/CHR swapping ability. ************************ * Mapper 1 -- MMC1 * ************************ Mapper 1 lets the game choose between 3 different PRG swapping schemes. 99% of games using this mapper use the first scheme listed: PRG: 16k Slot @ $8000 = Swappable 16k Slot @ $C000 = Fixed to last PRG page ---------OR-------- 16k Slot @ $8000 = Fixed to first PRG page (page 0) 16k Slot @ $C000 = Swappable ---------OR-------- 32k Slot @ $8000 = Swappable Mapper 1 also let's game switch between 2 different CHR swapping schemes. However, many games using this mapper use CHR-RAM. For those that don't: CHR: 4k Slot @ ppu$0000 = Swappable 4k Slot @ ppu$1000 = Swappable ---------OR-------- 8k Slot @ ppu$0000 = Swappable There are a handful of rare exceptions which rewire MMC1 in funky ways. Dragon Warrior 4, for instance, in a way breaks the above PRG swapping rules. For these games, refer to an MMC1 technical doc. ************************* * Mapper 2 -- UxROM * ************************* Mapper 2 is very simple. Only 1 swapping scheme. All games using this mapper have CHR-RAM, and thus do not swap CHR. PRG: 16k Slot @ $8000 = Swappable 16k Slot @ $C000 = Fixed to last PRG page ************************* * Mapper 4 -- MMC3 * ************************* MMC3 is hands down the most common mapper around. Odds are whatever game you're hacking (if it's a US release) is MMC3. Most games using this mapper use CHR-ROM and swap as indicated below: PRG: 8k Slot @ $8000 = Swappable 8k Slot @ $A000 = Swappable 8k Slot @ $C000 = Fixed to 2nd last PRG page 8k Slot @ $E000 = Fixed to last PRG page ----------OR---------- 8k Slot @ $8000 = Fixed to 2nd last PRG page 8k Slot @ $A000 = Swappable 8k Slot @ $C000 = Swappable 8k Slot @ $E000 = Fixed to last PRG page CHR: 2k Slot @ ppu$0000 = Swappable 2k Slot @ ppu$0800 = Swappable 1k Slot @ ppu$1000 = Swappable 1k Slot @ ppu$1400 = Swappable 1k Slot @ ppu$1800 = Swappable 1k Slot @ ppu$1C00 = Swappable ----------OR---------- 1k Slot @ ppu$0000 = Swappable 1k Slot @ ppu$0400 = Swappable 1k Slot @ ppu$0800 = Swappable 1k Slot @ ppu$0C00 = Swappable 2k Slot @ ppu$1000 = Swappable 2k Slot @ ppu$1800 = Swappable =================================== One final note =================================== When it comes to mappers... anything and everything goes. A "mapper" is really a combination of cartridge layout, wiring, MMC, and anything additional on the cart. Theoretically, it is even possible for an additional processor to be on the cartridge! So while there are general traits that are shared by many mappers... there are no "rules". The charts and explanations should be absorbed with the understanding that they are not carved in stone... and that there are mappers out there which deviate from the normal patterns. =================================== That's it! =================================== That's about everything I can think of. Hopefully this will clarify some of the grey areas left by other docs. =================================== Version History =================================== June 25, 2007 - version 1.0 - Initial release ================================================ FILE: docs/nes_arch.txt ================================================ Nintendo Entertainment System Architecture version 1.4 [09/09/1996] by Marat Fayzullin [fms@freeflight.com] WWW: http://www.freeflight.com/fms/ IRC: RST38h The following document describes the workings of Nintendo Entertainment System videogame console, also known as Famicom in the East (Korea, Japan), and Dandy in Europe (Russia, etc.). Note that this document is in no way based on any official Nintendo information and may be incomplete and incorrect in many places. "Nintendo Entertainment System" and "Famicom" are registered trademarks of Nintendo. I would like to thank following people for their help in obtaining this information and writing a NES emulator, as well as the moral support from some of them: (sorted alphabetically) Pascal Felber Pan of Anthrox John Stiles Kawasedo Patrick Lesaard Tink Marcel de Kogel Paul Robson Bas Vijfwinkel Alex Krasivsky Serge Skorobogatov The current version of this file is missing some information, such as sound hardware. I will add these parts in later releases. If you have any information on NES, which is not in this manual, feel free to write to fms@freeflight.com. Your help will be appreciated. ******************************* Contents ******************************* 1. General Architecture 2. Interrupts 3. I/O Ports 4. PPU Memory 5. Hit/VBlank Bits 6. Joysticks 7. Sprites 8. Memory Mappers a) Sequential b) Konami c) VROM Switch d) 5202 Chip e) Others 9. Sound (to be written) ************************* General Architecture ************************* NES is based on the 6502 CPU, and a custom video controller known as PPU (Picture Processing Unit). The PPU's video memory is separated from the main CPU memory and can be read/written via special ports. Cartridges may contain both ROM appearing in the main CPU address space at $8000-$FFFF, and VROM or VRAM appearing in the PPU address space at $0000-$1FFF and containing the Pattern Tables (aka Tile Tables). In smaller cartridges, which only have 16kB ROM, it takes place at $C000-$FFFF leaving $8000-$BFFF area unused. Internal NES VRAM is located at addresses $2000-$3FFF in the PPU memory. Some cartridges also have RAM at $6000-$7FFF, which may or may not be battery-backed. CPU Memory Map --------------------------------------- $10000 Upper Bank of Cartridge ROM --------------------------------------- $C000 Lower Bank of Cartridge ROM --------------------------------------- $8000 Cartridge RAM (may be battery-backed) --------------------------------------- $6000 Expansion Modules --------------------------------------- $5000 Input/Output --------------------------------------- $2000 2kB Internal RAM, mirrored 4 times --------------------------------------- $0000 ****************************** Interrupts ****************************** NES uses non-maskable interrupts (NMIs) generated by PPU in the end of each frame (so-called VBlank interrupts). Maskable interrupts, or IRQs, can also be generated by circuitry in a cart, but most carts do not generate them. The VBlank interrupts can be enabled/disabled by writing 1/0 into 7th bit of $2000. When a VBlank interrupts occur, CPU pushes return address and the status register on stack, and jumps to the address stored at location $FFFA (ROM in NES). The interrupt handler is supposed to finish its execution with RTI command which returns CPU to the main program execution. More information on the interrupt handling can be found in a decent book on 6502 CPU. ****************************** I/O ports ******************************* NES internal I/O ports are mapped into the areas of $2000-$2007 and $4000-$4017. Some ports' usage is unknown or unclear, and any information is appreciated. I/O Ports Map ------+-----+--------------------------------------------------------------- $2000 | RW | PPU Control Register 1 | 0-1 | Name Table to show: | | | | +-----------+-----------+ | | | 2 ($2800) | 3 ($2C00) | | | +-----------+-----------+ | | | 0 ($2000) | 1 ($2400) | | | +-----------+-----------+ | | | | Remember, though, that because of the mirroring, there are | | only 2 real Name Tables, not 4. | 2 | Vertical Write, 1 = PPU memory address increments by 32: | | | | Name Table, VW=0 Name Table, VW=1 | | +----------------+ +----------------+ | | |----> write | | | write | | | | | | V | | | | 3 | Sprite Pattern Table address, 1 = $1000, 0 = $0000 | 4 | Screen Pattern Table address, 1 = $1000, 0 = $0000 | 5 | Sprite Size, 1 = 8x16, 0 = 8x8 | 6 | Hit Switch, 1 = generate interrupts on Hit (incorrect ???) | 7 | VBlank Switch, 1 = generate interrupts on VBlank ------+-----+--------------------------------------------------------------- $2001 | RW | PPU Control Register 2 | 0 | Unknown (???) | 1 | Image Mask, 0 = don't show left 8 columns of the screen | 2 | Sprite Mask, 0 = don't show sprites in left 8 columns | 3 | Screen Switch, 1 = show picture, 0 = blank screen | 4 | Sprites Switch, 1 = show sprites, 0 = hide sprites | 5-7 | Unknown (???) ------+-----+--------------------------------------------------------------- $2002 | R | PPU Status Register | 0-5 | Unknown (???) | 6 | Hit Flag, 1 = PPU refresh has hit sprite #0 | | This flag resets to 0 when VBlank starts, or CPU reads $2002 | | (see "Hit/VBlank Bits"). | 7 | VBlank Flag, 1 = PPU is generating a Vertical Blanking Impulse | | This flag resets to 0 when VBlank ends, or CPU reads $2002 | | (see "Hit/VBlank Bits"). ------+-----+--------------------------------------------------------------- $2003 | W | Sprite Memory Address | | Used to set the address in the 256-byte Sprite Memory to be | | accessed via $2004. This address will increment by 1 after | | each access to $2004. The Sprite Memory contains coordinates, | | colors, and other attributes of the sprites (see "Sprites"). ------+-----+--------------------------------------------------------------- $2004 | RW | Sprite Memory Data | | Used to read/write the Sprite Memory. The address is set via | | $2003 and increments after each access. The Sprite Memory | | contains coordinates, colors, and other attributes of the | | sprites (see "Sprites"). ------+-----+--------------------------------------------------------------- $2005 | W | Background Scroll | | There are two scroll registers, vertical and horizontal, | | which are both written via this port. The first value written | | will go into the Vertical Scroll Register (unless it is >239, | | then it will be ignored). The second value will appear in the | | Horizontal Scroll Register. The Name Tables are assumed to be | | arranged in the following way: | | | | +-----------+-----------+ | | | 2 ($2800) | 3 ($2C00) | | | +-----------+-----------+ | | | 0 ($2000) | 1 ($2400) | | | +-----------+-----------+ | | | | When scrolled, the picture may span over several Name Tables. | | Remember, though, that because of the mirroring, there are | | only 2 real Name Tables, not 4. ------+-----+--------------------------------------------------------------- $2006 | | PPU Memory Address | | See "PPU Memory". ------+-----+--------------------------------------------------------------- $2007 | | PPU Memory Data | | See "PPU Memory". ------+-----+--------------------------------------------------------------- $4000-$4013 | Sound Registers | See "Sound". ------+-----+--------------------------------------------------------------- $4014 | W | DMA Access to the Sprite Memory | | Writing a value N into this port, causes an area of CPU memory | | at address $100*N to be transferred into the Sprite Memory. ------+-----+--------------------------------------------------------------- $4015 | W | Sound Switch | 0 | Channel 1, 1 = enable sound | 1 | Channel 2, 1 = enable sound | 2 | Channel 3, 1 = enable sound | 3 | Channel 4, 1 = enable sound | 4 | Channel 5, 1 = enable sound | 5-7 | Unused (???) ------+-----+--------------------------------------------------------------- $4016 | RW | Joystick 1 + Strobe | 0 | Joystick 1 data | 1 | Joystick 1 presence, 0 = connected | 2-5 | Unused, set to 0 (???) | 6-7 | Unknown, set to 10 (???) | | See "Joysticks". ------+-----+--------------------------------------------------------------- $4017 | R | Joystick 2 | 0 | Joystick 2 data | 1 | Joystick 2 presence, 0 = connected | 2-5 | Unused, set to 0 (???) | 6-7 | Unknown, set to 10 (???) | | See "Joysticks". ------+-----+--------------------------------------------------------------- ****************************** PPU Memory ****************************** In a real NES, reading/writing PPU memory should only be attempted during VBlank period. Many smaller ROMs have read-only memory (VROM) for the Pattern Tables. In this case, you won't be able to write into this memory. The $3F00 and $3F10 locations in VRAM mirror each other (i.e. it is the same memory cell) and define the background color of the picture. Writing to PPU memory: a) Write upper address byte into $2006 b) Write lower address byte into $2006 c) Write data into $2007. After each write, the address will increment either by 1 (bit 2 of $2000 is 0) or by 32 (bit 2 of $2000 is 1). Reading from PPU memory: a) Write upper address byte into $2006 b) Write lower address byte into $2006 c) Read data from $2007. The first byte read from $2007 will be invalid. Then, the address will increment by 1 after each read. Name Table contains tile numbers organized into 32 rows of 32 bytes each. Tiles are 8x8 pixels each. Therefore, the whole Name Table is 32x32 tiles or 256x256 pixels. In the NTSC version of NES, upper and lower 16 pixels are not shown, thus, the screen becomes 256x224 pixels. In the PAL version of NES, upper and lower 8 pixels are not show, thus, the screen becomes 256x240 pixels. Pattern Table contains tile images in the following format: Character Colors Contents of Pattern Table ...o.... 00010000 00010000 $10 +-> 00000000 $00 ..O.O... 00202000 00000000 $00 | 00101000 $28 .0...0.. 03000300 01000100 $44 | 01000100 $44 O.....O. 20000020 00000000 $00 | 10000010 $82 ooooooo. -> 11111110 11111110 $FE | 00000000 $00 O.....O. 20000020 00000000 $00 | 10000010 $82 0.....0. 30000030 10000010 $82 | 10000010 $82 ........ 00000000 00000000 $00 | 00000000 $00 +---------+ Note that only two bits for each pixel of a character are stored in the Pattern Table. Other two are taken from the Attribute Table. Thus, the total number of simultaneous colors on the NES screen is 16. Each byte in the Attribute Table represents a 4x4 group of tiles on the screen, which makes an 8x8 attribute table. Each 4x4 tile group is subdivided into four 2x2 squares as follows: (0,0) (1,0) 0| (2,0) (3,0) 1 (0,1) (1,1) | (2,1) (3,1) --------------+---------------- (0,2) (1,2) 2| (2,2) (3,2) 3 (0,3) (1,3) | (2,3) (3,3) The attribute byte contains upper two bits of the color number for each 2x2 square (the lower two bits are stored in the Pattern Table): Bits Function Tiles -------------------------------------------------------------- 7,6 Upper color bits for square 3 (2,2),(3,2),(2,3),(3,3) 5,4 Upper color bits for square 2 (0,2),(1,2),(0,3),(1,3) 3,2 Upper color bits for square 1 (2,0),(3,0),(2,1),(3,1) 1,0 Upper color bits for square 0 (0,0),(1,0),(0,1),(1,1) There are two 16-byte Palette Tables: the one at $3F00, used for the picture, and another one at $3F10, containing the sprite palette. The $3F00 and $3F10 locations in VRAM mirror each other (i.e. it is the same memory cell) and define the background color of the picture. There is only enough VRAM for 2 Name Tables and Attribute Tables. Two others are going to be mirrors of the first two, i.e. exact copies of them. Which pages are mirrored depends on the cartridge circuitry. With vertical mirroring, tables 2 and 3 are the mirrors of pages 0 and 1 appropriately. With horizontal mirroring, pages 1 and 3 are the mirrors of pages 0 and 2 appropriately. PPU Memory Map --------------------------------------- $4000 Empty --------------------------------------- $3F20 Sprite Palette --------------------------------------- $3F10 Image Palette --------------------------------------- $3F00 Empty --------------------------------------- $3000 Attribute Table 3 --------------------------------------- $2FC0 Name Table 3 (32x25 tiles) --------------------------------------- $2C00 Attribute Table 2 --------------------------------------- $2BC0 Name Table 2 (32x25 tiles) --------------------------------------- $2800 Attribute Table 1 --------------------------------------- $27C0 Name Table 1 (32x25 tiles) --------------------------------------- $2400 Attribute Table 0 --------------------------------------- $23C0 Name Table 0 (32x25 tiles) --------------------------------------- $2000 Pattern Table 1 (256x2x8, may be VROM) --------------------------------------- $1000 Pattern Table 0 (256x2x8, may be VROM) --------------------------------------- $0000 *************************** Hit/VBlank Bits **************************** The VBlank flag is contained in the 7th bit of read-only location $2002. It indicates whether PPU is scanning the screen, or generating a vertical blanking impulse. It is set in the end of each frame (scanline 232), and stays on until the next screen refresh starts from the scanline 8. The program can reset this bit prematurely by reading from $2002. The Hit flag is contained in the 6th bit of read-only location $2002. It goes to 1 when PPU starts refreshing the first scanline where sprite#0 is located. For example, if sprite#0's Y coordinate is 34, the Hit flag will be set in scanline 34. The Hit flag is reset when vertical blanking impulse starts. The program can reset this bit prematurely by reading from $2002. ******************************* Joysticks ****************************** There are two joysticks which are accessed via locations $4016 and $4017. To reset joysticks, write first 1, then 0 into $4016. This way, you will generate a strobe in the joysticks' circuitry. Then, read either from $4016 (for joystick 0) or from $4017 (for joystick 1). Each read will give you the status of a single button in the 0th bit (1 if pressed, 0 otherwise): Read # | 1 2 3 4 5 6 7 8 -------+--------------------------------------------------------- Button | A B SELECT START UP DOWN LEFT RIGHT Bit 1 indicates whether joystick is connected to the port or not. It is set to 0 if the joystick is connected, 1 otherwise. Bits 6 and 7 of $4016/$4017 also seem to have some significance, which is not clear yet. The rest of bits is set to zeroes. Some games expect to get *exactly* $41 from $4016/$4017, if a button is pressed, which has to be taken into account. ******************************* Sprites ******************************** There are 64 sprites, which can be either 8x8 or 8x16 pixels. Sprites patterns are stored in on of the Pattern Tables in the PPU Memory. Sprite attributes are stored in the Sprite Memory of 256 bytes, which is not a part of neither CPU nor PPU address space. The entire contents of Sprite Memory can be written via DMA transfer using location $4014 (see above). Sprite Memory can also be accessed byte-by-byte by putting the starting address into $2003 and then writing/reading $2004 (the address will be incremented after each access). The format of sprite attributes is as follows: Sprite Attribute RAM: | Sprite#0 | Sprite#1 | ... | Sprite#62 | Sprite#63 | | | +---- 4 bytes: 0: Y position of the left-top corner - 1 1: Sprite pattern number 2: Color and attributes: bits 1,0: two upper bits of color bits 2,3,4: Unknown (???) bit 5: if 1, display sprite behind background bit 6: if 1, flip sprite horizontally bit 7: if 1, flip sprite vertically 3: X position of the left-top corner Sprite patterns are fetched in the exactly same way as the tile patterns for the background picture. The only difference occurs in the 16x8 sprites: the top half of the sprite is taken from the Sprite Pattern Table set in the $2000 port, while the bottom part is taken from the same location of the alternative Pattern Table. Therefore, if PPU is displaying a 16x8 sprite, and the Sprite Pattern Table is set to $1000, the bottom half of this sprite will be taken out of the $0000 Pattern Table, and vice versa. **************************** Memory Mappers **************************** There are many diffirent memory mappers (aka MMCs) used in the NES cartridges. They are used to switch ROM and VROM pages, and do some other tasks. I will only describe the MMCs I'm familiar with. Any new information on these and other MMCs is highly appreciated. The MMC numbers are given in terms of the .NES file field "Mapper Type". 1. Mapper #1, Sequential This is a sequential mapper used in many 256kB cartridges, such as Bomberman 2, Destiny Of The Emperor, Megaman 2, Airwolf, Operation Wolf, Castlevania 2, Silk Worm, Yoshi, Break Thru. It may be used to switch ROM and VROM. If there is no VROM, 8kB of VRAM is present at $0000. In some cases (mostly RPG games) such cartridges also contain battery-backed RAM at $6000-$7FFF. The mapper has four 5bit registers, which are accessed via following addresses: Register Address Range Function --------------------------------------------------------------------------- 0 $8000-$9FFF Mirroring and VROM Page Size select The 0th bit of this register selects the mirroring type (1 for horizontal, 0 for vertical). The 4th bit selects the size of VROM pages. When it is 1, two 4kB VROM pages can be switched independently at $0000 and $1000. Otherwise, there is a single 8kB VROM page at $0000. 1 $A000-$BFFF VROM page select This register sets either 8kB or 4kB VROM page at $0000, depending on the page size selected via register 0. 2 $C000-$DFFF Second VROM page select for 4kB pages If 4kB VROM pages selected via register 0, this register sets the VROM page at $1000. Otherwise, its value is ignored. 3 $E000-$FFFF ROM page select This register sets 16kB ROM page at $8000. The page at $C000 is always hardwired to the last ROM page in the cartridge. The cartridge starts with page 0 at $8000. --------------------------------------------------------------------------- In order to write to a mapper register, write $80 into any of the locations first. This will reset the mapper. Then write the value bit by bit into an appropriate address range. For example, the following assembly code will write $0C into register 3: lda #$80 ; Resetting mapper sta $8000 ; lda #$0C ; This is our value sta $EFD9 ; Writing bit 0 lsr a ; Shifting sta $EFD9 ; Writing bit 1 lsr a ; Shifting sta $EFD9 ; Writing bit 2 lsr a ; Shifting sta $EFD9 ; Writing bit 3 lsr a ; Shifting sta $EFD9 ; Writing bit 4 2. Mapper #2, Konami This is a quite simple mapper used in most Konami (Life Force, Castlevania, Metal Gear) and some other cartridges. It only switches the ROM. All cartridges with this mapper have 8kB VRAM at $0000 (i.e. no VROM). The mapper has a single 8bit register which can be written via locations $8000-$FFFF. It contains a number of 16kB ROM page at $8000. The page at $C000 is always hardwired to the last ROM page in the cartridge. The cartridge starts with page 0 at $8000. There is one more thing to note about this mapper: although any address in the $8000-$FFFF range can be used to access the mapper, most games prefer to use the address with the last digit equal to the value they write out. Thus, $07 can be written to $9FF7, $05 to $9FF5, and so forth. The reason for this is unknown. 3. Mapper #3, VROM Switch Mapper #3, also known as a VROM switch, is used in the Goonies series and many Japanese-only games. It only allows you to switch 8kB pages of VROM. The ROM is either 16kB or 32kB and is not paged. The mapper has a single 8bit register which can be written via locations $8000-$FFFF. It contains a number of 8kB VROM page at $0000. As with mapper #2, many games use locations with the last digit equal to the value being written. I do not know why. 4. Mapper #4, 5202 Chip (???) This mapper (or should I say 'an expansion chip'?) is used in many recent cartridges, such as Batman Returns, Super Contra, Vindicators, Silver Surfer, etc. It is an extremely complicated device, which is able to generate its own interrupts via IRQ line, and has a set of commands to switch ROM and VROM. VROM pages are 1kB, ROM pages appear to be 8kB. I do not completely understand how this mapper works, so any information is appreciated. The chip is controlled via following locations: Address Function --------------------------------------------------------------------------- $8000 A command number (0-7) is written here. Also, write to this register appears to reset the change made by a write into $E000. $8001 An value for command is written here. $A000 The 0th bit controls mirroring (1 = horizontal mirroring). $A001 Same as $8001 (???) $C000 Unknown $C001 Unknown $E000 The 5th bit appears to swap memory at $8000-$8FFF and $A000-$AFFF, when set to 1. $E001 Unknown --------------------------------------------------------------------------- In order to use the mapper, you should first write a command number into $8000, and then a value (page number) into $8001. Following commands exist: Cmd Function --------------------------------------------------------------------------- 0 Select 2 consequent 1kB VROM pages at $0000. The 0th bit of a value written into $8001 does not matter, i.e. 5 will always select pages 4 and 5. 1 Select 2 consequent 1kB VROM pages at $0800. The 0th bit of a value written into $8001 does not matter, i.e. 5 will always select pages 4 and 5. 2 Select a 1kB VROM page at $1000. 3 Select a 1kB VROM page at $1400. 4 Select a 1kB VROM page at $1800. 5 Select a 1kB VROM page at $1C00. 6 Select a 8kB ROM page at $8000. The initial value seems to be 0. 7 Select a 8kB ROM page at $A000. The initial value seems to be 1. --------------------------------------------------------------------------- Note that the ROM pages at $C000 and $E000 are hardwired to the last pages of the ROM, and can not be switched (they can be swapped via $E000 though). 5. Other mappers There are several other mappers, some of them very sophisticated. iNES partially supports them, but as this support either doesn't work correctly, or the mappers are uncommon (such as 100-in-1 cartridge mapper, I don't cover them here. ******************************** Sound ********************************* To be written. --------------------- Marat Fayzullin ================================================ FILE: docs/nes_graphics.txt ================================================ How NES Graphics work The Basics --------------------- All the graphical information is stored within the 16kb memory of the Picture Processing Unit (PPU). The first area of PPU memory is known as the "Pattern Tables." The pattern tables are 8kb in size, which is usually split in half, one part for the background, the other for sprites. The usage of the pattern tables is determined by the PPU control registers. All graphics are stored in 8x8 pixel "tiles" within the pattern tables which are arranged to form backgrounds and sprites. The pattern tables (also known as VROM) contains half the graphics information for these tiles. The NES is capable of displaying 16 colors at once, since each pixel is 4 bits that are a lookup of the palette. The pattern table contains the low two bits, and the upper two either come from the attribute table (for the background) or from sprite ram (for sprites). Some roms contain what is known as CHR-RAM. CHR-RAM stands for CHaRacter RAM and contains 8kb banks of pattern tables which can be swapped in and out of the PPU via an MMC. This saves the trouble of copying it from within the program code to the PPU like games without CHR-RAM do. The second area of PPU memory is known as "Name Tables." There are usually two, although there are addresses for 4. The other two are mirrors of the actual tables, which is determined by the mirroring bit of the .NES header. Each name table is 960 bytes, which corresponds with 960 8x8 tiles that make up the background. The background is 32x30 tiles, or 256x240 pixels. In a NTSC NES, however, the top and bottom 8 lines are not displayed, making the actual resolution 256x224. Note that some games use only one name table, and others can use 4. These settings are dependant on the memory mapper used. Paired with each name table is an attribute table. These tables contain the upper two bits of each pixel's color that are matched with the lower two bits from the pattern table. However, the attribute table is only 64 bytes in size, meaning that each byte contains the upper two bits for a group of 4x4 tiles (or 32x32 pixels). This puts a limitation on your choice of colors in the background. However, you can get around the attribute table limitation by using MMC5, which allows you to use 4 name tables and the upper two bits for each individual tile. For more information, read Y0SHi's very informative NESTECH.DOC Also stored in PPU memory are two 16 color palettes. One is used for the background, the other for sprites. These are not actual rgb palettes, but lookup tables of a 256 color palette that the PPU translates into tv signals. You can modify the values stored in the palette, allowing you to create many effects such as fades, flashing, or transparency. There is an independent area of memory known as sprite ram, which is 256 bytes in size. This memory stores 4 bytes of information for 64 sprites. These 4 bytes contain the x and y location of the sprite on the screen, the upper two color bits, the tile index number (pattern table tile of the sprite), and information about flipping (horizontal and vertical), and priority (behind/on top of background). Sprite ram can be accessed byte-by-byte through the NES registers, or also can be loaded via DMA transfer through another register. There are various other aspects of the PPU that can be controlled via several nes registers. Here's a list of registers and what they do: PPU Control register 1 (PPUCTRL0): * Selects the Name table to display * Sets the ppu address increment (for reading/writing) * Sets the address within the pattern table for sprite tiles * Sets the address within the pattern table for background tiles * Selects the sprite size (8x8 or 8x16) * Sets whether to execute an interupt when drawing sprite 0 * Sets whether to execute an interupt during the Vblank period PPU Control register 2 (PPUCTRL1): * Sets the disply to color or mono-tone * Sets whether to clip the left 8 pixels of the background * Sets whether to clip sprites within the left 8 pixels of the background * Sets whether to display the screen or not * Selects the screen background color (black, red, blue, green) PPU Status register (PPUSTAT): * Returns whether PPU is in a Vblank period * Returns whether there are more than 8 sprites on the current scanline * Returns whether sprite 0 has been drawn on the current scanline Background Scroll (BGSCROLL): * Sets the horizontal and vertical scroll (written to twice) ================================================ FILE: docs/nes_tech.txt ================================================ +---------------------------------------------+ | Nintendo Entertainment System Documentation | | | | Version: 2.00 | +---------------------------------------------+ +-------------------+ | Table of Contents | +-------------------+ 1. Introduction A. Disclaimer B. Why? C. Mission D. Dedications E. "Thank You"s 2. Acronymns A. Internals B. Hardware 3. CPU A. General Information B. Memory Map C. Interrupts D. NES-specific Customizations E. Notes 4. PPU A. General Information B. PPU Map C. Name Tables D. Pattern Tables E. Attribute Tables F. Palettes G. Name Table Mirroring H. Palette Mirroring I. Background Scrolling J. Screen and Sprite Layering K. Sprites and SPR-RAM L. Sprite #0 Hit Flag M. Horizontal and Vertical Blanking N. $2005/2006 Magic O. VRAM Read/Writes P. Notes 5. pAPU 6. Joypads, paddles, expansion ports A. General Information B. The Zapper C. Four-player devices D. Paddles E. Power Pad F. R.O.B. (Robot Operated Buddy) G. Signatures H. Expansion ports I. Notes 7. Memory Mapping Hardware 8. Registers 9. File Formats A. iNES Format (.NES) 10. Programming the NES A. General Information B. CPU Notes C. PPU Notes 11. Emulation A. General Information B. CPU Notes C. PPU Notes D. APU Notes 12. Reference Material A. CPU Information B. PPU Information C. APU Information D. Memory Mapper Information E. Mailing Lists F. WWW Sites G. Hardware Information +-----------------+ | 1. Introduction | +-----------------+ A. Disclaimer ------------- I am in no way held responsible for the results of this information. This is public-domain information, and should not be used for commercial purposes. If you wish to use this document for commercial purposes, please contact me prior to development, so that I may discuss the outline of your project with you. I am not trying to hinder anyone financially: if you wish to do real Nintendo Entertainment System development, con- tacting either Nintendo of America or Nintendo Company, Ltd. would be wise. Their addresses are listed here: Nintendo of America Nintendo Company, Ltd. P.O. Box 957 60 Fukuine Redmond, WA 98073 Kamitakamatsu-cho, USA Higashiyama-ku, Koyoto 602, Japan All titles of cartridges and console systems are registered trademarks of their respective owners. (I just don't deem it necessary to list every single one by hand). B. Why? ------- At the time this document was created, there was only one piece of lit- erature covering the internals to the NES: Marat Fayzullin's documen- tation, otherwise known as "NES.DOC". While Fayzullin's documentation was lacking in many areas, it provided a strong base for the basics, and in itself truly stated how complex the little grey box was. I took the opportunity to expand on "NES.DOC," basing other people's findings, as well as my own, on experience; experience which has helped make this document what it has become today. The beginning stages of this document looked almost like a replica of Fayzullin's documentation, with both minor and severe changes. Marat Fayzullin himself later picked up a copy of my documentation, and later began referring people to it. Keep in mind, without Marat's "NES.DOC" document, I would have never had any incentive to write this one. C. Mission ---------- The goal of this document is simplistic: to provide accurate and up-to- date information regarding the Nintendo Entertainment System, and it's Famicom counterpart. D. Dedications -------------- I'd like to dedicate this document to Alex Krasivsky. Alex has been a great friend, and in my eyes, truly started the ball of emulation rolling. During the good times, and the bad times, Alex was there. Spasibo, Alex; umnyj Russki... E. "Thank You"s --------------- I'd like to take the time and thank all the people who helped make this document what it is today. I couldn't have done it without all of you. Alex Krasivsky - bcat@lapkin.rosprint.ru Andrew Davie Avatar Z - swahlen@nfinity.com Barubary - barubary@mailexcite.com Bluefoot - danmcc@injersey.com CiXeL Chi-Wen Yang - yangfanw@ms4.hinet.net Chris Hickman - typhoonz@parodius.com D - slf05@cc.usu.edu Dan Boris - dan.boris@coat.com David de Regt - akilla@earthlink.net Donald Moore - moore@futureone.com Fredrik Olsson - flubba@hem2.passagen.se Icer Addis - bldlust@maelstrom.net Jon Merkel - jpm5974@omega.uta.edu Kevin Horton - khorton@iquest.net Loopy - zxcvzxcv@netzero.net Marat Fayzullin - fms@cs.umd.edu Mark Knibbs - mark_k@iname.com Martin Nielsen - mnielsen@get2net.dk Matt Conte - itsbroke@classicgaming.com Matthew Richey - mr6v@andrew.cmu.edu Memblers - 5010.0951@tcon.net MiKael Iushin - acc@tulatelecom.ru Mike Perry - mj-perry@uiuc.edu Morgan Johansson - morgan.johansson@mbox301.swipnet.se Neill Corlett - corlett@elwha.nrrc.ncsu.edu Pat Mccomack - splat@primenet.com Patrik Alexandersson - patrikus@hem2.passagen.se Paul Robson - AutismUK@aol.com Ryan Auge - rauge@hay.net Stumble - stumble@alpha.pulsar.net Tennessee Carmel-Veilleux - veilleux@ameth.org Thomas Steen - Thomas.Steen@no.jotankers.com Tony Young - KBAAA@aol.com Vince Indriolo - indriolo@nm.picker.com \FireBug\ - lavos999@aol.com Special thanks goes out to Stumble, for providing a myriad of infor- mation over IRC, while avoiding sleep to do so. +--------------+ | 2. Acronymns | +--------------+ A. Internals ------------ CPU - Central Processing Unit: Self-explanitory. The NES uses a standard 6502 (NMOS). PPU - Picture Processing Unit: Used to control graphics, sprites, and other video-oriented features. pAPU - pseuedo-Audio Processing Unit: Native to the CPU; generates waveforms via (5) audio channels:: four (4) analogue, and one (1) digital. There is no physical IC for audio process- ing nor generation inside the NES. MMC - Multi-Memory Controller: Micro-controllers used in NES games to access memory beyond the 6502 64Kbyte boundary. They can also be used to access extra CHR-ROM, and may be used for "special effects" such as forcing and IRQ, and other things. VRAM - Video RAM: The RAM which is internal to the PPU. There is 16kbits of VRAM installed in the NES. SPR-RAM - Sprite RAM: 256 bytes of RAM which is used for sprites. It is not part of VRAM nor ROM, though it's local to the PPU. PRG-ROM - Program ROM: The actual program-code area of memory. Also can be used to describe areas of code which are external to the actual code area and are swapped in via an MMC. PRG-RAM - Program RAM: Synonymous with PRG-ROM, except that it's RAM. CHR-ROM - Character ROM: The VRAM data which is kept external to the PPU, swapped in and out via an MMC, or "loaded" into VRAM during the power-on sequence. VROM - Synonymous with CHR-ROM. SRAM - Save RAM: RAM which is commonly used in RPGs such as "Cry- stalis," the Final Fantasy series, and "The Legend of Zelda." WRAM - Synonymous with SRAM. DMC - Delta Modulation Channel: The channel of the APU which handles digital data. Commonly referred to as the PCM (Pulse Code Modulation) channel. EX-RAM - Expansion RAM: This is the memory used within Nintendo's MMC5, allowing games to extend the capabilities of VRAM. B. Hardware ----------- NES - Nintendo Entertainment System: Self-explanitory. Dandy - Synonymous (hardware-wise) with the Famicom. Famicom - Synonymous with the NES, except for not supporting the RAW method of DMC digital audio playback. FDS - Famicom Disk System: Unit which sits atop the Famicom, support- ing the use of 3" double-sided floppy disks for games. +--------+ | 3. CPU | +--------+ A. General Information ---------------------- The NES uses a customized NMOS 6502 CPU, engineered and produced by Ricoh. It's primary customization adds audio. The NTSC NES runs at 1.7897725MHz, and 1.773447MHz for PAL. B. Memory Map ------------- +---------+-------+-------+-----------------------+ | Address | Size | Flags | Description | +---------+-------+-------+-----------------------+ | $0000 | $800 | | RAM | | $0800 | $800 | M | RAM | | $1000 | $800 | M | RAM | | $1800 | $800 | M | RAM | | $2000 | 8 | | Registers | | $2008 | $1FF8 | R | Registers | | $4000 | $20 | | Registers | | $4020 | $1FDF | | Expansion ROM | | $6000 | $2000 | | SRAM | | $8000 | $4000 | | PRG-ROM | | $C000 | $4000 | | PRG-ROM | +---------+-------+-------+-----------------------+ Flag Legend: M = Mirror of $0000 R = Mirror of $2000-2008 every 8 bytes (e.g. $2008=$2000, $2018=$2000, etc.) C. Interrupts ------------- The 6502 has three (3) interrupts: IRQ/BRK, NMI, and RESET. Each interrupt has a vector. A vector is a 16-bit address which spec- ifies a location to "jump to" when the interrupt is triggered. IRQ/BRK is triggered under two circumstances, hence it's split name: when a software IRQ is executed (the BRK instruction), or when a hardware IRQ is executed (via the IRQ line). RESET is triggered on power-up. The ROM is loaded into memory, and the 6502 jumps to the address specified in the RESET vector. No registers are modified, and no memory is cleared; these only occur during power-up. NMI stands for Non-Maskable Interrupt, and is generated by each refresh (VBlank), which occurs at different intervals depending upon the system used (PAL/NTSC). NMI is updated 60 times/sec. on NTSC consoles, and 50 times/sec on PAL. Interrupt latency on the 6502 is seven (7) cycles; this means it takes seven (7) cycles to move in and out of an interrupt. Most interrupts should return using the RTI instruction. Some NES carts do not use this method, such as SquareSoft's "Final Fantasy 1" title. These carts return from interrupts in a very odd fashion: by manipul- ating the stack by hand, and then doing an RTS. This is technically valid, but morally shunned. The aforementioned nterrupts have the following vector addresses, mapped to areas of ROM: $FFFA = NMI $FFFC = RESET $FFFE = IRQ/BRK Interrupt priorities are as follows: Highest = RESET NMI Lowest = IRQ/BRK D. NES-specific Customizations ------------------------------ The NES's 6502 does not contain support for decimal mode. Both the CLD and SED opcodes function normally, but the 'd' bit of P is unused in both ADC and SBC. It is common practice for games to CLD prior to code execution, as the status of 'd' is unknown on power-on and on reset. Audio registers are mapped internal to the CPU; all waveform gener- ation is done internal to the CPU as well. E. Notes -------- Please note the two separate 16K PRG-ROM segments; they may be linear, but they play separate roles depending upon the size of the cartridge. Some games only hold one (1) 16K bank of PRG-ROM, which should be loaded into both $C000 and $8000. Most games load themselves into $8000, using 32K of PRG-ROM space. The first game to use this method is Super Mario Brothers. However, all games wit more than one (1) 16K bank of PRG-ROM load themselves into $8000 as well. These games use Memory Mappers to swap in and out PRG-ROM data, as well as CHR-ROM. When a BRK is encountered, the NES's 6502 pushes the CPU status flag onto the stack with the 'b' CPU bit set. On an IRQ or NMI, the CPU pushes the status flag onto the stack with the 'b' bit clear. This is done because of the fact that a hardware IRQ (IRQ) and a software IRQ (BRK) both share the same vector. For example, one could use the following code to distinguish the difference between the two: C134: PLA ; Copy CPU status flag C135: PHA ; Return status flag to stack C136: AND #$10 ; Check D4 ('b' CPU bit) C138: BNE is_BRK_opcode ; If set then it is a software IRQ (BRK) Executing BRK inside of NMI will result in the pushed 'b' bit being set. The 6502 has a bug in opcode $6C (jump absolute indirect). The CPU does not correctly calculate the effective address if the low-byte is $FF. Example: C100: 4F C1FF: 00 C200: 23 .. D000: 6C FF C1 - JMP ($C1FF) Logically, this will jump to address $2300. However, due to the fact that the high-byte of the calculate address is *NOT* increased on a page-wrap, this will actually jump to $4F00. It should be noted that page wrapping does *NOT* occur in indexed- indirect addressing modes. Due to limitations of zero-page, all indexed-indirect read/writes should apply a logical AND #$FF to the effective address after calculation. Example: C000: LDX #3 ; Reads indirect address from $0002+$0003, C002: LDA ($FF,X) ; not $0102+$0103. +--------+ | 4. PPU | +--------+ A. General Information ---------------------- Mirroring (also referred to as "shadowing") is the process of mapping particular addresses or address ranges to other addresses/ranges via hardware. B. Memory Map ------------- Included here are two (2) memory maps. The first is a "RAM Memory Map," which despite being less verbose describes the actual regions which point to physical RAM in the NES itself. The second is a "Programmer Memory Map" which is quite verbose and describes the entire memory region of the NES and how it's used/manipulated. RAM Memory Map +---------+-------+--------------------+ | Address | Size | Description | +---------+-------+--------------------+ | $0000 | $1000 | Pattern Table #0 | | $1000 | $1000 | Pattern Table #1 | | $2000 | $800 | Name Tables | | $3F00 | $20 | Palettes | +---------+-------+--------------------+ Programmer Memory Map +---------+-------+-------+--------------------+ | Address | Size | Flags | Description | +---------+-------+-------+--------------------+ | $0000 | $1000 | C | Pattern Table #0 | | $1000 | $1000 | C | Pattern Table #1 | | $2000 | $3C0 | | Name Table #0 | | $23C0 | $40 | N | Attribute Table #0 | | $2400 | $3C0 | N | Name Table #1 | | $27C0 | $40 | N | Attribute Table #1 | | $2800 | $3C0 | N | Name Table #2 | | $2BC0 | $40 | N | Attribute Table #2 | | $2C00 | $3C0 | N | Name Table #3 | | $2FC0 | $40 | N | Attribute Table #3 | | $3000 | $F00 | R | | | $3F00 | $10 | | Image Palette #1 | | $3F10 | $10 | | Sprite Palette #1 | | $3F20 | $E0 | P | | | $4000 | $C000 | F | | +---------+-------+-------+--------------------+ C = Possibly CHR-ROM N = Mirrored (see Subsection G) P = Mirrored (see Subsection H) R = Mirror of $2000-2EFF (VRAM) F = Mirror of $0000-3FFF (VRAM) C. Name Tables -------------- The NES displays graphics using a matrix of "tiles"; this grid is called a Name Table. Tiles themselves are 8x8 pixels. The entire Name Table itself is 32x30 tiles (256x240 pixels). Keep in mind that the displayed resolution differs between NTSC and PAL units. The Name Tables holds the tile number of the data kept in the Pattern Table (continue on). D. Pattern Tables ----------------- The Pattern Table contains the actual 8x8 tiles which the Name Table refers to. It also holds the lower two (2) bits of the 4-bit colour matrix needed to access all 16 colours of the NES palette. Example: VRAM Contents of Colour Addr Pattern Table Result ------ --------------- -------- $0000: %00010000 = $10 --+ ...1.... Periods are used to .. %00000000 = $00 | ..2.2... represent colour 0. .. %01000100 = $44 | .3...3.. Numbers represent .. %00000000 = $00 +-- Bit 0 2.....2. the actual palette .. %11111110 = $FE | 1111111. colour #. .. %00000000 = $00 | 2.....2. .. %10000010 = $82 | 3.....3. $0007: %00000000 = $00 --+ ........ $0008: %00000000 = $00 --+ .. %00101000 = $28 | .. %01000100 = $44 | .. %10000010 = $82 +-- Bit 1 .. %00000000 = $00 | .. %10000010 = $82 | .. %10000010 = $82 | $000F: %00000000 = $00 --+ The result of the above Pattern Table is the character 'A', as shown in the "Colour Result" section in the upper right. E. Attribute Tables ------------------- Each byte in an Attribute Table represents a 4x4 group of tiles on the screen. There's multiple ways to describe what the function of one (1) byte in the Attribute Table is: * Holds the upper two (2) bits of a 32x32 pixel grid, per 16x16 pixels. * Holds the upper two (2) bits of sixteen (16) 8x8 tiles. * Holds the upper two (2) bits of four (4) 4x4 tile grids. It's quite confusing; two graphical diagrams may help: +------------+------------+ | Square 0 | Square 1 | #0-F represents an 8x8 tile | #0 #1 | #4 #5 | | #2 #3 | #6 #7 | Square [x] represents four (4) 8x8 tiles +------------+------------+ (i.e. a 16x16 pixel grid) | Square 2 | Square 3 | | #8 #9 | #C #D | | #A #B | #E #F | +------------+------------+ The actual format of the attribute byte is the following (and corris- ponds to the above example): Attribute Byte (Square #) ---------------- 33221100 ||||||+--- Upper two (2) colour bits for Square 0 (Tiles #0,1,2,3) ||||+----- Upper two (2) colour bits for Square 1 (Tiles #4,5,6,7) ||+------- Upper two (2) colour bits for Square 2 (Tiles #8,9,A,B) +--------- Upper two (2) colour bits for Square 3 (Tiles #C,D,E,F) F. Palettes ----------- The NES has two 16-colour "palettes": the Image Palette and the Sprite Palette. These palettes are more of a "lookup table" than an actual palette, since they do not hold physical RGB values. D7-D6 of bytes written to $3F00-3FFF are ignored. G. Name Table Mirroring ----------------------- One should keep in mind that there are many forms of mirroring when understanding the NES. Some methods even use CHR-ROM-mapped Name Tables (mapper-specific). The NES itself only contains 2048 ($800) bytes of RAM used for Name Tables. However, as shown in Subsection B, the NES has the capability of addressing up to four (4) Name Tables. By default, many carts come with "horizontal" and "vertical" mirroring, allowing you to change where the Name Tables point into the NES's PPU RAM. This form of mirroring affects two (2) Name Tables simultaneously; you cannot switch Name Tables independently. The following chart should assist in understanding all the types of mirroring encountered on the NES. Please note that the addresses shown (12-bit in size) refer to the Name Table portion of the NES's PPU RAM; one may consider these synonymous with "$2xxx" in the VRAM region: Name NT#0 NT#1 NT#2 NT#3 Flags +--------------------------+------+------+------+------+-------+ | Horizontal | $000 | $000 | $400 | $400 | | | Vertical | $000 | $400 | $000 | $400 | | | Four-screen | $000 | $400 | $800 | $C00 | F | | Single-screen | | | | | S | | CHR-ROM mirroring | | | | | C | +--------------------------+------+------+------+------+-------+ F = Four-screen mirroring relies on an extra 2048 ($800) of RAM (kept on the cart), resulting in four (4) physical independent Name Tables. S = Single-screen games have mappers which allow you to select which PPU RAM area you want to use ($000, $400, $800, or $C00); all the NTs point to the same PPU RAM address. C = Mapper #68 (Afterburner 2) allows you to map CHR-ROM to the Name Table region of the NES's PPU RAM area. Naturally this makes the Name Table ROM-based, and one cannot write to it. However, this feature can be controlled via the mapper itself, allowing you to enable or disable this feature. H. Palette Mirroring -------------------- Mirroring occurs between the Image Palette and the Sprite Palette. Any data which is written to $3F00 is mirrored to $3F10. Any data written to $3F04 is mirrored to $3F14, etc. etc... Colour #0 in the upper three (3) palettes of both the Image and Sprite palette defines transparency (the actual colour stored there is not drawn on-screen). The PPU uses the value in $3F00 to define background colour. For a more verbose explanation, assume the following: * $0D has been written to $3F00 (mirrored to $3F10) * $03 has been written to $3F08 (mirrored to $3F18) * $1A has been written to $3F18 * $3F08 is read into the accumulator The PPU will use $0D as the background colour, despite $3F08 holding a value of $03 (since colour #0 in all the palette entries defines transparency, it is not drawn). Finally, the accumulator will hold a value of $1A, which is mirrored from $3F18. Again, the value of $1A is not drawn, since colour #0 defines transparency. The entire Image and Sprite Palettes are both mirrored to other areas of VRAM as well; $3F20-3FFF are mirrors of both palettes, respectively. D7-D6 of bytes written to $3F00-3FFF are ignored. I. Background Scrolling ----------------------- The NES can scroll the background (pre-rendered Name Table + Pattern Table + Attribute Table) independently of the sprites which are over- layed on top of it. The background can be scrolled horizontally and vertically. Scrolling works as follows: Horizontal Scrolling Vertical Scrolling 0 512 +-----+-----+ +-----+ 0 | | | | | | A | B | | A | | | | | | +-----+-----+ +-----+ | | | B | | | +-----+ 480 Name Table "A" is specified via Bits D1-D0 in register $2000, and "B" is the Name Table after (due to mirroring, this is dynamic). This doesn't work for game which use Horizontal & Vertical scrolling simultaneously. The background will span across multiple Name Tables, as shown here: +---------------+---------------+ | Name Table #2 | Name Table #3 | | ($2800) | ($2C00) | +---------------+---------------+ | Name Table #0 | Name Table #1 | | ($2000) | ($2400) | +---------------+---------------+ Writes to the Horizontal Scroll value in $2005 range from 0 to 256. Writes to the Vertical Scroll value range from 0-239; values above 239 are considered negative (e.g. a write of 248 is really -8). J. Screen and Sprite Layering ----------------------------- There is a particular order in which the NES draws it's contents: FRONT BACK +----+-----------+----+-----------+-----+ | CI | OBJs 0-63 | BG | OBJs 0-63 | EXT | +----+-----------+----+-----------+-----+ | SPR-RAM | | SPR-RAM | | BGPRI==0 | | BGPRI==1 | +-----------+ +-----------+ CI stands for 'Colour Intensity', which is synonmous with D7-D5 of $2001. BG is the BackGround, and EXT is for the EXTension port video signal. 'BGPRI' represents the 'Background Priority' bit in SPR-RAM, on a per-sprite basis (D5, Byte 2). OBJ numbers represent actual Sprite numbers, not Tile Index values. FRONT is considered what is seen atop all other layers (drawn last), and BACK is deemed what is below most other layers (drawn first). K. Sprites and SPR-RAM ---------------------- The NES supports 64 sprites, which can be either 8x8 or 8x16 pixels in size. The sprite data is kept within the Pattern Table region of VRAM. Sprite attributes such as flipping and priority, are stored in SPR-RAM, which is a separate 256 byte area of memory, independent of ROM and VRAM. The format of SPR-RAM is as follows: +-----------+-----------+-----+------------+ | Sprite #0 | Sprite #1 | ... | Sprite #63 | +-+------+--+-----------+-----+------------+ | | +------+----------+--------------------------------------+ + Byte | Bits | Description | +------+----------+--------------------------------------+ | 0 | YYYYYYYY | Y Coordinate - 1. Consider the coor- | | | | dinate the upper-left corner of the | | | | sprite itself. | | 1 | IIIIIIII | Tile Index # | | 2 | vhp000cc | Attributes | | | | v = Vertical Flip (1=Flip) | | | | h = Horizontal Flip (1=Flip) | | | | p = Background Priority | | | | 0 = In front | | | | 1 = Behind | | | | c = Upper two (2) bits of colour | | 3 | XXXXXXXX | X Coordinate (upper-left corner) | +------+----------+--------------------------------------+ The Tile Index # is obtained the same way as Name Table data. Sprites which are 8x16 in size function a little bit differently. A 8x16 sprite which has an even-numbered Tile Index # use the Pattern Table at $0000 in VRAM; odd-numbered Tile Index #s use $1000. *NOTE*: Register $2000 has no effect on 8x16 sprites. All 64 sprites contain an internal priority; sprite #0 is of a higher priority than sprites #63 (sprite #0 should be drawn last, etc.). Only eight (8) sprites can be displayed per scan-line. Each entry in SPR-RAM is checked to see if it's in a horizontal range with the other sprites. Remember, this is done on a per scan-line basis, not on a per sprite basis (e.g. done 256 times, not 256/8 or 256/16 times). (NOTE: On a real NES unit, if sprites are disabled (D4 of $2001 is 0) for a long period of time, SPR-RAM will gradually degrade. A proposed concept is that SPR-RAM is actually DRAM, and D4 controls the DRAM refresh cycle). L. Sprite #0 Hit Flag --------------------- The PPU is capable of figuring out where Sprite #0 is, and stores it's findings in D6 of $2002. The way this works is as follows: The PPU scans for the first actual non-transparent "sprite pixel" and the first non-transparent "background pixel." A "background pixel" is a tile which is in use by the Name Table. Remember that colour #0 defines transparency. The pixel which causes D6 to be set *IS* drawn. The following example should help. The following are two tiles. Transparent colours (colour #0) are defined via the underscore ('_') character. An asterisk ('*') represents when D6 will be set. Sprite BG Result ------ -- ------ __1111__ ________ __1111__ _111111_ _______2 _1111112 11222211 ______21 11222211 112__211 + _____211 = 112__*11 '*' will be drawn as colour #2 112__211 ____2111 112_2211 11222211 ___21111 11222211 _111111_ __211111 _1111111 __1111__ _2111111 _2111111 This also applies to sprites that are underneathe the BG (via the 'Background Priority' SPR-RAM bit), though the above example would be 'BG+Sprite'. Also, D6 is cleared (set to 0) after each VBlank. M. Horizontal and Vertical Blanking ----------------------------------- The NES, like every console, has a refresh: where the display device relocates the electron gun to display visible data. The most common display device is a television set. The refresh occurs 60 times a second on an NTSC device, and 50 on a PAL device. The gun itself draws pixels left to right: this process results in one (1) horizontal scanline being drawn. After the gun is done drawing the entire scanline, the gun must return to the left side of the display device, becoming ready to draw the next scanline. The process of the gun returning to the left side of the display is the Horizontal Blank period (HBlank). When the gun has completed drawing all of the scanlines, it must return to the top of the display device; the time it takes for the gun to re-position itself atop the device is called the Vertical Blank period (VBlank). As you can see from the below diagram, the gun more or less works in a zig-zag pattern until VBlank is reached, then the process repeats: +-----------+ +--->|***********| <-- Scanline 0 | | ___---~~~ | <-- HBlank V |***********| <-- Scanline 1 B | ___---~~~ | <-- HBlank l | ... | ... a | ... | ... n |***********| <-- Scanline 239 k +-----+-----+ | | +--VBlank--+ An NTSC NES has the following refresh and screen layout: +--------+ 0 ----+ | | | | | | | Screen | +-- (0-239) 256x240 on-screen results | | | | | | +--------+ 240 --+ | ?? | +-- (240-242) Unknown +--------+ 243 --+ | | | | VBlank | +-- (243-262) VBlank | | | +--------+ 262 --+ The Vertical Blank (VBlank) flag is contained in D7 of $2002. It indicates whether PPU is in VBlank or not. A program can reset D7 by reading $2002. N. $2005/2006 Magic ------------------- For detailed information pertaining to the $2005 and $2006 registers, refer to Loopy's $2005/2006 document. His document provides entirely accurate information regarding how these registers work. Contact Loopy for more information. O. PPU Quirks ------------- The first read from VRAM is invalid. Due to this aspect, the NES will returned pseudo-buffered values from VRAM rather than linear as expec- ted. See the below example: VRAM $2000 contains $AA $BB $CC $DD. VRAM incrementation value is 1. The result of execution is printed in the comment field. LDA #$20 STA $2006 LDA #$00 STA $2006 ; VRAM address now set at $2000 LDA $2007 ; A=?? VRAM Buffer=$AA LDA $2007 ; A=$AA VRAM Buffer=$BB LDA $2007 ; A=$BB VRAM Buffer=$CC LDA #$20 STA $2006 LDA #$00 STA $2006 ; VRAM address now set at $2000 LDA $2007 ; A=$CC VRAM Buffer=$AA LDA $2007 ; A=$AA VRAM Buffer=$BB As shown, the PPU will post-increment it's internal address data after the first read is performed. This *ONLY APPLIES* to VRAM $0000-3EFF (e.g. Palette data and their respective mirrors do not suffer from this phenomenon). P. Notes -------- The PPU will auto-increment the VRAM address by 1 or 32 (based on D2 of $2000) after accessing $2007. +---------+ | 5. pAPU | +---------+ To be written. Prior information was inaccurate or incorrect. No one has 100% accurate sound information at this time. This section will be completed when someone decides to reverse engineer the pAPU section of the NES, and provide me with information (or a reference to infor- mation). +--------------------------------------+ | 6. Joypads, paddles, expansion ports | +--------------------------------------+ A. General Information ---------------------- The NES supports a myriad of input devices, including joypads, Zappers (light guns), and four-player devices. Joypad #1 and #2 are accessed via $4016 and $4017, respectively. The joypads are reset via a strobing-method: writing 1, then 0, to $4016. See Subsection H for information regarding "half-strobing." On a full strobe, the joypad's button status will be returned in a single-bit stream (D0). Multiple reads need to be made to read all the information about the controller. 1 = A 9 = Ignored 17 = +--+ 2 = B 10 = Ignored 18 = +-- Signature 3 = SELECT 11 = Ignored 19 = | 4 = START 12 = Ignored 20 = +--+ 5 = UP 13 = Ignored 21 = 0 6 = DOWN 14 = Ignored 22 = 0 7 = LEFT 15 = Ignored 23 = 0 8 = RIGHT 16 = Ignored 24 = 0 See Subsection G for information about Signatures. B. The Zapper ------------- The Zapper (otherwise known as the "Light Gun") simply uses bits within $4016 and $4017, described in Section 8. See bits D4, D3, and D0. It is possible to have two Zapper units connected to both joypad ports simultaneously. C. Four-player devices ---------------------- Some NES games allow the use of a four-player adapter, extending the number of usable joypads from two (2) to four (4). Carts which use the quad-player device are Tengen's "Gauntlet II," and Nintendo's "RC Pro Am 2." All four (4) controllers read their status-bits from D0 of $4016 or $4017, as Subsection A states. For register $4016, reads #1-8 control joypad #1, and reads #9-16 control joypad #3. For $4017, it is respective for joypad #2 and #4. The following is a list of read #s and their results. 1 = A 9 = A 17 = +--+ 2 = B 10 = B 18 = +-- Signature 3 = SELECT 11 = SELECT 19 = | 4 = START 12 = START 20 = +--+ 5 = UP 13 = UP 21 = 0 6 = DOWN 14 = DOWN 22 = 0 7 = LEFT 15 = LEFT 23 = 0 8 = RIGHT 16 = RIGHT 24 = 0 See Subsection G for information about Signatures. D. Paddles ---------- Taito's "Arkanoid" uses a paddle as it's primary controller. The paddle position is read via D1 of $4017; the read data is inverted (0=1, 1=0). The first value read is the MSB, and the 8th value read is (obviously) the LSB. Valid value ranges are 98 to 242, where 98 rep- resents the paddle being turned completely counter-clockwise. For example, if %01101011 is read, the value would be NOT'd, making %10010100 which is 146. The paddle also contains one button, which is read via D1 of $4016. A value of 1 specifies that the button is being pressed. E. Power Pad ------------ No information is currently available. F. R.O.B. (Robot Operated Buddy) -------------------------------- No information is currently available. G. Signatures ------------- A signature allows the programmer to detect if a device is connected to one of the four (4) ports or not, and if so, what type of device it is. Valid/known signatures are: %0000 = Disconnected %0001 = Joypad ($4016 only) %0010 = Joypad ($4017 only) H. Expansion ports ------------------ The joypad strobing process requires dual writes: 1, then 0. If the strobing process is not completed, or occurs in a non-standard order, the joypads are no longer the item of communication: the expansion port is. For NES users, the expansion port is located on the bottom of the unit, covered by a small grey piece of plastic. Famicom users have a limited expansion port on the front of their unit, which was commonly used for joypads or turbo-joypads. Such an example of communicating with the expansion port would be the following code: LDA #%00000001 STA $4016 STA $4017 ; Begin read mode of expansion port LDA #%00000011 ; Write %110 to the expansion port STA $4016 I have yet to encounter a cart which actually uses this method of communication. I. Notes -------- None. +----------------------------+ | 7. Memory Mapping Hardware | +----------------------------+ Due to the large number of mappers used (over 64), the "MMC" section which was once fluid in v0.53 of this document, has now been removed. All is not lost, as another document by \FireBug\ of Vertigo 2099 con- tains accurate information about nearly every mapper in existence. You can retrieve a copy via one of the following URLs: http://free.prohosting.com/~nintendo/mappers.nfo Please note I take no responsibility for the information contained in the aforementioned document. Contact lavos999@aol.com for more information. +--------------+ | 8. Registers | +--------------+ Programmers communicate with the PPU and pAPU via registers, which are nothing more than pre-set memory locations which allow the coder to make changes to the NES. Without registers, programs wouldn't work: period. Each register is a 16-bit address. Each register has a statistics field in parentheses located immediately after its description. The legend: R = Readable W = Writable 2 = Double-write register 16 = 16-bit register NOTE: 16-bit registers actually consist of two linear 8-bit registers, which can (and will be) *INDEPENDANTLY* assigned. The reason for specifying them as 16-bit is for ease of documentation. For instance, "$4002+$4003" would mean that D15-D8 would be in $4003, and D7-D0 would be in $4002. NOTE: Bits not listed are to be considered unused. +---------+----------------------------------------------------------+ | Address | Description | +---------+----------------------------------------------------------+ | $2000 | PPU Control Register #1 (W) | | | | | | D7: Execute NMI on VBlank | | | 0 = Disabled | | | 1 = Enabled | | | D6: PPU Master/Slave Selection --+ | | | 0 = Master +-- UNUSED | | | 1 = Slave --+ | | | D5: Sprite Size | | | 0 = 8x8 | | | 1 = 8x16 | | | D4: Background Pattern Table Address | | | 0 = $0000 (VRAM) | | | 1 = $1000 (VRAM) | | | D3: Sprite Pattern Table Address | | | 0 = $0000 (VRAM) | | | 1 = $1000 (VRAM) | | | D2: PPU Address Increment | | | 0 = Increment by 1 | | | 1 = Increment by 32 | | | D1-D0: Name Table Address | | | 00 = $2000 (VRAM) | | | 01 = $2400 (VRAM) | | | 10 = $2800 (VRAM) | | | 11 = $2C00 (VRAM) | +---------+----------------------------------------------------------+ | $2001 | PPU Control Register #2 (W) | | | | | | D7-D5: Full Background Colour (when D0 == 1) | | | 000 = None +------------+ | | | 001 = Green | NOTE: Do not use more | | | 010 = Blue | than one type | | | 100 = Red +------------+ | | | D7-D5: Colour Intensity (when D0 == 0) | | | 000 = None +--+ | | | 001 = Intensify green | NOTE: Do not use more | | | 010 = Intensify blue | than one type | | | 100 = Intensify red +--+ | | | D4: Sprite Visibility | | | 0 = Sprites not displayed | | | 1 = Sprites visible | | | D3: Background Visibility | | | 0 = Background not displayed | | | 1 = Background visible | | | D2: Sprite Clipping | | | 0 = Sprites invisible in left 8-pixel column | | | 1 = No clipping | | | D1: Background Clipping | | | 0 = BG invisible in left 8-pixel column | | | 1 = No clipping | | | D0: Display Type | | | 0 = Colour display | | | 1 = Monochrome display | +---------+----------------------------------------------------------+ | $2002 | PPU Status Register (R) | | | | | | D7: VBlank Occurance | | | 0 = Not occuring | | | 1 = In VBlank | | | D6: Sprite #0 Occurance | | | 0 = Sprite #0 not found | | | 1 = PPU has hit Sprite #0 | | | D5: Scanline Sprite Count | | | 0 = Eight (8) sprites or less on current scan- | | | line | | | 1 = More than 8 sprites on current scanline | | | D4: VRAM Write Flag | | | 0 = Writes to VRAM are respected | | | 1 = Writes to VRAM are ignored | | | | | | NOTE: D7 is set to 0 after read occurs. | | | NOTE: After a read occurs, $2005 is reset, hence the | | | next write to $2005 will be Horizontal. | | | NOTE: After a read occurs, $2006 is reset, hence the | | | next write to $2006 will be the high byte portion. | | | | | | For detailed information regarding D6, see Section 4, | | | Subsection L. | +---------+----------------------------------------------------------+ | $2003 | SPR-RAM Address Register (W) | | | | | | D7-D0: 8-bit address in SPR-RAM to access via $2004. | +---------+----------------------------------------------------------+ | $2004 | SPR-RAM I/O Register (W) | | | | | | D7-D0: 8-bit data written to SPR-RAM. | +---------+----------------------------------------------------------+ | $2005 | VRAM Address Register #1 (W2) | | | | | | Commonly used used to "pan/scroll" the screen (sprites | | | excluded) horizontally and vertically. However, there | | | is no actual panning hardware inside the NES. This | | | register controls VRAM addressing lines. | | | | | | Refer to Section 4, Subsection N, for more information. | +---------+----------------------------------------------------------+ | $2006 | VRAM Address Register #2 (W2) | | | | | | Commonly used to specify the 16-bit address in VRAM to | | | access via $2007. However, this register controls VRAM | | | addressing bits, and therefore should be used with | | | knowledge of how it works, and when it works. | | | | | | Refer to Section 4, Subsection N, for more information. | +---------+----------------------------------------------------------+ | $2007 | VRAM I/O Register (RW) | | | | | | D7-D0: 8-bit data read/written from/to VRAM. | +---------+----------------------------------------------------------+ | $4000 | pAPU Pulse #1 Control Register (W) | | $4001 | pAPU Pulse #1 Ramp Control Register (W) | | $4002 | pAPU Pulse #1 Fine Tune (FT) Register (W) | | $4003 | pAPU Pulse #1 Coarse Tune (CT) Register (W) | | $4004 | pAPU Pulse #2 Control Register (W) | | $4005 | pAPU Pulse #2 Ramp Control Register (W) | | $4006 | pAPU Pulse #2 Fine Tune Register (W) | | $4007 | pAPU Pulse #2 Coarse Tune Register (W) | | $4008 | pAPU Triangle Control Register #1 (W) | | $4009 | pAPU Triangle Control Register #2 (?) | | $400A | pAPU Triangle Frequency Register #1 (W) | | $400B | pAPU Triangle Frequency Register #2 (W) | | $400C | pAPU Noise Control Register #1 (W) | | $400D | Unused (???) | | $400E | pAPU Noise Frequency Register #1 (W) | | $400F | pAPU Noise Frequency Register #2 (W) | | $4010 | pAPU Delta Modulation Control Register (W) | | $4011 | pAPU Delta Modulation D/A Register (W) | | $4012 | pAPU Delta Modulation Address Register (W) | | $4013 | pAPU Delta Modulation Data Length Register (W) | +---------+----------------------------------------------------------+ | $4014 | Sprite DMA Register (W) | | | | | | Transfers 256 bytes of memory into SPR-RAM. The address | | | read from is $100*N, where N is the value written. | +---------+----------------------------------------------------------+ | $4015 | pAPU Sound/Vertical Clock Signal Register (R) | | | | | | D6: Vertical Clock Signal IRQ Availability | | | 0 = One (1) frame occuring, hence IRQ cannot | | | occur | | | 1 = One (1) frame is being interrupted via IRQ | | | D4: Delta Modulation | | | D3: Noise | | | D2: Triangle | | | D1: Pulse #2 | | | D0: Pulse #1 | | | 0 = Not in use | | | 1 = In use | | +----------------------------------------------------------+ | | pAPU Channel Control (W) | | | | | | D4: Delta Modulation | | | D3: Noise | | | D2: Triangle | | | D1: Pulse #2 | | | D0: Pulse #1 | | | 0 = Channel disabled | | | 1 = Channel enabled | +---------+----------------------------------------------------------+ | $4016 | Joypad #1 (RW) | | | | | | READING: | | | D4: Zapper Trigger | | | 0 = Pulled | | | 1 = Released (not held) | | | D3: Zapper Sprite Detection | | | 0 = Sprite not in position | | | 1 = Sprite in front of cross-hair | | | D0: Joypad Data | | +----------------------------------------------------------+ | | WRITING: | | | Joypad Strobe (W) | | | | | | D0: Joypad Strobe | | | 0 = Clear joypad strobe | | | 1 = Reset joypad strobe | | +----------------------------------------------------------+ | | WRITING: | | | Expansion Port Latch (W) | | | | | | D0: Expansion Port Method | | | 0 = Write | | | 1 = Read | +---------+----------------------------------------------------------+ | $4017 | Joypad #2/SOFTCLK (RW) | | | | | | READING: | | | D7: Vertical Clock Signal (External) | | | 0 = Not occuring | | | 1 = Occuring | | | D6: Vertical Clock Signal (Internal) | | | 0 = Occuring (D6 of $4016 affected) | | | 1 = Not occuring (D6 of $4016 untouchable) | | | D4: Zapper Trigger | | | 0 = Pulled | | | 1 = Released (not held) | | | D3: Zapper Sprite Detection | | | 0 = Sprite not in position | | | 1 = Sprite in front of cross-hair | | | D0: Joypad Data | | +----------------------------------------------------------+ | | WRITING: | | | Expansion Port Latch (W) | | | | | | D0: Expansion Port Method | | | 0 = ??? | | | 1 = Read | +---------+----------------------------------------------------------+ +-----------------+ | 9. File Formats | +-----------------+ A. iNES Format (.NES) --------------------- +--------+------+------------------------------------------+ | Offset | Size | Content(s) | +--------+------+------------------------------------------+ | 0 | 3 | 'NES' | | 3 | 1 | $1A | | 4 | 1 | 16K PRG-ROM page count | | 5 | 1 | 8K CHR-ROM page count | | 6 | 1 | ROM Control Byte #1 | | | | %####vTsM | | | | | ||||+- 0=Horizontal mirroring | | | | | |||| 1=Vertical mirroring | | | | | |||+-- 1=SRAM enabled | | | | | ||+--- 1=512-byte trainer present | | | | | |+---- 1=Four-screen mirroring | | | | | | | | | | +--+----- Mapper # (lower 4-bits) | | 7 | 1 | ROM Control Byte #2 | | | | %####0000 | | | | | | | | | | +--+----- Mapper # (upper 4-bits) | | 8-15 | 8 | $00 | | 16-.. | | Actual 16K PRG-ROM pages (in linear | | ... | | order). If a trainer exists, it precedes | | ... | | the first PRG-ROM page. | | ..-EOF | | CHR-ROM pages (in ascending order). | +--------+------+------------------------------------------+ +-------------------------+ | 10. Programming the NES | +-------------------------+ A. General Information ---------------------- None. B. CPU Notes ------------ None. See Section 11, Subsection B for more possible information. C. PPU Notes ------------ Reading and writing to VRAM consists of a multi-step process: Writing to VRAM Reading from VRAM --------------- ----------------- 1) Wait for VBlank 1) Wait for VBlank 2) Write upper VRAM address 2) Write upper VRAM address byte into $2006 byte into $2006 3) Write lower VRAM address 3) Write lower VRAM address byte into $2006 byte into $2006 4) Write data to $2007 4) Read $2007 (invalid data once) 5) Read data from $2007 NOTE: Step #4 when reading VRAM is only necessary when reading VRAM data not in the $3F00-3FFF range. NOTE: Accessing VRAM should only be performed during VBlank. Attempts to access VRAM outside of VBlank will usually result in garbage showing up on the screen. See Section 4, Subsection N for more information regarding why this occurs. Waiting for VBlank is quite simple: 8000: LDA $2002 BPL $8000 Reading $2002 will result in all bits being returned; however, D7 will be reset to 0 after the read is performed. The actual on-screen palette used by the NES, as stated prior, is not RGB. However, a near-exact replica can be found in common NES emulators today. Contact the appropriate authors of these emulators to obtain a valid RGB palette. Be sure to clear the internal VRAM address via $2006 semi-often. You will often encounter a situation where a palette fade or a VRAM update will cause the screen "to be trashed" (squares on the screen, or what seem to be graphical "glitches"). The reason for this is that your code took longer than a VBlank. When the VBlank goes to refresh the screen with the data in the PPU, it takes whatever value is in the internal VRAM address and uses that as the starting base for Name Table #0. The solution is to fix your code by re-assigning the VRAM address to $0000 (or $2000), so that the refresh may occur successfully. Such code would be: LDA #$00 STA $2006 STA $2006 You will find code like this in commercial games quite often. +---------------+ | 11. Emulation | +---------------+ A. General Information ---------------------- If you're going to be programming an emulator in C or C++, please be familiar with pointers. Being familiar with pointers will help you out severely when it comes to handling mirroring and VRAM addressing. For you assembly buffs out there, obviously pointers are nothing more than indirect addressing -- it's easier to change a 32-bit value than to swap in and out an entire 64K of data. When SRAM ($6000-7FFF) is disabled, writes to the memory area should be ignored. Reads will possibly return data previously left on the bus, and therefore when emulated should return 0 (or should be trap- ped). RAM-based memory areas ($0000-07FF) should *NOT* be zeroed on RESET; they should be zeroed on power on/off. (Technically, the RAM is not zeroed on power on/off either: the RAM will slowly dissapate over time when the unit it off. However, for emulation purposes, please make sure that a cold boot and a warm boot do different things). See Section 12, Subsection E for Mailing List information. B. CPU Notes ------------ The NES does not use a 65c02 (CMOS) CPU as rumored. Ignore opcodes which are bad (or support the option of trapping them). Some ROM images out there, such as "Adventures of Lolo" contain bad opcodes, due to dirty connectors on the cartridge during the extract- ion process (or other reasons). There are 154 valid opcodes (out of 256 total) on the NES. C. PPU Notes ------------ The formulae to calculate the base address of a Name Table tile number is: (TILENUM * 16) + PATTERNTABLE Where TILENUM is the tile number in the Name Table, and PATTERNTABLE is the Pattern Table Address defined via register $2000. It's recommended that DOS programmers use what is known as "MODE-Q," a 256x256x256 "tweaked" video mode, for writing their emulator. Try to avoid Mode-X modes, as they are non-chained, and result in painfully slow graphics. Chained modes (like MODE-13h) are linear, and work best for speedy graphics. Since the NES's resolution is 256x240, the aforementioned "MODE-Q" should meet all necessary requirements. Most emulators do not limit the number of sprites which can be displayed per scanline, while the actual NES will show flicker as a result of more than eight (8). {Put some more garbage here; it's early...} Emulators should _NOT_ mask out unused bits within registers; doing so may result in a cart not working. D. APU Notes ------------ To be written. +------------------------+ | 12. Reference Material | +------------------------+ A. CPU Information ------------------ None. B. PPU Information ------------------ None. C. APU Information ------------------ None. D. MMC Information ------------------ None. E. Mailing Lists ---------------- There is a NES Development Mailing List in existence. Contact Mark Knibbs for more information. This list is for anyone who wishes to discuss tech- nical issues about the NES; it is not a list for picking up the latest and greatest information about NES emulators or what not. F. WWW Sites ------------ The following are a list of WWW sites which contain NES-oriented material. If you encounter errors, bad links, or other anomolies while visiting these sites, contact the site authors/owners, NOT me. Thanks. http://nesdev.parodius.com/ Contains a verbose amount of documentation regarding anything NES-oriented, including hard-to-find mapper documentation. Seems to be a decent NES information depository. http://www.ameth.org/~veilleux/NES_info.html Currently only contains hardware-oriented material, such as overviews of cart and unit ASICs, mappers, and MMCs. Many pinout diagrams for mappers and NES units are available here. Also provides documentation on NES repair, modifying your NES to give stereo output, applying stereo mixing to your NES, and much much more. G. Hardware Information ----------------------- The following security bits may be purchased from MCM Electronics (http://www.mcmelectronics.com/): For NES carts: 22-1145 (3.8mm security bit) For NES units: 22-1150 (4.5mm security bit) The 4.5mm security screw is also used for the Super Nintendo Enter- tainment System (SNES), and Nintendo 64. ================================================ FILE: docs/ppu/blargg_tests_readme.txt ================================================ NTSC NES PPU Tests ------------------ These ROMs test a few aspects of the NTSC NES PPU operation. They have been tested on an actual NES and all give a passing result. I wrote them to verify that my NES emulator's PPU was working properly. Each ROM runs several tests and reports a result code on screen and by beeping a number of times. A result code of 1 always indicates that all tests were passed; see below for the meaning of other codes for each test. The main source code for each test is included, and most tests are clearly divided into sections. Some of the common support code is included, but not all, since it runs on a custom setup. Contact me if you want to assemble the tests yourself. Shay Green (swap to e-mail) palette_ram ----------- PPU palette RAM read/write and mirroring test 1) Tests passed 2) Palette read shouldn't be buffered like other VRAM 3) Palette write/read doesn't work 4) Palette should be mirrored within $3f00-$3fff 5) Write to $10 should be mirrored at $00 6) Write to $00 should be mirrored at $10 power_up_palette ---------------- Reports whether initial values in palette at power-up match those that my NES has. These values are probably unique to my NES. 1) Palette matches 2) Palette differs from table sprite_ram ---------- Tests sprite RAM access via $2003, $2004, and $4014 1) Tests passed 2) Basic read/write doesn't work 3) Address should increment on $2004 write 4) Address should not increment on $2004 read 5) Third sprite bytes should be masked with $e3 on read 6) $4014 DMA copy doesn't work at all 7) $4014 DMA copy should start at value in $2003 and wrap 8) $4014 DMA copy should leave value in $2003 intact vbl_clear_time -------------- The VBL flag ($2002.7) is cleared by the PPU around 2270 CPU clocks after NMI occurs. 1) Tests passed 2) VBL flag cleared too soon 3) VBL flag cleared too late vram_access ----------- Tests PPU VRAM read/write and internal read buffer operation 1) Tests passed 2) VRAM reads should be delayed in a buffer 3) Basic Write/read doesn't work 4) Read buffer shouldn't be affected by VRAM write 5) Read buffer shouldn't be affected by palette write 6) Palette read should also read VRAM into read buffer 7) "Shadow" VRAM read unaffected by palette transparent color mirroring ================================================ FILE: docs/ppu/nmi_sync_ntsc_readme.txt ================================================ NES Precise NMI Synchronization ------------------------------- This library allows synchronizing exactly to the PPU from within a normal NMI handler. It allows PPU writes from within an NMI handler of the same precision that is otherwise only possible using completely cycle-timed code. It supports NTSC and PAL. The code is written for the ca65 assembler. Other assemblers will require minor changes. For more about how the technique works, see http://wiki.nesdev.com/w/index.php/Consistent_frame_synchronization . Demos ----- NTSC and PAL demos are included. These show minimal use of this library to manually draw a line using timed writes. They manually draw a line by setting bit 0 of $2001 to enable monochrome mode. The time of the write determines the position on screen, so any synchronization problems will cause the line's left side to move. Reference lines are shown above and below the manually-drawn one, showing the correct left edge position. On the NTSC version, the left pixel of the middle line will be darker, since it's flashing: ******************** *** -******************* *** ******************** On the PAL version, the left pixel or two will be darker, since it's flashing. The left edge's general position will change randomly each time you press reset. The upper line shows the farthest left it can ever be after reset, and the lower line shows the farthest right it can be. It may appear as one of the following: ******************** *** -******************* *** ****************** ******************** *** -****************** *** ****************** ******************** *** -***************** *** ****************** Usage ----- To use this library: * Include "nmi_sync.s". * Call init_nmi_sync[_pal] before synchronization is needed, then wait in a loop that calls wait_nmi, does anything that is necessary between NMIs, then loops back. * Inside NMI, call begin_nmi_sync, do sprite DMA, delay appropriately, then call end_nmi_sync. If running on NTSC NES, sprite and/or background rendering MUST be enabled before calling end_nmi_sync. It can be disabled again after it returns. * On frames where synchronization isn't needed, but will be needed a few frames later, call track_nmi_sync. If it won't be needed for a long time, nothing needs to be done, and init_nmi_sync can be called again later to re-synchronize and start over. After end_nmi_sync returns, the next instruction will be synchronized to 2286 (NTSC)/7471 (PAL) cycles after the frame began. If the NMI handler's timing is off by even one cycle, synchronization will fail sometimes. To verify timing, write an odd value to $2001 after synchronization. The point where monochrome mode begins on the scanline should be very stable. If it ever jiggles, then something is wrong in your code. The following demonstrates: .include "nmi_sync.s" reset: ... jsr init_nmi_sync/init_nmi_sync_pal loop: jsr wait_nmi ...anything done outside of NMI... jmp loop nmi: ...save registers... jsr begin_nmi_sync ; count as 6 cycles ... ; Instructions between nmi: and STA $4014 must take an even ; number of cycles. STA $4014 must be done as a part of ; synchronization. sta $4014 ; count as 4 cycles ... ; Instructions between nmi: and here must take ; 1715 (NTSC)/6900 (PAL) cycles. delay 1715 - ... ; NTSC delay 6900 - ... ; PAL ; On NTSC, sprite and/or background rendering MUST be ; enabled at this point, or else synchronization will ; be lost. jsr end_nmi_sync ; Sprite and background rendering can be disabled again ; at this point, if it's not needed. ; Next instruction is now synchronized to exactly ; 2286 (NTSC)/7471 (PAL) cycles after cycle that ; frame began in. ... ...restore registers... rti NTSC Timing ----------- Given the following NMI handler nmi: ... jsr end_nmi_sync delay N cycles lda #$01 sta $2001 ; writes 2286+N+5 cycles into frame The $2001 write will be 2286+N+5 cycles after frame began. To have the $2001 write at a particular pixel, calculate N with pixel = y * 341 + x N = (pixel + 290) / 3 For example, to write at y=121 x=80, N should be 13877. The pixel position can be calculated from N with pixel = N * 3 - 290 y = pixel / 341 x = pixel - (y * 341) where y=0 x=0 is the top-left pixel. For example, if the delay is 13877, then the $2001 write will occur at y=121 x=80. After init_nmi_sync is called, the first, third, fifth, etc. frames have the above timing. On the second, fourth, sixth, etc. frames, the write is one pixel LATER (x=81 in the example). This one-pixel jitter is an unavoidable hardware limitation. The above applies to enabling monochrome mode by setting bit 0 of $2001; other registers take effect at slightly different times. For some registers, the pixel written can vary slightly after pressing reset. It's best to use the above as a guide, reduce delay until glitches occur due to it occurring too early, increase delay until glitches occur as well, then choose a delay in the middle of those two extremes. PAL Timing ---------- Given the following NMI handler nmi: ... jsr end_nmi_sync delay N cycles lda #$01 sta $2001 ; writes 7471+N+5 cycles into frame The $2001 write will be 7471+N+5 cycles after frame began. To have the $2001 write at a particular pixel, calculate N with pixel = y * 341 + x N = (pixel * 5 + 1444) / 16 For example, to write at y=121 x=82, N should be 13009. The pixel position can be calculated from N with pixel = (N * 16 - 1444 + extra) / 5 y = pixel / 341 x = pixel - (y * 341) where y=0 x=0 is the top-left pixel, and extra is an additional delay that depends on whether it's an even or odd frame, and also a random offset selected when reset is pressed. For example, if the delay is 13009, then the $2001 write will occur no earlier than y=121 x=81. After init_nmi_sync is called, the first, third, fifth, etc. frames have the above timing. On the second, fourth, sixth, etc. frames, extra is 8 greater, causing the write to be one or two pixels LATER (x=83 or 84 in the example). This jitter is an unavoidable hardware limitation. After pressing reset, extra is set to a random value from 0 to 7, causing writes to be one or two pixels later. This doesn't change until reset is pressed. This is also unavoidable. The above applies to enabling monochrome mode by setting bit 0 of $2001; other registers take effect at slightly different times. For some registers, the pixel written can vary slightly after pressing reset. It's best to use the above as a guide, reduce delay until glitches occur due to it occurring too early, increase delay until glitches occur as well, then choose a delay in the middle of those two extremes. Limitations ----------- * DMC samples can't be played, since they introduce too much timing variation. A normal NMI performs just as well/poorly in this case. * If NMI occurs while executing an instruction that takes more than three cycles, synchronization will be upset for that frame. Note that a taken branch counts as more than three cycles, due to an obscure detail. To avoid this, call wait_nmi each frame, or sit in a loop of instructions two/three-cycle instructions. I have found some workarounds that allow NMI to occur during almost any instruction, but they require some extra helper sprites and use of the sprite overflow flag; contact me for details. * Every frame from that point, NMI must either call begin/end_nmi_sync, or track_nmi_sync, or else synchronization will be lost and init_nmi_sync will need to be called again. * The NMI handler must not read $2002 until after end_nmi_sync has been called. * After synchronizing, NMI and rendering must be enabled by the next frame, and left enabled (rendering can be disabled on PAL since it doesn't affect PPU timing). If rendering isn't desired, it can be enabled just before calling end_nmi_sync, then disabled afterwards. * Sprite DMA must be done on frames needing synchronization, even if no sprites are being used. * Even when perfectly synchronized, frames don't always begin exactly on a cycle. On NTSC, a given cycle will toggle between two adjacent pixels. On PAL, it will toggle between the calculated pixel and one or two pixels after. These effects are hardware limitations; this library synchronizes as precisely as is possible in software. Thanks ------ * Bregalad for his initial questions that inspired the idea, and for trying an early version. -- Shay Green ================================================ FILE: docs/ppu/oam_read_readme.txt ================================================ NES OAM Read Test ----------------- Tests OAM reading ($2004), being sure it reads the byte from OAM at the current address in $2003. It scans OAM from 0 to $FF, testing each byte in sequence. It prints a '-' where it reads back from the current address, and '*' where it doesn't. Each row represents 16 bytes of OAM, 16 rows total. Results ------- On my NTSC front-loader NES, I get the following four general patterns at random after power/reset: ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- oam_read Passed ---------------- ---------------- --------*------* ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- 694ADBE0 oam_read Failed ---------------- ---------------- ********-------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- E9E8E60F oam_read Failed **************** *********------- --------*-*-*-*- *-*-*-*-*-*-*-*- *-*-*-*-*-*-*-*- *-*-*-*-*-*-*-*- ***-*-*-*-*-*-*- *-*-*-*-*-*-*-*- *-*-*-*-*-*-*-*- *-*-*-*-*-*-*-*- *-*-*-*-*-*-*-*- *-*-*-*-*-*-*-*- *-*-*-*-*-*-*-*- *-*-*-*-*-*-*-*- ***-*-*-*-*-*-*- *-*-*-*-*-*-*-*- 44551956 oam_read Failed Flashes, clicks, other glitches ------------------------------- Some tests might need to turn the screen off and on, or cause slight audio clicks. This does not indicate failure, and should be ignored. Only the test result reported at the end is important, unless stated otherwise. Text output ----------- Tests generally print information on screen. They also output the same text as a zero-terminted string beginning at $6004, allowing examination of output in an NSF player, or a NES emulator without a working PPU. The tests also work properly if the PPU doesn't set the VBL flag properly or doesn't implement it at all. The final result is displayed and also written to $6000. Before the test starts, $80 is written there so you can tell when it's done. If a test needs the NES to be reset, it writes $81 there (emulator should wait a couple of frames after seeing $81). In addition, $DE $B0 $G1 is written to $6001-$6003 to allow an emulator to detect when a test is being run, as opposed to some other NES program. In NSF builds, the final result is also reported via a series of beeps (see below). See the source code for more information about a particular test and why it might be failing. Each test has comments and correct output at the top. NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to avoid the NSF player from thinking the track is silent and thus ending the track before it's done testing. In addition to the other text output methods described above, NSF builds report essential information bytes audibly, including the final result. A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason as listed in the source code by the corresponding set_code line. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 -- Shay Green ================================================ FILE: docs/ppu/oam_stress_readme.txt ================================================ NES OAM Stress Test ------------------- Thoroughly tests OAM address ($2003) and read/write ($2004). On an NTSC NES, this passes only for one of the four random PPU-CPU synchronizations at power/reset. Test takes about 30 seconds, unless it fails. This test randomly sets the address, then randomly either writes a random number of random bytes, or reads from the current address a random number of times and verifies that it matches what's expected. It does this for tens of seconds (refreshing OAM periodically so it doesn't fade). Once done, it verifies that all bytes in OAM match what's expected. Expected behavior: $2003 write sets OAM address. $2004 write sets byte at current OAM address to byte written, then increments OAM address. $2004 read gives byte at current OAM address, without modifying OAM address. Flashes, clicks, other glitches ------------------------------- Some tests might need to turn the screen off and on, or cause slight audio clicks. This does not indicate failure, and should be ignored. Only the test result reported at the end is important, unless stated otherwise. Text output ----------- Tests generally print information on screen. They also output the same text as a zero-terminted string beginning at $6004, allowing examination of output in an NSF player, or a NES emulator without a working PPU. The tests also work properly if the PPU doesn't set the VBL flag properly or doesn't implement it at all. The final result is displayed and also written to $6000. Before the test starts, $80 is written there so you can tell when it's done. If a test needs the NES to be reset, it writes $81 there (emulator should wait a couple of frames after seeing $81). In addition, $DE $B0 $G1 is written to $6001-$6003 to allow an emulator to detect when a test is being run, as opposed to some other NES program. In NSF builds, the final result is also reported via a series of beeps (see below). See the source code for more information about a particular test and why it might be failing. Each test has comments and correct output at the top. NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to avoid the NSF player from thinking the track is silent and thus ending the track before it's done testing. In addition to the other text output methods described above, NSF builds report essential information bytes audibly, including the final result. A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason as listed in the source code by the corresponding set_code line. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 -- Shay Green ================================================ FILE: docs/ppu/open_bus_readme.txt ================================================ NES PPU Open-Bus Test --------------------- Tests behavior when reading from open-bus PPU bits/registers, those bits that aren't otherwise defined. Unlike other open-bus addresses, the PPU ones are separate. Takes about 5 seconds to run. The PPU effectively has a "decay register", an 8-bit register. Each bit can be refreshed with a 0 or 1. If a bit isn't refreshed with a 1 for about 600 milliseconds, it will decay to 0 (some decay sooner, depending on the NES and temperature). Writing to any PPU register sets the decay register to the value written. Reading from a PPU register is more complex. The following shows the effect of a read from each register: Addr Open-bus bits 7654 3210 - - - - - - - - - - - - - - - - $2000 DDDD DDDD $2001 DDDD DDDD $2002 ---D DDDD $2003 DDDD DDDD $2004 ---- ---- $2005 DDDD DDDD $2006 DDDD DDDD $2007 ---- ---- non-palette DD-- ---- palette A D means that this bit reads back as whatever is in the decay register at that bit, and doesn't refresh the decay register at that bit. A - means that this bit reads back as defined by the PPU, and refreshes the decay register at the corresponding bit. Flashes, clicks, other glitches ------------------------------- Some tests might need to turn the screen off and on, or cause slight audio clicks. This does not indicate failure, and should be ignored. Only the test result reported at the end is important, unless stated otherwise. Text output ----------- Tests generally print information on screen. They also output the same text as a zero-terminted string beginning at $6004, allowing examination of output in an NSF player, or a NES emulator without a working PPU. The tests also work properly if the PPU doesn't set the VBL flag properly or doesn't implement it at all. The final result is displayed and also written to $6000. Before the test starts, $80 is written there so you can tell when it's done. If a test needs the NES to be reset, it writes $81 there (emulator should wait a couple of frames after seeing $81). In addition, $DE $B0 $G1 is written to $6001-$6003 to allow an emulator to detect when a test is being run, as opposed to some other NES program. In NSF builds, the final result is also reported via a series of beeps (see below). See the source code for more information about a particular test and why it might be failing. Each test has comments and correct output at the top. NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to avoid the NSF player from thinking the track is silent and thus ending the track before it's done testing. In addition to the other text output methods described above, NSF builds report essential information bytes audibly, including the final result. A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason as listed in the source code by the corresponding set_code line. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 -- Shay Green ================================================ FILE: docs/ppu/ppu_2c02_ref.txt ================================================ ******************************* *NTSC 2C02 technical reference* ******************************* Brad Taylor (BTTDgroup@hotmail.com) 5th release: April 23rd, 2004 Thanks to the NES community. http://nesdev.parodius.com. Special thanks to Neal Tew for scrolling information. Recommended literature: Nintendo's PPU patent document (U.S.#4,824,106). Note: to display this document properly, your text viewer needs two things: 1. support for the classic VGA-based text mode 256 character set with line-drawing characters. 2. word-wrap. windows notepad can easially do both if you change the font over to terminal style. Topics discussed ---------------- 2C02 integrated components list 2C02 pin nomenclature & signal descriptions 2C02 programming Video signal generation PPU base timing Miscellanious PPU info PPU memory access cycles Frame rendering details Scanline rendering details In-range object evaluation Details of playfield render pipeline Details of object pattern fetch & render Extra cycle frames The MMC3's scanline counter PPU pixel priority quirk PPU scrolling & addressing in a nutshell +-------------------------------+ |2C02 integrated components list| +-------------------------------+ - control registers and misc. flags - pixel & scanline counters - colorburst phase generator - VRAM address latches & counters - picture address buffer (tile index byte) - VRAM read buffer - object attribute memory (OAM) - OAM element pointer register/counter - OAM temporary memory & scanline comparator - vertical & horizontal inverter - OAM pixel buffers - playfield pixel buffer - multiplexer - palette memory - level decoder/phase selector/DAC - byte pointer flip-flop +-------------------------------------------+ |2C02 pin nomenclature & signal descriptions| +-------------------------------------------+ ___ ___ |* \/ | R/W >01] [40< VCC D0 [02] [39> ALE D1 [03] [38] AD0 D2 [04] [37] AD1 D3 [05] [36] AD2 D4 [06] [35] AD3 D5 [07] [34] AD4 D6 [08] [33] AD5 D7 [09] [32] AD6 A2 >10] 2C02 [31] AD7 A1 >11] [30> A8 A0 >12] [29> A9 /CS >13] [28> A10 EXT0 [14] [27> A11 EXT1 [15] [26> A12 EXT2 [16] [25> A13 EXT3 [17] [24> /R CLK >18] [23> /W /VBL <19] [22< /SYNC VEE >20] [21> VOUT |________| R/W, D0-D7, A2-A0, /CS: these are the PPU's control bus signals responsible for programming the 2C02's internal registers. R/W controls data direction (write data into PPU reg on zero), A0-A2 selects the internal PPU register to read/write, and while /CS is set to zero, D0-D7 is used to transfer the data bits to/from the selected register (if /CS=1, D0-D7 float). The next section documents the operation of the registers. EXT0-EXT3: this bus can either be used as a pixel input (for overlapping externally generated graphics with the 2C02's), or output (for driving another graphics processor), depending on how the 2C02 is programmed. Normally this bus is programmed to be an input, since NES/FC mainboards always ground these four pins. CLK: this is the 2C02's 21.48 MHz clock input line. /VBL: this signal issues a zero logic level when the PPU has entered it's VBLANK time, and can stay zero for as long as 20 scanlines. This signal is usually tied to the 2A03's /NMI line, in order to generate the non-maskable interrupt on a per-frame basis. Software acknowledging the /VBL-based interrupt usually quickly clear & set again a /VBL gate bit via a register, so that the time /VBL is active is usually less than a scanline. This output is also open-collector. VEE, VCC: ground, and +5VDC power signals, respectfully. VOUT: the 2C02's unbuffered composite video output. This signal usually travels to a two-stage common collector transistor amplifier, in order to boost the video drive to support 75 ohm loads at 1 volt peak-to-peak. /SYNC: this signal when zero, will force the status of colorburst control, scanline and pixel counters/flip-flops used inside the PPU to definite states. Generally, this is the means of which two 2C02s connected together in a master-slave config (via the EXT bus) can syncronize together; the master PPU's /VBL line feeds the vblank information to the slave's /SYNC input. On Famicom consoles, this pin is always tied to logical one. On the NES however, this pin is tied in with the 2A03's reset input, and as a result, the picture is always disabled while the reset switch is held in on an NES. /R, /W, ALE, AD0-AD7, A8-A13: these signals control the PPU-related data bus. ALE is activated (logical one) when the PPU puts address bits 0 thru 7 on the AD bus (typically a 74LS373 is used to store the low address bus contents). An active /R or /W signal (when logical zero) indicates that a memory device connected to the PPU's AD bus may decode the 14-bit address (formed between the external A0-A7 latch, and the A8-A13 lines), and drive data in the direction indicated (/R means data is being sent into the 2C02, opposite for /W). Never more than one /R, /W, or ALE signal is activated simultaniously. +----------------+ |2C02 programming| +----------------+ This section lays out how 2C02 ports & programmable internal memory structures are organized. Names for these ports throughout the document will simply consist of adding $200 to the end of the number (i.e., $2002). Anything not explained here will be later on. Writable 2C02 registers ----------------------- reg bit desc --- --- ---- 0 0 X scroll name table selection. 1 Y scroll name table selection. 2 increment PPU address by 1/32 (0/1) on access to port 7 3 object pattern table selection (if bit 5 = 0) 4 playfield pattern table selection 5 8/16 scanline objects (0/1) 6 EXT bus direction (0:input; 1:output) 7 /VBL disable (when 0) 1 0 disable composite colorburst (when 1). Effectively causes gfx to go black & white. 1 left side screen column (8 pixels wide) playfield clipping (when 0). 2 left side screen column (8 pixels wide) object clipping (when 0). 3 enable playfield display (on 1). 4 enable objects display (on 1). 5 R (to be documented) 6 G (to be documented) 7 B (to be documented) 3 - internal object attribute memory index pointer (64 attributes, 32 bits each, byte granular access). stored value post-increments on access to port 4. 4 - returns object attribute memory location indexed by port 3, then increments port 3. 5 - scroll offset port. 6 - PPU address port to access with port 7. 7 - PPU memory write port. Readable 2C02 registers ----------------------- reg bit desc --- --- ---- 2 5 more than 8 objects on a single scanline have been detected in the last frame 6 a primary object pixel has collided with a playfield pixel in the last frame 7 vblank flag 4 - object attribute memory write port (incrementing port 3 thenafter) 7 - PPU memory read port. Object attribute structure (4*8 bits) ------------------------------------- ofs bit desc --- --- ---- 0 - scanline coordinate minus one of object's top pixel row. 1 - tile index number. Bit 0 here controls pattern table selection when reg 0.5 = 1. 2 0 palette select low bit 1 palette select high bit 5 object priority (> playfield's if 0; < playfield's if 1) 6 apply bit reversal to fetched object pattern table data 7 invert the 3/4-bit (8/16 scanlines/object mode) scanline address used to access an object tile 3 - scanline pixel coordite of most left-hand side of object. +-----------------------+ |Video signal generation| +-----------------------+ A 21.48 MHz clock signal is fed into the 2C02. This is the NES's main clock line, which is shared by the 2A03. Inside the PPU, the 21.48 MHz signal is used to clock a three-stage Johnson counter. The complimentery outputs of both master and slave portions of each stage are used to form 12 mutually exclusive output phases- all 3.58 MHz each (the NTSC colorburst). These 12 different phases form the basis of all color generation for the PPU's composite video output. Naturally, when the user programs the lower 4-bits of a palette register, they are essentially selecting any 1 of 12 phases to be routed to the PPU's video out pin (this corresponds to chrominance (tint/hue) video information) when the appropriate pixel indexes it. Other chrominance combinations (0 & 13) are simply hardwired to a 1 or 0 to generate grayscale pixels. Bits 4 & 5 of a palette entry selects 1 of 4 linear DC voltage offsets to apply to the selected chrominance signal (this corresponds to luminance (brightness) video information) for a pixel. Chrominance values 14 & 15 yield a black pixel color, regardless of any luminance value setting. Luminance value 0, mixed with chrominance value 13 yield a "blacker than black" pixel color. This super black pixel has an output voltage level close to the vertical/horizontal syncronization pulses. Because of this, some video monitors will display warped/distorted screens for games which use this color for black (Game Genie is the best example of this). Essentially what is happening is the video monitor's horizontal timing is compromised by what it thinks are extra syncronization pulses in the scanline. This is not damaging to the monitors which are effected by it, but use of the super black color should be avoided, due to the graphical distortion it causes. The amplitude of the selected chrominance signal (via the 4 lower bits of a palette register) remain constant regardless of bits 4 or 5. Thus it is not possible to adjust the saturation level of a particular color. +---------------+ |PPU base timing| +---------------+ Other than the 3-stage Johnson counter, the 21.48 MHz signal is not used directly by any other PPU hardware. Instead, the signal is divided by 4 to get 5.37 MHz, and is used as the smallest unit of timing in the PPU. All following references to PPU clock cycle (abbr. "cc") timing in this document will be in respect to this timing base, unless otherwise indicated. - Pixels are rendered at the same rate as the base PPU clock. In other words, 1 clock cycle= 1 pixel. - 341 PPU cc's make up the time of a typical scanline (or 341/3 CPU cc's). - One frame consists of 262 scanlines. This equals 341*262 PPU cc's per frame (divide by 3 for # of CPU cc's). +------------------------+ |PPU memory access cycles| +------------------------+ All PPU memory access cycles are 2 clocks long, and can be made back-to-back (typically done during rendering). Here's how the access breaks down: At the beginning of the access cycle, PPU address lines 8..13 are updated with the target address. This data remains here until the next time an access cycle occurs. The lower 8-bits of the PPU address lines are multiplexed with the data bus, to reduce the PPU's pin count. On the first clock cycle of the access, A0..A7 are put on the PPU's data bus, and the ALE (address latch enable) line is activated for the first half of the cycle. This loads the lower 8-bit address into an external 8-bit transparent latch strobed by ALE (74LS373 is used). On the second clock cycle, the /RD (or /WR) line is activated, and stays active for the entire cycle. Appropriate data is driven onto the bus during this time. +----------------------+ |Miscellanious PPU info| +----------------------+ - The internal 25-element palette RAM can be accessed by programming the PPU address port with a range in $3Fxx. Address bit [4] indicates whether the playfield (0) or object (1) palettes should be selected. Address bits [3..2] indicates the palette index (0..3), and bits [1..0] specify the palette element index (1..3). The transparency color palette element can be accessed when address bits 3..0 are all zero. - Reading from $2002 clears the vblank flag (bit 7), and resets the internal $2005/6 flip-flop. Writes here have no effect. - The output of pin /VBL on the 2C02 is the logical NAND between 2002.7 and 2000.7. - $2002.5 and $2002.6 after being set, stay that way for the first 20 scanlines of the new frame, relative to the VINT. - palette RAM is accessed internally during playfield rendering (i.e., the palette address/data is never put on the PPU bus during this time). Additionally, when the programmer accesses palette RAM via $2006/7, the palette address accessed actually does show up on the PPU address bus, but the PPU's /RD & /WR flags are not activated. This is required; to prevent writing over name table data falling under the approprite mirrored area (since the name table RAM's address decoder simply consists of an inverter connected to the A13 line- effectively decoding all addresses in $2000-$3FFF). - Because the PPU cannot make a read from PPU memory immediately upon request (via $2007), there is an internal buffer, which acts as a 1-stage data pipeline. As a read is requested, the contents of the read buffer are returned to the NES's CPU. After this, at the PPU's earliest convience (according to PPU read cycle timings), the PPU will fetch the requested data from the PPU memory, and throw it in the read buffer. Writes to PPU mem via $2007 are pipelined as well, but I currently haven unknown to me if the PPU uses this same buffer (this could be easily tested by writing somthing to $2007, and seeing if the same value is returned immediately after reading). +-----------------------+ |Frame rendering details| +-----------------------+ The following describes the PPU's status during all 262 scanlines of a frame. Any scanlines where work is done (like image rendering), consists of the steps which will be described in the next section. 0..19: Starting at the instant the VINT flag is pulled down (when a NMI is generated), 20 scanlines make up the period of time on the PPU which I like to call the VINT period. During this time, the PPU makes no access to it's external memory (i.e. name / pattern tables, etc.). 20: After 20 scanlines worth of time go by (since the VINT flag was set), the PPU starts to render scanlines. This first scanline is a dummy one; although it will access it's external memory in the same sequence it would for drawing a valid scanline, no on-screen pixels are rendered during this time, making the fetched background data immaterial. Both horizontal *and* vertical scroll counters are updated (presumably) at cc offset 256 in this scanline. Other than that, the operation of this scanline is identical to any other. The primary reason this scanline exists is to start the object render pipeline, since it takes 256 cc's worth of time to determine which objects are in range or not for any particular scanline. 21..260: after rendering 1 dummy scanline, the PPU starts to render the actual data to be displayed on the screen. This is done for 240 scanlines, of course. 261: after the very last rendered scanline finishes, the PPU does nothing for 1 scanline (i.e. the programmer gets screwed out of perfectly good VINT time). When this scanline finishes, the VINT flag is set, and the process of drawing lines starts all over again. +--------------------------+ |Scanline rendering details| +--------------------------+ Naturally, the PPU will fetch data from name, attribute, and pattern tables during a scanline to produce an image on the screen. This section details the PPU's doings during this time. As explained before, external PPU memory can be accessed every 2 cc's. With 341 cc's per scanline, this gives the PPU enough time to make 170 memory accesses per scanline (and it uses all of them!). After the 170th fetch, the PPU does nothing for 1 clock cycle. Remember that a single pixel is rendered every clock cycle. Memory fetch phase 1 thru 128 ----------------------------- 1. Name table byte 2. Attribute table byte 3. Pattern table bitmap #0 4. Pattern table bitmap #1 This process is repeated 32 times (32 tiles in a scanline). This is when the PPU retrieves the appropriate data from PPU memory for rendering the playfield. The first playfield tile fetched here is actually the 3rd to be drawn on the screen (the playfield data for the first 2 tiles to be rendered on this scanline are fetched at the end of the scanline prior to this one). All valid on-screen pixel data arrives at the PPU's video out pin during this time (256 clocks). For determining the precise delay between when a tile's bitmap fetch phase starts (the whole 4 memory fetches), and when the first pixel of that tile's bitmap data hits the video out pin, the formula is (16-n) clock cycles, where n is the fine horizontal scroll offset (0..7 pixels). This information is relivant for understanding the exact timing operation of the "object 0 collision" flag. Note that the PPU fetches an attribute table byte for every 8 sequential horizontal pixels it draws. This essentially limits the PPU's color area (the area of pixels which are forced to use the same 3-color palette) to only 8 horizontally sequential pixels. It is also during this time that the PPU evaluates the "Y coordinate" entries of all 64 objects in object attribute RAM (OAM), to see if the objects are within range (to be drawn on the screen) for the *next* scanline (this is why Y-coordinate entries in the OAM must be programmed to a value 1 less than the scanline the object is to appear on). Each evaluation (presumably) takes 4 clock cycles, for a total of 256 (which is why it's done during on-screen pixel rendering). In-range object evaluation -------------------------- An 8-bit comparator is used to calculate the 9-bit difference between the current scanline (minus 21), and each Y-coordinate (plus 1) of every object entry in the OAM. Objects are considered in range if the comparator produces a difference in the range of 0..7 (if $2000.5 currently = 0), or 0..15 (if $2000.5 currently = 1). (Note that a 9-bit comparison result is generated. This means that setting object scanline coordinates for ranges -1..-15 are actually interpreted as ranges 241..255. For this reason, objects with these ranges will never be considered to be part of any on-screen scanline range, and will not allow smooth object scrolling off the top of the screen.) Tile index (8 bits), X-coordinate (8 bits), & attribute information (4 bits; vertical inversion is excluded) from the in-range OAM element, plus the associated 4-bit result of the range comparison accumulate in a part of the PPU called the "sprite temporary memory". Logical inversion is applied to the loaded 4-bit range comparison result, if the object's vertical inversion attribute bit is set. Since object range evaluations occur sequentially through the OAM (starting from entry 0 to 63), the sprite temporary memory always fills in order from the highest priority in-range object, to lower ones. A 4-bit "in-range" counter is used to determine the number of found objects on the scanline (from 0 up to 8), and serves as an index pointer for placement of found object data into the 8-element sprite temporary memory. The counter is reset at the beginning of the object evaluation phase, and is post-incremented everytime an object is found in-range. This occurs until the counter equals 8, when found object data after this is discarded, and a flag (bit 5 of $2002) is raised, indicating that it is going to be dropping objects for the next scanline. An additional memory bit associated with the sprite temporary memory is used to indicate that the primary object (#0) was found to be in range. This will be used later on to detect primary object-to-playfield pixel collisions. Playfield render pipeline details --------------------------------- As pattern table & palette select data is fetched, it is loaded into internal latches (the palette select data is selected from the fetched byte via a 2-bit 1-of-4 selector). At the start of a new tile fetch phase (every 8 cc's), both latched pattern table bitmaps are loaded into the upper 8-bits of 2- 16-bit shift registers (which both shift right every clock cycle). The palette select data is also transfered into another latch during this time (which feeds the serial inputs of 2 8-bit right shift registers shifted every clock). The pixel data is fed into these extra shift registers in order to implement fine horizontal scrolling, since the periods when the PPU fetch tile data is fixed. A single bit from each shift register is selected, to form the valid 4-bit playfield pixel for the current clock cycle. The bit selection offset is based on the fine horizontal scroll value (this selects bit positions 0..7 for all 4 shift registers). The selected 4-bit pixel data will then be fed into the multiplexer (described later) to be mixed with object data. Memory fetch phase 129 thru 160 ------------------------------- 1. Garbage name table byte 2. Garbage name table byte 3. Pattern table bitmap #0 for applicable object (for next scanline) 4. Pattern table bitmap #1 for applicable object (for next scanline) This process is repeated 8 times. This is the period of time when the PPU retrieves the appropriate pattern table data for the objects to be drawn on the *next* scanline. When less than 8 objects exist on the next scanline (as the in-range object evaluation counter indicates), dummy pattern table fetches take place for the remaining fetches. Internally, the fetched dummy-data is discarded, and replaced with completely transparent bitmap patterns). Although the fetched name table data is thrown away, and the name table address is somewhat unpredictable, the address does seem to relate to the first name table tile to be fetched for the next scanline. This would seem to imply that PPU cc #256 is when the PPU's scroll/address counters have their horizontal scroll values automatically updated. It should also be noted that because this fetch is required for objects on the next scanline, it is neccessary for a garbage scanline to exist prior to the very first scanline to be actually rendered, so that object attribute RAM entries can be evaluated, and the appropriate bitmap data retrieved. As far as the wasted fetch phases here, this is because Nintendo wanted to reuse the playfield pattern table fetch hardware. Details of object pattern fetch & render ---------------------------------------- Where the PPU fetches pattern table data for an individual object is conditioned on the contents of the sprite temporary memory element, and $2000.5. If $2000.5 = 0, the tile index data is used as usual, and $2000.3 selects the pattern table to use. If $2000.5 = 1, the MSB of the range result value become the LSB of the indexed tile, and the LSB of the tile index value determines pattern table selection. The lower 3 bits of the range result value are always used as the fine vertical offset into the selected pattern. Horizontal inversion (bit order reversing) is applied to fetched bitmaps, if indicated in the sprite temporary memory element. The fetched pattern table data (which is 2 bytes), plus the associated 3 attribute bits (palette select & priority), and the x coordinate byte in sprite temporary memory are then loaded into a part of the PPU called the "sprite buffer memory" (the primary object present bit is also copied). This memory area again, is large enough to hold the contents for 8 sprites. The composition of one sprite buffer element here is: 2 8-bit shift registers (the fetched pattern table data is loaded in here, where it will be serialized at the appropriate time), a 3-bit latch (which holds the color & priority data for an object), and an 8-bit down counter (this is where the x coordinate is loaded). The counter is decremented every time the PPU renders a pixel (the first 256 cc's of a scanline; see "Memory fetch phase 1 thru 128" above). When the counter equals 0, the pattern table data in the shift registers will start to serialize (1 shift per clock). Before this time, or 8 clocks after, consider the outputs of the serializers for each stage to be 0 (transparency). The streams of all 8 object serializers are prioritized, and ultimately only one stream (with palette select & priority information) is selected for output to the multiplexer (where object & playfield pixels are prioritized). The data for the first sprite buffer entry (including the primary object present flag) has the first chance to enter the multiplexer, if it's output pixel is non-transparent (non-zero). Otherwise, priority is passed to the next serializer in the sprite buffer memory, and the test for non-transparency is made again (the primary object present status will always be passed to the multiplexer as false in this case). This is done until the last (8th) stage is reached, when the object data is passed through unconditionally. Keep in mind that this whole process occurs every clock cycle (hardware is used to determine priority instantly). Multiplexer operation --------------------- The multiplexer does 2 things: determines primary object collisions, and decides which pixel data to pass through to index the palette RAM- either the playfield's or the object's. Primary object collisions occur when a non-transparent playfield pixel coincides with a non-transparent object pixel, while the primary object present status entering the multiplexer for the current clock cycle is true. This causes a flip-flop ($2002.6) to be set, and remains set until the next frame starts to be rendered again. The decision for selecting the data to pass through to the palette index is made rather easilly. The condition to use object (opposed to playfield) data is: (OBJpri=foreground OR PFpixel=xparent) AND OBJpixel<>xparent Since the PPU has 2 palettes; one for objects, and one for playfield, the appropriate palette will be selected depending on which pixel data is passed through. After the palette look-up, the operation of events follows the aforementioned steps in the "video signal generation" section. Memory fetch phase 161 thru 168 ------------------------------- 1. Name table byte 2. Attribute table byte 3. Pattern table bitmap #0 (for next scanline) 4. Pattern table bitmap #1 (for next scanline) This process is repeated 2 times. It is during this time that the PPU fetches the appliciable playfield data for the first and second tiles to be rendered on the screen for the *next* scanline. These fetches initialize the internal playfield pixel pipelines (2- 16-bit shift registers) with valid bitmap data. The rest of tiles (3..32) are fetched at the beginning of the following scanline. Memory fetch phase 169 thru 170 ------------------------------- 1. Name table byte 2. Name table byte I'm unclear of the reason why this particular access to memory is made. The name table address that is accessed 2 times in a row here, is also the same nametable address that points to the 3rd tile to be rendered on the screen (or basically, the first name table address that will be accessed when the PPU is fetching playfield data on the next scanline). After memory access 170 ----------------------- The PPU simply rests for 1 cycle here (or the equivelant of half a memory access cycle) before repeating the whole pixel/scanline rendering process. +------------------+ |Extra cycle frames| +------------------+ Scanline 20 is the only scanline that has variable length. On every odd frame, this scanline is only 340 cycles (the dead cycle at the end is removed). This is done to cause a shift in the NTSC colorburst phase. You see, a 3.58 MHz signal, the NTSC colorburst, is required to be modulated into a luminance carrying signal in order for color to be generated on an NTSC monitor. Since the PPU's video out consists of basically square waves (as opposed to sine waves, which would be preferred), it takes an entire colorburst cycle (1/3.58 MHz) for an NTSC monitor to identify the color of a PPU pixel accurately. But now you remember that the PPU renders pixels at 5.37 MHz- 1.5x the rate of the colorburst. This means that if a single pixel resides on a scanline with a color different to those surrounding it, the pixel will probably be misrepresented on the screen, sometimes appearing faintly. Well, to somewhat fix this problem, they added this extra pixel into every odd frame (shifting the colorburst phase over a bit), and changing the way the monitor interprets isolated colored pixels each frame. This is why when you play games with detailed background graphics, the background seems to flicker a bit. Once you start scrolling the screen however, it seems as if some pixels become invisible; this is how stationary PPU images would look without this cycle removed from odd frames. Certain scroll rates expose this NTSC PPU color caveat regardless of the toggling phase shift. Some of Zelda 2's dungeon backgrounds are a good place to see this effect. +---------------------------+ |The MMC3's scanline counter| +---------------------------+ As most people know, the MMC3 bases it's scanline counter on PPU address line A13 (which is why IRQ's can be fired off manually by toggling A13 a bunch of times via $2006). What's not common knowledge is the number of times A13 is expected to toggle in a scanline (although if you've been paying close attention to the doc here, you should already know ;) A13 was probably used for the IRQ counter (as opposed to using the PPU's /READ line) because this address line already needed to be connected to the MMC for bankswitching purposes (so in other words, to reduce the MMC3's pin count by 1). They also probably used this method of counting (as opposed to a CPU cycle counter) since A13 cycles (0 -> 1) exactly 42 times per scanline, whereas the CPU count of cycles per scanline is not an exact integer (113.67). Having said that, I guess Nintendo wanted to provide an "easy-to-use" method of generating special image effects, without making programmers have to figure out how many clock cycles to program an IRQ counter with (a pretty lame excuse for not providing an IRQ counter with CPU clock cycle precision (which would have been more useful and versatile)). Regardless of any values PPU registers are programmed with, A13 will operate in a predictable fashion during image rendering (and if you understand how PPU addressing works, you should understand that A13 is the *only* address line with fixed behaviour during image rendering). +------------------------+ |PPU pixel priority quirk| +------------------------+ Object data is prioritized between itself, then prioritized between the playfield. There are some odd side effects to this scheme of rendering, however. For instance, imagine a low priority object pixel with foreground priority, a high priority object pixel with background priority, and a playfield pixel all coinciding (all non-transparent). Ideally, the playfield is considered to be the middle layer between background and foreground priority objects. This means that the playfield pixel should hide the background priority object pixel (regardless of object priority), and the foreground priority object should appear atop the PF pixel. However, because of the way the PPU renders (as just described), OBJ priority is evaluated first, and therefore the background object pixel wins, which means that you'll only be seeing the PF pixel after this mess. A good game to demonstrate this behaviour is Megaman 2. Go into airman's stage. First, jump into the energy bar, just to confirm that megaman's sprite is of a higher priority than the energy bar's. Now, get to the second half of the stage, where the clouds cover the energy bar. The energy bar will be ontop of the clouds, but megaman will be behind them. Now, look what happens when you jump into the energy bar here... you see the clouds where megaman underlaps the energy bar. +----------------------------------------+ |PPU scrolling & addressing in a nutshell| +----------------------------------------+ The upcoming chart is a 2-dimensional matrix representing how address/data entering/leaving the PPU relates to it's internal counters and registers. The top row of the diagram reprents data entering the PPU, and how internal PPU registers are directly effected by it. The left column here describes the means of how the data enters the PPU (either by programming PPU registers, or when the PPU fetches data off the VRAM data bus), and the right column shows how the bits of the written data is mapped to the internal PPU registers (the bits of these registers are then reprogrammed with value of the specified data bit). Numbers 0..7 are used here to represent the data bits written (numbers bits not displayed here means that this data is unused), and "-" is used to indicate that a binary value of 0 is to be written. The middle row and right column of the diagram represents a model of the PPU's internal counters and latches directly related to scrolling/addressing. The top row of the blocks represent the latches/registers. If a bottom row to the blocks exist, these are counters that when loaded, load with the value of the latches directly atop them. The operation of the counters, and when they are loaded, will be described later. The bottom row of the diagram represents how the status of internal PPU registers/counters effect PPU address lines. The description of the columns here are similar to the first row's. However, the digits appearing in the right column now represent the PPU's physical address lines 0..13 (hexidecimal digits are used in the diagram). The absence of address line #'s not appearing here are explained by the notes written next to the access type description in the left column. Finally, address bits that map to PPU registers which have counters below them get their signal only from the counter part of the device, never the latch (top) part. register/counter nomenclature ----------------------------- NT: name table AT: attribute/color table PT: pattern table FV: fine vertical scroll latch/counter FH: fine horizontal scroll latch VT: vertical tile index latch/counter HT: horizontal tile index latch/counter V: vertical name table selection latch/counter H: horizontal name table selection latch/counter S: playfield pattern table selection latch PAR: picture address register (as named in patent document) AR: tile attribute (palette select) value latch /1: first write to 2005 or 2006 since reading 2002 /2: second write to 2005 or 2006 since reading 2002 ͻ 2000 1 0 4 2005/1 76543 210 2005/2 210 76543 2006/1 -54 3 2 10 2006/2 765 43210 NT read 76543210 AT read (4) 10 Ķ ͻͻͻͻͻͻͻͻͻ PPU registers FVVH VT HT FHS PARAR PPU counters ĶĶĶĶĶͼͼͼͼ ͼͼͼͼͼ Ķ 2007 access DC B A 98765 43210 NT read (1) B A 98765 43210 AT read (1,2,4) B A 543c 210b PT read (3) 210 C BA987654 ͼ notes: 1: address lines DC = 10. 2: address lines 9876 = 1111. 3: address line D = 0. address line 3 relates to the pattern table fetch occuring (the PPU always makes them in pairs). 4: The PPU has an internal 4-position, 2-bit shifter, which it uses for obtaining the 2-bit palette select data during an attribute table byte fetch. To represent how this data is shifted in the diagram, letters c..a are used in the diagram to represent the 3-bit right-shift position amount to apply to the data read from the attribute data (a is always 0). This is why you only see bits 0 and 1 used off the read attribute data in the diagram. Counter operation ----------------- During picture rendering, or VRAM access via 2007, the scroll counters (FV, V, H, VT & HT) increment. The fashion in which they increment is determined by the type of VRAM access the PPU is doing. VRAM access via 2007 -------------------- If the VRAM address increment bit (2000.2) is clear (inc. amt. = 1), all the scroll counters are daisy-chained (in the order of HT, VT, H, V, FV) so that the carry out of each counter controls the next counter's clock rate. The result is that all 5 counters function as a single 15-bit one. Any access to 2007 clocks the HT counter here. If the VRAM address increment bit is set (inc. amt. = 32), the only difference is that the HT counter is no longer being clocked, and the VT counter is now being clocked by access to 2007. VRAM access during rendering ---------------------------- Because of how name table data is organized, the counters cannot operate in the same fashion as they do during 2007 access. During the time screen data is to be rendered (when 2001.3 or 2001.4 is 1, and scanline range (relative to VINT) is 20..260), 2 counters are established in the PPU (to fetch name, attribute, and pattern table data), and are clocked as will be described. The first one, the horizontal scroll counter, consists of 6 bits, and is made up by daisy-chaining the HT counter to the H counter. The HT counter is then clocked every 8 pixel dot clocks (or every 8/3 CPU clock cycles). The second counter, the vertical scroll, is 9 bits, and is made up by daisy-chaining FV to VT, and VT to V. FV is clocked by the PPU's horizontal blanking impulse, and therefore will increment every scanline. VT operates here as a divide-by-30 counter, and will only generate a carry condition when the count increments from 29 to 30 (the counter will also reset). Dividing by 30 is neccessary to prevent attribute data in the name tables from being used as tile index data. counter loading/updating ------------------------ There are 2 conditions that update all 5 PPU scroll counters with the contents of the latches adjacent to them. The first is after a write to 2006/2. The second, is at the beginning of scanline 20, when the PPU starts rendering data for the first time in a frame (this update won't happen if all rendering is disabled via 2001.3 and 2001.4). There is one condition that updates the H & HT counters, and that is at the end of the horizontal blanking period of a scanline. Again, image rendering must be occuring for this update to be effective. establishing full split screen scrolls mid-frame ------------------------------------------------ although it is not possible to update FV to any desired value mid-screen exclusively via 2006 (since the MSB is zero'd out from the write), it is possible to mix writes to 2005 & 2006 together, so that it is possible. By resetting the 2005/2006 pointer flip-flop (by reading $2002), writing bytes to the below registers in this sequence, will allow all scroll counters to be updated with ANY desired value, including FV. Note that only relivant updates are mentioned, since data in the scroll latches is overwritten many times in the example below. reg update --- ------ 2006: nametable toggle bits (V, H). 2005: FV & bits 3,4 of VT. 2005: FH. This is effective immediately. 2006: HT & bits 0,1,2 of VT. It is on the last write to 2006 that all values previously written will be loaded into the scroll counters. EOF ================================================ FILE: docs/ppu/ppu_scrolling.txt ================================================ Subject: [nesdev] the skinny on nes scrolling Date: Tue, 13 Apr 1999 16:42:00 -0600 From: loopy Reply-To: nesdev@onelist.com To: nesdev@onelist.com From: loopy --------- the current information on background scrolling is sufficient for most games; however, there are a few that require a more complete understanding. here are the related registers: (v) vram address, a.k.a. 2006 which we all know and love. (16 bits) (t) another temp vram address (16 bits) (you can really call them 15 bits, the last isn't used) (x) tile X offset (3 bits) the ppu uses the vram address for both reading/writing to vram thru 2007, and for fetching nametable data to draw the background. as it's drawing the background, it updates the address to point to the nametable data currently being drawn. bits 0-11 hold the nametable address (-$2000). bits 12-14 are the tile Y offset. --------- stuff that affects register contents: (sorry for the shorthand logic but i think it's easier to see this way) 2000 write: t:0000110000000000=d:00000011 2005 first write: t:0000000000011111=d:11111000 x=d:00000111 2005 second write: t:0000001111100000=d:11111000 t:0111000000000000=d:00000111 2006 first write: t:0011111100000000=d:00111111 t:1100000000000000=0 2006 second write: t:0000000011111111=d:11111111 v=t scanline start (if background and sprites are enabled): v:0000010000011111=t:0000010000011111 frame start (line 0) (if background and sprites are enabled): v=t note! 2005 and 2006 share the toggle that selects between first/second writes. reading 2002 will clear it. note! all of this info agrees with the tests i've run on a real nes. BUT if there's something you don't agree with, please let me know so i can verify it. ________________________________________________________ NetZero - We believe in a FREE Internet. Shouldn't you? Get your FREE Internet Access and Email at http://www.netzero.net/download.html ------------------------------------------------------------------------ New hobbies? New curiosities? New enthusiasms? http://www.ONElist.com Sign up for a new e-mail list today! Subject: [nesdev] Re: the skinny on nes scrolling Date: Tue, 13 Apr 1999 17:48:54 -0600 From: loopy Reply-To: nesdev@onelist.com To: nesdev@onelist.com From: loopy (more notes on ppu logic) you can think of bits 0,1,2,3,4 of the vram address as the "x scroll"(*8) that the ppu increments as it draws. as it wraps from 31 to 0, bit 10 is switched. you should see how this causes horizontal wrapping between name tables (0,1) and (2,3). you can think of bits 5,6,7,8,9 as the "y scroll"(*8). this functions slightly different from the X. it wraps to 0 and bit 11 is switched when it's incremented from _29_ instead of 31. there are some odd side effects from this.. if you manually set the value above 29 (from either 2005 or 2006), the wrapping from 29 obviously won't happen, and attrib data will be used as name table data. the "y scroll" still wraps to 0 from 31, but without switching bit 11. this explains why writing 240+ to 'Y' in 2005 appeared as a negative scroll value. ________________________________________________________ NetZero - We believe in a FREE Internet. Shouldn't you? Get your FREE Internet Access and Email at http://www.netzero.net/download.html ------------------------------------------------------------------------ Looking for a new hobby? Want to make a new friend? http://www.ONElist.com Come join one of the 115,000 e-mail communities at ONElist! ================================================ FILE: docs/ppu/read_buffer_test_readme.txt ================================================ NES PPU Read Buffer Tests ---------------------------------- This mammoth test pack tests many aspects of the NES system, mostly centering around the PPU $2007 read buffer. The test will take about 20 seconds. The program attempts to do as many tests as possible before reporting the result. When the screen is blanked for a long time, audio is used to report progress. A low-pitched fat tone indicates failure; bright beeps indicate progress. If a sub-test fails, at a certain point the list of all failed tests is provided in a numeric form, and a textual explanation of the first failed test is shown. Full list of tests performed is below. Note that the tests are not performed in a numerical order. For example, test #47 (does palette reading work at all) is performed before test #7 (does sequential palette reading work). Test 2 (TEST_PPUMEMORYIO): PPU memory I/O does not work. Possible areas of problem: - PPU not implemented - PPU memory writing ($2007) - PPU memory reading ($2007) - PPU memory area $2C00-$2FFF Test 3 (TEST_ONEBYTEBUFFER): Non-palette PPU memory reads should have one-byte buffer. Test 4 (TEST_CIRAM_READ): CIRAM reading does not work. Test 5 (TEST_CIRAM_SEQ_READ_1): Sequential CIRAM reading with 1-byte increment does not work. Test 6 (TEST_CIRAM_SEQ_READ_32): Sequential CIRAM reading with 32-byte increment does not work. Test 7 (TEST_PALETTE_RAM_SEQ_READ_1): Sequential PALETTE reading with 1-byte increment does not work. Test 8 (TEST_PALETTE_RAM_SEQ_READ_32): Sequential PALETTE reading with 32-byte increment does not work. Test 9 (TEST_CHRROM_READ): CHR-ROM reading does not work. Test 10 (TEST_CHRROM_SEQ_READ_1): Sequential CHR-ROM reading with 1-byte increment does not work. Test 11 (TEST_CHRROM_SEQ_READ_32): Sequential CHR-ROM reading with 32-byte increment does not work. Test 12 (TEST_CIRAM_SEQ_WRITE_1): Sequential CIRAM writes with 1-byte increment does not work. Test 13 (TEST_CIRAM_SEQ_WRITE_32): Sequential CIRAM writes with 32-byte increment does not work. Test 14 (TEST_NTA_MIRRORING_FAIL_1NTA): 1-nametable setup seems to be active, even though this ROM is explicitly configured for horizontal mirroring. Test 15 (TEST_NTA_MIRRORING_FAIL_4NTA): Four-screen setup seems to be active, even though this ROM is explicitly configured for horizontal mirroring. Test 16 (TEST_NTA_MIRRORING_FAIL_VERT): Vertical mirroring seems to be active, even though this ROM is explicitly configured for horizontal mirroring. Test 17 (TEST_PPU_OPEN_BUS): Any data that is transferred through PPU I/O should linger and be readable for a while in any PPU register that does not have a read function. This is called "open bus". To minimally pass this test, you need to at least provide a bridge between $2003(W) and $2000(R). Test 18 (TEST_PPU_OPEN_BUS_SHORTCUT): Reading a write-only PPU register should not just give the current value of SPRADDR. That would be a too lazy workaround for a failed test! Test 19 (TEST_PPU_OPENBUS_MUST_NOT_COPY_READBUFFER): PPU memory read buffer is not the open bus. Reading the bus should repeat the last value that was transferred, not disclose the buffered byte. Test 20 (TEST_PPU_OPENBUS_FROM_WRITE2000_MUST_NOT_WRITETO_READBUFFER): A write to $2000 must not overwrite the $2007 read buffer. Test 21 (TEST_PPU_OPENBUS_FROM_WRITE2001_MUST_NOT_WRITETO_READBUFFER): A write to $2001 must not overwrite the $2007 read buffer. Test 22 (TEST_PPU_OPENBUS_FROM_WRITE2002_MUST_NOT_WRITETO_READBUFFER): A write to $2002 must not overwrite the $2007 read buffer. Test 23 (TEST_PPU_OPENBUS_FROM_WRITE2003_MUST_NOT_WRITETO_READBUFFER): A write to $2003 must not overwrite the $2007 read buffer. Test 24 (TEST_PPU_OPENBUS_FROM_WRITE2004_MUST_NOT_WRITETO_READBUFFER): A write to $2004 must not overwrite the $2007 read buffer. Test 25 (TEST_PPU_OPENBUS_FROM_WRITE2005_MUST_NOT_WRITETO_READBUFFER): A write to $2005 must not overwrite the $2007 read buffer. Test 26 (TEST_PPU_OPENBUS_FROM_WRITE2006_MUST_NOT_WRITETO_READBUFFER): A write to $2006 must not overwrite the $2007 read buffer. Test 27 (TEST_PPU_OPENBUS_FROM_WRITE2007_MUST_NOT_WRITETO_READBUFFER): A write to $2007 must not overwrite the $2007 read buffer. Test 28 (TEST_PPU_OPENBUS_FROM_READ2000_MUST_NOT_WRITETO_READBUFFER): A read from $2000 must not overwrite the $2007 read buffer. Test 29 (TEST_PPU_OPENBUS_FROM_READ2001_MUST_NOT_WRITETO_READBUFFER): A read from $2001 must not overwrite the $2007 read buffer. Test 30 (TEST_PPU_OPENBUS_FROM_READ2002_MUST_NOT_WRITETO_READBUFFER): A read from $2002 must not overwrite the $2007 read buffer. Test 31 (TEST_PPU_OPENBUS_FROM_READ2003_MUST_NOT_WRITETO_READBUFFER): A read from $2003 must not overwrite the $2007 read buffer. Test 32 (TEST_PPU_OPENBUS_FROM_READ2004_MUST_NOT_WRITETO_READBUFFER): A read from $2004 must not overwrite the $2007 read buffer. Test 33 (TEST_PPU_OPENBUS_FROM_READ2005_MUST_NOT_WRITETO_READBUFFER): A read from $2005 must not overwrite the $2007 read buffer. Test 34 (TEST_PPU_OPENBUS_FROM_READ2006_MUST_NOT_WRITETO_READBUFFER): A read from $2006 must not overwrite the $2007 read buffer. Test 35 (TEST_PPU_OPENBUS_INDEXED): STA $2000,Y with Y=7 must issue a dummy read to $2007. Test 36 (TEST_PPU_OPENBUS_INDEXED2): STA $1FF0,Y with Y=$17 mustn't issue a dummy read to $2007. Test 37 (TEST_PPU_OPENBUS_FROM_READ_MIRROR_MUST_WRITETO_READBUFFER): A read from a mirrored copy of $2007 must act as if $2007 was read, and update the same read buffer. Test 38 (TEST_PPU_READ_WITH_AND): The AND instruction must be usable for reading $2007 or any other I/O port. Test 39 (TEST_PPU_READ_WITH_ORA): The ORA instruction must be usable for reading $2007 or any other I/O port. Test 40 (TEST_PPU_READ_WITH_EOR): The EOR instruction must be usable for reading $2007 or any other I/O port. Test 41 (TEST_PPU_READ_WITH_CMP): The CMP instruction must be usable for reading $2007 or any other I/O port. Test 42 (TEST_PPU_READ_WITH_CPX): The CPX instruction must be usable for reading $2007 or any other I/O port. Test 43 (TEST_PPU_READ_WITH_CPY): The CPY instruction must be usable for reading $2007 or any other I/O port. Test 44 (TEST_PPU_READ_WITH_ADC): The ADC instruction must be usable for reading $2007 or any other I/O port. Test 45 (TEST_PPU_READ_WITH_SBC): The SBC instruction must be usable for reading $2007 or any other I/O port. Test 46 (TEST_ONEBYTEBUFFER_PALETTE): Palette reads from PPU should not have one-byte buffer. Test 47 (TEST_PALETTE_READS): Palette reads from PPU do not seem to be working at all. Test 48 (TEST_PALETTE_READS_UNRELIABLE): Palette reads from PPU seem to work randomly. Test 49 (TEST_PALETTE_MIRRORS): Palette indexes $3F1x should be mirrors of $3F0x when x is 0, 4, 8, or C. Test 50 (TEST_PALETTE_UNIQUE): It must be possible to store unique data in each of $3F00, $3F04, $3F08 and $3F0C. Test 51 (TEST_PPU_PALETTE_WRAP): PPU addresses 3F00-3F1F should be mirrored within the whole 3F00-3FFF region, for a total of 8 times. Test 52 (TEST_PPU_MEMORY_14BIT_A): Failed sub-test 1 of: The two MSB within the PPU memory address should be completely ignored in all circumstances, effectively mirroring the 0000-3FFF address range within the whole 0000-FFFF region, for a total of 4 times. Test 53 (TEST_PPU_MEMORY_14BIT_B): Failed sub-test 2 of: The two MSB within the PPU memory address should be completely ignored in all circumstances, effectively mirroring the 0000-3FFF address range within the whole 0000-FFFF region, for a total of 4 times. Test 54 (TEST_PPU_MIRROR_3000): PPU memory range 3000-3EFE should be a mirror of the PPU memory range 2000-2EFE. Test 55 (TEST_PPU_READ_3EFF): Setting PPU address to 3EFF and reading $2007 twice should give the data at $3F00, not the data at $2EFF. Test 56 (TEST_PPU_MIRROR_2F): Reading PPU memory range 3Fxx should put contents of 2Fxx into the read buffer. Test 57 (TEST_PPU_SEQ_READ_WRAP): Setting PPU address to 3FFF & reading $2007 thrice should give the contents of $0000. Test 58 (SEQ_READ_INTERNAL): Unexpected: VROM contents at $0000 and $1FFF read the same. This should never happen in this test ROM. Test 59 (TEST_VADDR): Relationship between $2005 and $2006 is not implemented properly. Here is a guide. It explains which registers use which parts of the address. Note that only the second write to $2006 updates the address really used by $2007. FEDCBA9876543210ZYX: bit pos. ^^^^^^^^^^^^^^------ =$2007 zz543210-------------- $2006#1 76543210------ $2006#2 76543210--- $2005#1 210--76543----------- $2005#2 10---------------- $2000 Test 60 (TEST_RAM_MIRRORING): CPU RAM at 0000-07FF should be mirrored 4 times, in the following address ranges: - 0000-07FF - 0800-0FFF - 1000-17FF - 1800-1FFF Test 61 (TEST_PPUIO_MIRRORING): PPU I/O memory at 2000-2007 should be mirrored within the whole 2000-3FFF region, for a total of 1024 times. Test 62 (TEST_SPHIT_AND_VBLANK): Sprite 0 hit flag should not read as set during vblank. Test 63 (TEST_SPHIT_DIRECT): Sprite 0 hit test by poking data directly into $2003-4 ^ Possible causes for failure: - $2003/$2004 not implemented - No sprite 0 hit tests - Way too long vblank period Test 64 (TEST_SPHIT_DIRECT_READBUFFER): Sending 5 bytes of data into $2003 and $2004 must not overwrite the $2007 read buffer. Test 65 (TEST_SPHIT_DMA_ROM): Sprite 0 hit test using DMA ($4014) using ROM as source ^ Possible causes for failure: - $4014 DMA cannot read from anything other than RAM Test 66 (TEST_SPHIT_DMA_READBUFFER): Invoking a $4014 DMA with a non-$20 value must not overwrite the $2007 read buffer. Test 67 (TEST_SPHIT_DMA_PPU_BUS): Sprite 0 hit test using DMA ($4014) using PPU I/O bus as source ^ In this test, $4014 <- #$20. Possible causes for failure: - DMA does not do proper reads - PPU bus does not preserve last transferred values - $2002 read returned a value that differs from expected - $2004 read modifies the OAM Test 68 (TEST_DMA_PPU_SIDEEFFECT): Writing $20 into $4014 should generate 32 reads into $2007 as a side-effect, each time incrementing the PPU read address. Test 69 (TEST_SPHIT_DMA_RAM): Sprite 0 hit test using DMA. All internal RAM pages are tested, including mirrored addresses. Failing the test may imply faulty mirroring. Test 70 (TEST_CHRROM_READ_BANKED): CHR ROM read through $2007 does not honor mapper 3 (CNROM) bank switching Test 71 (TEST_CHRROM_READ_BANKED_BUFFER): The $2007 read buffer should not retroactively react to changes in VROM mapping. When you read $2007, the data is stored in a buffer ("latch"), and the previous content of the buffer is returned. It is not a delayed read request. Test 72 (TEST_CHRROM_WRITE): CHR ROM on mapper 3 (CNROM boards) must not be writable. Test 73 (TEST_BUFFER_DELAY_BLANK_1FRAME): The PPU read buffer should survive 1 frame of idle with rendering disabled. Test 74 (TEST_BUFFER_DELAY_BLANK_2SECONDS): The PPU read buffer should survive 2 seconds of idle with rendering disabled. Test 75 (TEST_BUFFER_DELAY_INTERNAL): Unexpected: VROM contents at $1Bxx did not match what was hardcoded into the program. Test 76 (TEST_BUFFER_DELAY_VISIBLE_1FRAME): The PPU read buffer should survive 1 frame of idle with rendering enabled. Test 77 (TEST_BUFFER_DELAY_VISIBLE_1SECOND): The PPU read buffer should survive 1 second of idle with rendering enabled. Test 78 (TEST_BUFFER_DELAY_VISIBLE_3SECONDS): The PPU read buffer should survive 3 seconds of idle with rendering enabled. Test 79 (TEST_BUFFER_DELAY_VISIBLE_7SECONDS): The PPU read buffer should survive 7 seconds of idle with rendering enabled. Expected output: TEST:test_ppu_read_buffer :) ------------------------------- Testing basic PPU memory I/O. Performing tests that combine sprite 0 hit flag, $4014 DMA and the RAM mirroring... Graphical artifacts during this test are OK and expected. Hit No-Hit Direct poke OK OK DMA with ROM OK OK DMA + PPU bus OK OK DMA with RAM OK OK ------------------------------- This next test will take a while. In order to distract you with entertainment, art is provided. Contemplate on the art while the test is in progress. Passed The ":)" should be blue/purple; the "OK" should be brownish orange, and the "Graphical artifacts" paragraph should also be brownish orange. Everything else should be white. In the painting by Thomas Kinkade that is shown before the "Passed" text appears, the ground should be pleasantly green. The text outputted to the $6000 console is slightly different than the reference shown above, because parts of the text above are placed on the screen directly, and for other reasons. Because this ROM contains a large amount of text and some graphics data, portions of the ROM had to be compressed to avoid increasing the ROM size too much. Should one want to rebuild the ROM, a particular set of tools will be needed; including nasm, gcc, and php. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The text output may include ANSI color codes, which take the form of an esc character ($1B), an opening bracket ('['), and a sequence of numbers and semicolon characters, terminated by a non-digit character ('m'). The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. -- Joel Yliluoma Shay Green ================================================ FILE: docs/ppu/sprite_hit_readme.txt ================================================ 01-basics --------- Tests basic sprite 0 hit behavior (nothing timing related). 2) Flag isn't working at all 3) Should hit even when completely behind background 4) Should miss when background rendering is off 5) Should miss when sprite rendering is off 6) Should miss when all rendering is off 7) All-transparent sprite should miss 8) Only low two palette index bits are relevant 9) Any non-zero palette index should hit with any other 10) Should miss when background is all transparent 11) Should always miss other sprites 02-alignment ------------ Tests alignment of sprite hit with background. Places a solid background tile in the middle of the screen and places the sprite on all four edges both overlapping and non-overlapping. 2) Basic sprite-background alignment is way off 3) Sprite should miss left side of bg tile 4) Sprite should hit left side of bg tile 5) Sprite should miss right side of bg tile 6) Sprite should hit right side of bg tile 7) Sprite should miss top of bg tile 8) Sprite should hit top of bg tile 9) Sprite should miss bottom of bg tile 10) Sprite should hit bottom of bg tile 03-corners ---------- Tests sprite 0 hit using a sprite with a single pixel set, for each of the four corners. 2) Lower-right pixel should hit 3) Lower-left pixel should hit 4) Upper-right pixel should hit 5) Upper-left pixel should hit 04-flip ------- Tests sprite 0 hit for single pixel sprite and background. 2) Horizontal flipping doesn't work 3) Vertical flipping doesn't work 4) Horizontal + Vertical flipping doesn't work 05-left_clip ------------ Tests sprite 0 hit with regard to clipping of left 8 pixels of screen. 2) Should miss when entirely in left-edge clipping 3) Left-edge clipping occurs when $2001 is not $1E 4) Left-edge clipping is off when $2001 = $1E 5) Left-edge clipping should block hits only when X = 0 6) Should miss; sprite pixel covered by left-edge clip 7) Should hit; sprite pixel outside left-edge clip 8) Should hit; sprite pixel outside left-edge clip 06-right_edge ------------- Tests sprite 0 hit with regard to column 255 (ignored) and off right edge of screen. 2) Should always miss when X = 255 3) Should hit; sprite has pixels < 255 4) Should miss; sprite pixel is at 255 5) Should hit; sprite pixel is at 254 6) Should also hit; sprite pixel is at 254 07-screen_bottom ---------------- Tests sprite 0 hit with regard to bottom of screen. 2) Should always miss when Y >= 239 3) Can hit when Y < 239 4) Should always miss when Y = 255 5) Should hit; sprite pixel is at 238 6) Should miss; sprite pixel is at 239 7) Should hit; sprite pixel is at 238 08-double_height ---------------- Tests basic sprite 0 hit double-height operation. 2) Lower sprite tile should miss bottom of bg tile 3) Lower sprite tile should hit bottom of bg tile 3) Lower sprite tile should miss top of bg tile 4) Lower sprite tile should hit top of bg tile 09-timing --------- Tests sprite 0 hit timing to PPU clock accuracy. 2) PPU VBL timing is wrong 3) Flag set too soon for upper-left corner 4) Flag set too late for upper-left corner 5) Flag set too soon for upper-right corner 6) Flag set too late for upper-right corner 7) Flag set too soon for lower-left corner 8) Flag set too late for lower-left corner 9) Flag cleared too soon at end of VBL 10) Flag cleared too late at end of VBL 10-timing_order --------------- Ensures that hit time is based on position on screen, and unaffected by which pixel of sprite is being hit (upper-left, lower-right, etc.) 2) Upper-left corner 3) Upper-right corner 4) Lower-left corner 5) Lower-right corner 6) Hit time shouldn't be based on pixels under left clip 7) Hit time shouldn't be based on pixels at X=255 8) Hit time shouldn't be based on pixels off right edge Multi-tests ----------- The NES/NSF builds in the main directory consist of multiple sub-tests. When run, they list the subtests as they are run. The final result code refers to the first sub-test that failed. For more information about any failed subtests, run them individually from rom_singles/ and nsf_singles/. Multi-tests ----------- The NES/NSF builds in the main directory consist of multiple sub-tests. When run, they list the subtests as they are run. The final result code refers to the first sub-test that failed. For more information about any failed subtests, run them individually from rom_singles/ and nsf_singles/. Multi-tests ----------- The NES/NSF builds in the main directory consist of multiple sub-tests. When run, they list the subtests as they are run. The final result code refers to the first sub-test that failed. For more information about any failed subtests, run them individually from rom_singles/ and nsf_singles/. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. -- Shay Green ================================================ FILE: docs/ppu/sprite_overflow_readme.txt ================================================ 01-basics --------- Tests basic operation of sprite overflow flag (bit 5 of $2002). 2) Should set flag when 9 sprites are on scanline 3) Reading $2002 shouldn't clear flag 4) Shouldn't clear flag at beginning of VBL 5) Should clear flag at end of VBL 6) Shouldn't set flag when $2001=$00 7) Should set normally when $2001=$08 8) Should set normally when $2001=$10 02-details ---------- Tests details of sprite overflow flag 2) Should set flag even when sprites are under left clip 3) Disabling rendering shouldn't clear flag 4) Should clear flag at the end of VBL even when $2001=0 5) Should set flag even when sprite Y coordinates are 239 6) Shouldn't set flag when sprite Y coordinates are 240 (off screen) 7) Shouldn't set flag when sprite Y coordinates are 255 (off screen) 8) Should set flag regardless of which sprites are involved 9) Shouldn't set flag when all scanlines have 7 or fewer sprites 10) Double-height sprites aren't handled properly 03-timing --------- Tests sprite overflow flag timing to PPU clock accuracy 2) PPU VBL timing is wrong 3) PPU VBL timing is wrong 3) Flag cleared too early at end of VBL 4) Flag cleared too late at end of VBL 5) Flag set too early for first scanline 6) Flag set too late for first scanline 7) Horizontal positions shouldn't affect timing 8) Set too early for last sprites on first scanline 9) Set too late for last sprites on first scanline 10) Set too early for last scanline 11) Set too late for last scanline 12) Set too early when 9th sprite # is way after 8th 13) Set too late when 9th sprite # is way after 8th 14) Overflow on second scanline occurs too early 15) Overflow on second scanline occurs too late 04-obscure ---------- Tests the pathological behavior when 8 sprites are on a scanline and the one just after the 8th is not on the scanline. After that, the PPU interprets different bytes of each following sprite as its Y coordinate. 1 2 3 4 5 6 7 8 9 10 11 12 13 14: If 1-8 are on the same scanline, 9 isn't, then the second byte of 10, the third byte of 11, fourth byte of 12, first byte of 13, second byte of 14, etc. are treated as those sprites' Y coordinates for the purpose of setting the overflow flag. This search continues until all sprites have been scanned or one of the (erroneously interpreted) Y coordinates places the sprite within the scanline. 2) Checks that second byte of sprite #10 is treated as its Y 3) Checks that third byte of sprite #11 is treated as its Y 4) Checks that fourth byte of sprite #12 is treated as its Y 5) Checks that first byte of sprite #13 is treated as its Y 6) Checks that second byte of sprite #14 is treated as its Y 7) Checks that search stops at the last (64th) sprite 8) Same as test #2 but using a different range of sprites 05-emulator ----------- Tests things that an optimized emulator is likely get wrong 2) Didn't set flag when $2002 wasn't read during frame 3) Disabling rendering didn't recalculate flag time 4) Changing sprite RAM didn't recalculate flag time 5) Changing sprite height didn't recalculate time Multi-tests ----------- The NES/NSF builds in the main directory consist of multiple sub-tests. When run, they list the subtests as they are run. The final result code refers to the first sub-test that failed. For more information about any failed subtests, run them individually from rom_singles/ and nsf_singles/. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. -- Shay Green ================================================ FILE: docs/ppu/tv_readme.txt ================================================ TV pass or fail? This program is designed for NES and tests various aspects of the display it is connected to. Press the A Button to switch screens. _____________________________________________________________________ NTSC chroma/luma crosstalk The PPU in the PlayChoice arcade system generates RGB video, with red, green, and blue color information on separate cables. The PPU in the NES generates composite video, with chroma (color) and luma (brightness) information carried on one cable at different frequency bands. To keep the circuit cheap, it does not perform proper filtering to keep the chroma from bleeding into the luma. This especially has an effect on 45 degree diagonal lines. But an accurate emulator must preserve the same artifacts, as games such as Blaster Master rely on them to create the richest color palette. This screen displays something noticeably different on an NTSC NES PPU vs. the RGB PPU that most PC based NES emulators emulate. Display on RGB system: Display on NTSC system: ,---------. ,---------. | ===== | | ===== | |%%%%%%%%%| | PASS! | | | | | | PRESS A | | PRESS A | `---------' `---------' _____________________________________________________________________ Pixel aspect ratio PC displays most commonly generate square pixels. A square pixel on an NTSC display is 7/12 of a chroma cycle wide, but the NES PPU did not generate square pixels. Instead, it generated pixels 8/12 of a chroma cycle wide, which are somewhat wider than they are tall. This made games' graphics appear stretched. If they are displayed with square pixels on a PC based emulator, graphics will not appear with the intended proportions. This screen shows three rectangles. One is a square on NTSC NES and PlayChoice, one is a square on PAL NES, and one is a square with square pixels. _____________________________________________________________________ Legal Copyright 2007 Damian Yerrick Do not distribute this quick and dirty preview version to the public until it has been tested on an NES. ================================================ FILE: docs/ppu/vbl_nmi_readme.txt ================================================ NES PPU Tests ------------- These tests verify the behavior and timing of the NTSC PPU's VBL flag, NMI enable, and NMI interrupt. Timing is tested to an accuracy of one PPU clock. Note that often the NES starts up with a different value in the clock divider, causing PPU timing to be slightly different and fail some of the tests. These test the timings that have been most fully documented and emulated. 01-vbl_basics ------------- Tests basic VBL operation and VBL period. 2) VBL period is way off 3) Reading VBL flag should clear it 4) Writing $2002 shouldn't affect VBL flag 5) $2002 should be mirrored at $200A 6) $2002 should be mirrored every 8 bytes up to $2FFA 7) VBL period is too short with BG off 8) VBL period is too long with BG off 02-vbl_set_time --------------- Verifies time VBL flag is set. Reads $2002 twice and prints VBL flags from them. Test is run one PPU clock later each time, around the time the flag is set. 00 - V 01 - V 02 - V 03 - V ; after some resets this is - - 04 - - ; flag setting is suppressed 05 V - 06 V - 07 V - 08 V - 03-vbl_clear_time ----------------- Tests time VBL flag is cleared. Reads $2002 and prints VBL flag. Test is run one PPU clock later each line, around the time the flag is cleared. 00 V 01 V 02 V 03 V 04 V 05 V 06 - 07 - 08 - 04-nmi_control -------------- Tests immediate NMI behavior when enabling while VBL flag is already set 2) Shouldn't occur when disabled 3) Should occur when enabled and VBL begins 4) $2000 should be mirrored every 8 bytes 5) Should occur immediately if enabled while VBL flag is set 6) Shouldn't occur if enabled while VBL flag is clear 7) Shouldn't occur again if writing $80 when already enabled 8) Shouldn't occur again if writing $80 when already enabled 2 9) Should occur again if enabling after disabled 10) Should occur again if enabling after disabled 2 11) Immediate occurence should be after NEXT instruction 05-nmi_timing ------------- Tests NMI timing. Prints which instruction NMI occurred after. Test is run one PPU clock later each line. 00 4 01 4 02 4 03 3 04 3 05 3 06 3 07 3 08 3 09 2 06-suppression -------------- Tests behavior when $2002 is read near time VBL flag is set. Reads $2002 one PPU clock later each time. Prints whether VBL flag read back as set, and whether NMI occurred. 00 - N 01 - N 02 - N 03 - N ; normal behavior 04 - - ; flag never set, no NMI 05 V - ; flag read back as set, but no NMI 06 V - 07 V N ; normal behavior 08 V N 09 V N 07-nmi_on_timing ---------------- Tests NMI occurrence when enabled near time VBL flag is cleared. Enables NMI one PPU clock later on each line. Prints whether NMI occurred. 00 N 01 N 02 N 03 N 04 N 05 - 06 - 07 - 08 - 08-nmi_off_timing ----------------- Tests NMI occurrence when disabled near time VBL flag is set. Disables NMI one PPU clock later on each line. Prints whether NMI occurred. 03 - 04 - 05 - 06 - 07 N 08 N 09 N 0A N 0B N 0C N 09-even_odd_frames ------------------ Tests clock skipped on every other PPU frame when BG rendering is enabled. Tries pattern of BG enabled/disabled during a sequence of 5 frames, then finds how many clocks were skipped. Prints number skipped clocks to help find problems. Correct output: 00 01 01 02 10-even_odd_timing ------------------ Tests timing of skipped clock every other frame when BG is enabled. Output: 08 08 09 07 2) Clock is skipped too soon, relative to enabling BG 3) Clock is skipped too late, relative to enabling BG 4) Clock is skipped too soon, relative to disabling BG 5) Clock is skipped too late, relative to disabling BG Multi-tests ----------- The NES/NSF builds in the main directory consist of multiple sub-tests. When run, they list the subtests as they are run. The final result code refers to the first sub-test that failed. For more information about any failed subtests, run them individually from rom_singles/ and nsf_singles/. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. -- Shay Green ================================================ FILE: docs/ppu/vbl_nmi_timing_readme.txt ================================================ NTSC NES PPU VBL/NMI Timing Tests --------------------------------- These ROMs test the timing of the VBL flag and NMI to an accuracy of a single PPU clock, and also check special cases. They have been tested on an actual NES and all give a passing result. Sometimes the NES starts up with a different PPU timing that causes some of the tests to fail; these tests don't check that timing arrangement. Each ROM runs several tests and reports the result on screen and by beeping a number of times. See below for the meaning of failure codes for each test. It's best to run the tests in order, because later ROMs depend on things tested by earlier ROMs and will give erroneous results if any earlier ones failed. Source code for each test is included, and most tests are clearly divided into sections. Support code is also included, but it runs on a custom devcart and assembler so it will require some effort to assemble. Contact me if you'd like assistance porting them to your setup. 1.frame_basics -------------- Tests basic VBL flag operation and general timing of PPU frames. 2) VBL flag isn't being set 3) VBL flag should be cleared after being read 4) PPU frame with BG enabled is too short 5) PPU frame with BG enabled is too long 6) PPU frame with BG disabled is too short 7) PPU frame with BG disabled is too long 2.vbl_timing ------------ Tests timing of VBL being set, and special case where reading VBL flag as it would be set causes it to not be set for that frame. 2) Flag should read as clear 3 PPU clocks before VBL 3) Flag should read as set 0 PPU clocks after VBL 4) Flag should read as clear 2 PPU clocks before VBL 5) Flag should read as set 1 PPU clock after VBL 6) Flag should read as clear 1 PPU clock before VBL 7) Flag should read as set 2 PPU clocks after VBL 8) Reading 1 PPU clock before VBL should suppress setting 3.even_odd_frames ----------------- Test clock skipped when BG is enabled on odd PPU frames. Tests enable/disable BG during 5 consecutive frames, then see how many clocks were skipped. Patterns are shown as XXXXX, where each X can either be B (BG enabled) or - (BG disabled). 2) Pattern ----- should not skip any clocks 3) Pattern BB--- should skip 1 clock 4) Pattern B--B- (one even, one odd) should skip 1 clock 5) Pattern -B--B (one odd, one even) should skip 1 clock 6) Pattern BB-BB (two pairs) should skip 2 clocks 4.vbl_clear_timing ------------------ Tests timing of VBL flag clearing. 2) Cleared 3 or more PPU clocks too early 3) Cleared 2 PPU clocks too early 4) Cleared 1 PPU clock too early 5) Cleared 3 or more PPU clocks too late 6) Cleared 2 PPU clocks too late 7) Cleared 1 PPU clock too late 5.nmi_suppression ----------------- Tests timing of NMI suppression when reading VBL flag just as it's set, and that this doesn't occur when reading one clock before or after. 2) Reading flag 3 PPU clocks before set shouldn't suppress NMI 3) Reading flag when it's set should suppress NMI 4) Reading flag 3 PPU clocks after set shouldn't suppress NMI 5) Reading flag 2 PPU clocks before set shouldn't suppress NMI 6) Reading flag 1 PPU clock after set should suppress NMI 7) Reading flag 4 PPU clocks after set shouldn't suppress NMI 8) Reading flag 4 PPU clocks before set shouldn't suppress NMI 9) Reading flag 1 PPU clock before set should suppress NMI 10)Reading flag 2 PPU clocks after set shouldn't suppress NMI 6.nmi_disable ------------- Tests NMI occurrence when disabling NMI just as VBL flag is set, and just after. 2) NMI shouldn't occur when disabled 0 PPU clocks after VBL 3) NMI should occur when disabled 3 PPU clocks after VBL 4) NMI shouldn't occur when disabled 1 PPU clock after VBL 5) NMI should occur when disabled 4 PPU clocks after VBL 6) NMI shouldn't occur when disabled 1 PPU clock before VBL 7) NMI should occur when disabled 2 PPU clocks after VBL 7.nmi_timing ------------ Tests timing of NMI and immediate occurrence when enabled with VBL flag already set. 2) NMI occurred 3 or more PPU clocks too early 3) NMI occurred 2 PPU clocks too early 4) NMI occurred 1 PPU clock too early 5) NMI occurred 3 or more PPU clocks too late 6) NMI occurred 2 PPU clocks too late 7) NMI occurred 1 PPU clock too late 8) NMI should occur if enabled when VBL already set 9) NMI enabled when VBL already set should delay 1 instruction 10)NMI should be possible multiple times in VBL -- Shay Green (swap to e-mail) ================================================ FILE: release-plz.toml ================================================ [workspace] allow_dirty = false changelog_config = "cliff.toml" changelog_update = true dependencies_update = true git_release_enable = true git_tag_enable = true pr_labels = ["release"] publish_allow_dirty = false semver_check = true [[package]] name = "tetanes" git_release_name = "TetaNES v{{ version }}" [[package]] name = "tetanes-core" git_release_name = "TetaNES Core v{{ version }}" ================================================ FILE: rust-toolchain.toml ================================================ [toolchain] channel = "nightly" components = ["rustfmt", "clippy", "llvm-tools-preview"] targets = ["wasm32-unknown-unknown"] profile = "default" ================================================ FILE: tetanes/CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.14.1](https://github.com/lukexor/tetanes/compare/0.13.0..0.14.1) - 2026-04-20 ### 🐛 Bug Fixes - Fixed missing debug cfg guards - ([2b64734](https://github.com/lukexor/tetanes/commit/2b647342356cd6ae48b38e28427af47ef62e2186)) ### 🎨 Styling - Fixed formatting - ([d7e37de](https://github.com/lukexor/tetanes/commit/d7e37de56a47b475a01b259b9988e1574e75b33e)) - Fixed nightly lints - ([801bb3c](https://github.com/lukexor/tetanes/commit/801bb3c95a95a7f98efb5a993f2efe7061daf00b)) ### ⚙️ Miscellaneous Tasks - Update packages - ([439af37](https://github.com/lukexor/tetanes/commit/439af377f93c0fb8af1353d1802da6ab5b8c407d)) ## [0.13.0](https://github.com/lukexor/tetanes/compare/0.12.2..0.13.0) - 2026-02-14 ### 🐛 Bug Fixes - Change cycle to u32 to improve 32-bit platforms like wasm - ([86f597c](https://github.com/lukexor/tetanes/commit/86f597c3c1422040e8f98ac4dfb29f3a2ffdc81f)) - Add hours and minutes to run time - ([4b5f6e6](https://github.com/lukexor/tetanes/commit/4b5f6e6fcec20e053e2ca736e1e4eec09d35803f)) - Fixed closing secondary windows - ([81b0e81](https://github.com/lukexor/tetanes/commit/81b0e81bae07d6dd6b459fbbb79550aa45c1e7b1)) - Fixed linux link spelling and small html loader issue - ([637385d](https://github.com/lukexor/tetanes/commit/637385d7ccbb157204bd3d90a311abc8d438a60e)) ### ⚡ Performance - Cpu opcode refactor - ([8916097](https://github.com/lukexor/tetanes/commit/8916097c39ee18828d9fd7bf70185a19eaf8e098)) ### 🎨 Styling - Fix nightly lints - ([40cd6e4](https://github.com/lukexor/tetanes/commit/40cd6e4520a1915f4b18537e14b160d11c4eda36)) ### ⚙️ Miscellaneous Tasks - Updated deps - ([ccb7463](https://github.com/lukexor/tetanes/commit/ccb7463c98be1e35fe7e6a47c2df9d76796ee349)) - Update deps - ([782dc21](https://github.com/lukexor/tetanes/commit/782dc213f25f34cc47af0c2f71cdf9bfd44ae28b)) - Try to shore up CD builds/uploads - ([96d3193](https://github.com/lukexor/tetanes/commit/96d3193aebefded818cff7bf6bd00bade1341a7f)) ## [0.12.2](https://github.com/lukexor/tetanes/compare/0.12.1..0.12.2) - 2025-04-05 ### ⛰️ Features - Recent rom enhancements. closes #391 - ([d70ca9b](https://github.com/lukexor/tetanes/commit/d70ca9bee97444d72af2442ceaf750b6cc03fb4e)) - Add don't show again checkbox to update window. closes #390 - ([5cb1186](https://github.com/lukexor/tetanes/commit/5cb1186a0cd1d0016197f21bf89ad7d44339a94e)) ### 🐛 Bug Fixes - Fixed ctrlc handling - ([131aa18](https://github.com/lukexor/tetanes/commit/131aa1879dd9d5594a66a4f2ce054bfa7674969d)) - Revert input serialization change, as it broke run-ahead - ([6489c57](https://github.com/lukexor/tetanes/commit/6489c579c738f88068423affb3833edd9105e523)) - Fix controls in index - ([567137b](https://github.com/lukexor/tetanes/commit/567137b5418525ec51dd63a50b23230b93a9f952)) - Removed extra touch scaling - ([44f41db](https://github.com/lukexor/tetanes/commit/44f41db0e83fcca49cd472115b5def9417aa6fc4)) - Removed download button for Mobile - ([035362a](https://github.com/lukexor/tetanes/commit/035362a21571782865c3fc4c7609d9f027a52a05)) - Fix touch events - ([1a8d3da](https://github.com/lukexor/tetanes/commit/1a8d3dad5243bb31858575e99e6961c33a2521d4)) - Fixed some configuration/repaint issues - ([8eb07da](https://github.com/lukexor/tetanes/commit/8eb07da1dbc53c7c2cebfe7b206dbd17ee98b1f5)) - Basic cpu error recovery options - ([c60d8d1](https://github.com/lukexor/tetanes/commit/c60d8d1d02dde9e3383849e1af43111f26db29c7)) - Remove color emojis from window titles. closes #410 - ([b149648](https://github.com/lukexor/tetanes/commit/b149648649c875df3303f6208e7de31995e2143b)) - Fix ctrl-c handling while file dialog is open - ([cb006f2](https://github.com/lukexor/tetanes/commit/cb006f2846e302e7498749fc8f079c7bce7b5739)) - Show warning if no audio device on startup. closes #388 - ([b0625e5](https://github.com/lukexor/tetanes/commit/b0625e5ccac22c0114bf19981549c9da1b5f2cab)) - Handle ctrl-c. closes #386 - ([817ee58](https://github.com/lukexor/tetanes/commit/817ee5843a1b8da8676555a52ef2a9a8489477ec)) ### 🚜 Refactor - Changed input serializing - ([ad37b47](https://github.com/lukexor/tetanes/commit/ad37b47ab07a54597eeb91b95fb524a0ea6b4e43)) - Cleaned up Memory struct - ([7cb31fb](https://github.com/lukexor/tetanes/commit/7cb31fb3878ee4e7e9b4ca9eabe123d570cc3176)) ### 📚 Documentation - Fix urls - ([8136c42](https://github.com/lukexor/tetanes/commit/8136c42bdab474e17ceda78819225349a3f1c520)) - Ensure features display in docs.rs - ([f58b31c](https://github.com/lukexor/tetanes/commit/f58b31cc9d27f0187d759796c64e34ccea3f784a)) - Remove custom docs.rs metadata - ([201b3bc](https://github.com/lukexor/tetanes/commit/201b3bc4f2ccddbb7b737fced5fa8ed67fac5d25)) ### ⚡ Performance - Re-enable puffin_egui with patched versions - ([24ee0cc](https://github.com/lukexor/tetanes/commit/24ee0cc6717442fdebead0f075994ab35cb382da)) ### 🎨 Styling - Updated loading for tetanes-web - ([02b7a24](https://github.com/lukexor/tetanes/commit/02b7a2439afb379a577408f586a313fac11a4c36)) - Some small cleanup - ([4757228](https://github.com/lukexor/tetanes/commit/4757228455fc1e9093ce1a6436fe48e8156df918)) - Warn if surface format isn't supported, add test gamma png - ([9a03731](https://github.com/lukexor/tetanes/commit/9a0373154d8527c5051e59e9748d2d6249b91288)) ### ⚙️ Miscellaneous Tasks - Updated dependencies - ([6683613](https://github.com/lukexor/tetanes/commit/66836130cbd3c2409ed25b2af7292ddec8efa032)) - Reduce dependencies - ([b16fcd3](https://github.com/lukexor/tetanes/commit/b16fcd353f7bc3233030c49243ddf1f65c80778e)) - Zip windows installer - ([ccfb7f4](https://github.com/lukexor/tetanes/commit/ccfb7f42184aabda574c783e46b82e9cfb4dd6e6)) - Updated deps - ([828b770](https://github.com/lukexor/tetanes/commit/828b770071cd9a5e54967573abef9d31888017bc)) - Add versions to artifacts - ([a29241f](https://github.com/lukexor/tetanes/commit/a29241f2508972bc998519ec0bdf11041e5c22ba)) ## [0.12.1](https://github.com/lukexor/tetanes/compare/0.12.0..0.12.1) - 2025-03-13 ### ⛰️ Features - Added shortcuts for shaders and ppu warmup flag - ([408b122](https://github.com/lukexor/tetanes/commit/408b122ed98f7edb7a26085fb921aa006bde7091)) ### 📚 Documentation - Fixed cargo doc url - ([782f7c5](https://github.com/lukexor/tetanes/commit/782f7c51b68c5fb52b483a3f151bd3db227286a9)) - Updated changelog and readmes - ([a4a3e8c](https://github.com/lukexor/tetanes/commit/a4a3e8c0775a7261b91f4238756ac5a20d2c4b48)) ### ⚙️ Miscellaneous Tasks - Fix/update ci, docs, and fixed nightly issue with tetanes-core - ([a6150ba](https://github.com/lukexor/tetanes/commit/a6150bad6703bbc661d7d5c8b63f5a6d47991868)) # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.12.0](https://github.com/lukexor/tetanes/compare/tetanes-v0.11.0..tetanes-v0.12.0) - 2025-03-12 ### ⛰️ Features - Jalecoss88006 - ([406777a](https://github.com/lukexor/tetanes/commit/406777abad8d61490aae2a33e2e71fc617db3f55)) - Namco163 - ([89d7fb4](https://github.com/lukexor/tetanes/commit/89d7fb4617bf844ad2090cd92f0e92cda9cc91fc)) - Added sunsoft/fme-7 - ([303dad8](https://github.com/lukexor/tetanes/commit/303dad85d0a6586a88b7067f2070c5ac9e4da6e4)) - Added nina003/nina006 - ([29503c3](https://github.com/lukexor/tetanes/commit/29503c3efc81fb3110eef63e9666de1e0c912015)) - Added dxrom ([#340](https://github.com/lukexor/tetanes/issues/340)) - ([906af59](https://github.com/lukexor/tetanes/commit/906af59038e95874dda254e02998030c913d8c61)) - Ppu-viewer ([#339](https://github.com/lukexor/tetanes/issues/339)) - ([fce7d89](https://github.com/lukexor/tetanes/commit/fce7d89f78148e9a367d47122eef7e6e8fe45b34)) - Bandai mappers 016, 153, 157, 159 ([#335](https://github.com/lukexor/tetanes/issues/335)) - ([f555ea4](https://github.com/lukexor/tetanes/commit/f555ea48d0273bc9d41b998926d451398acbb73c)) - Allow exporting save states in web ([#311](https://github.com/lukexor/tetanes/issues/311)) - ([627bbec](https://github.com/lukexor/tetanes/commit/627bbece49739ff479e69ba9e83df828c4d4a633)) - Add a debug build label - ([46b3d94](https://github.com/lukexor/tetanes/commit/46b3d94e5fd24900a95554a295257f0891ac1c53)) - Add test panic debug button - ([3866efa](https://github.com/lukexor/tetanes/commit/3866efab39ced8f3431ee27d24962a55397a9f07)) - Added screen reader/accesskit support - ([5fd1a73](https://github.com/lukexor/tetanes/commit/5fd1a73f112f74a6c0a81e722485842dd37e0a38)) - Added ui setting/debug windows - ([db8b122](https://github.com/lukexor/tetanes/commit/db8b122af6c5a52ad23ed89ffd6f2feb35515603)) - Enable webgpu for browsers that support it. closes #297 ([#298](https://github.com/lukexor/tetanes/issues/298)) - ([a6bde61](https://github.com/lukexor/tetanes/commit/a6bde619454bf8d77f98d462d89eccea4b0e42fc)) ### 🐛 Bug Fixes - Fixed several issues - ([60fcd90](https://github.com/lukexor/tetanes/commit/60fcd90e740833e94deb98896a17a51fcda38998)) - Fix cycle overflow - ([a4e1f05](https://github.com/lukexor/tetanes/commit/a4e1f058c6e899e9fd11578bfb40a36d6ea1980e)) - Add temporary webgpu flag - ([179e868](https://github.com/lukexor/tetanes/commit/179e868c9e1cee92df1d0568b60403c2df7579cb)) - Temporary wasm fix for check-cfg - ([30c6a61](https://github.com/lukexor/tetanes/commit/30c6a61c0d562f875a0d979ad655e199d7c7019a)) - Fix tetanes-core compiling on stable. closes #360 - ([adc5673](https://github.com/lukexor/tetanes/commit/adc5673a3ed5d80aff339c3ab6d95013fcb2d715)) - Fixed deny.toml - ([2c1f186](https://github.com/lukexor/tetanes/commit/2c1f18603f043c2dcb17db8fa8958ea1cbfd88d4)) - Fixed bank size check - ([c84c012](https://github.com/lukexor/tetanes/commit/c84c012c310ad466c5167b94f0228f5a482dec43)) - Fixed wasm - ([bd27814](https://github.com/lukexor/tetanes/commit/bd278140bcc7e7d433917f14e99038f2e6453027)) - Fixed video frame size - ([153094d](https://github.com/lukexor/tetanes/commit/153094d81d444376b112224409544375588c4f97)) - Fix scroll issues - ([218d786](https://github.com/lukexor/tetanes/commit/218d7860421eb4cfc4d7b833132f4c476935777a)) - Fixed increasing scale on web - ([8c4265e](https://github.com/lukexor/tetanes/commit/8c4265e10fc8b62cd7dcaa8a828fed1a07100a9f)) - Fixed shortcut text - ([cb73c21](https://github.com/lukexor/tetanes/commit/cb73c216936ad49dca4e2595485df4ccea957eaa)) - Fixed joypad keybinds and some UI styling - ([bc2f093](https://github.com/lukexor/tetanes/commit/bc2f093b4d02c54744f791f336a102424a7e5af1)) - Enable puffin on wasm - ([0b6f794](https://github.com/lukexor/tetanes/commit/0b6f79429c5d2a642c0ef6301bbcc9818973a234)) - Fix window theme - ([e3c42c7](https://github.com/lukexor/tetanes/commit/e3c42c7720f558c7348e2b82b3573d4748158850)) - Fixed window aspect ratio - ([17db5c8](https://github.com/lukexor/tetanes/commit/17db5c8a037ab3aefab560bca67545964069658f)) - Don't log/error when sending frames while paused - ([50825f8](https://github.com/lukexor/tetanes/commit/50825f82e9f04418fdefd56707ef2ec50cddd5ed)) - Fixed pause state when loading replay - ([d743b31](https://github.com/lukexor/tetanes/commit/d743b31c190cd93e42e3ab78b497e59bcc4ade88)) - Fixed roms path to default to current directory, if valid, and canonicalize - ([e00273f](https://github.com/lukexor/tetanes/commit/e00273f740f7fc095bc02c7ce6d0ba132a14c9bc)) - Ensure pixel brightness is using the same palette - ([ad2f873](https://github.com/lukexor/tetanes/commit/ad2f873f5652016b96317c000b4abbe0e35de421)) - Move some calculations to vertex shader that don't depend on v_uv - ([a6f262d](https://github.com/lukexor/tetanes/commit/a6f262db5d83950e86e0ec78bb74fc63e5c2bf85)) - Fixed logging location - ([ff36033](https://github.com/lukexor/tetanes/commit/ff36033d7bbbf64924d97d6e9a88dcf4db7dc60c)) - Fixed issue with lower end platforms not supporting larger texture dimensions - ([ef214db](https://github.com/lukexor/tetanes/commit/ef214dbc2f2eee016b7abdb0c2b0ee1858381ee4)) - Fix window resizing while handling zoom changes - ([6b3f690](https://github.com/lukexor/tetanes/commit/6b3f690b8ec21b907d353a7cad8561217e8d9dcf)) ### 🚜 Refactor - [**breaking**] Split mapper traits - ([3e4a372](https://github.com/lukexor/tetanes/commit/3e4a372dfdc4295851c93cca96044f84645ae14e)) - Removed egui-wgpu and egui-winit dependencies. ([#315](https://github.com/lukexor/tetanes/issues/315)) - ([b3d4e2c](https://github.com/lukexor/tetanes/commit/b3d4e2c70c6ee4cfa9aaf53a11c1ae802610ff99)) - Platform/ui cleanup - ([39f66e6](https://github.com/lukexor/tetanes/commit/39f66e6e912f9c95cf9c458cd072e5e041af09e3)) - Moved around platform code to condense it - ([0f18928](https://github.com/lukexor/tetanes/commit/0f18928b8f8ed031cac7a170557c0296916c99bc)) - Prefer deferred viewports ([#306](https://github.com/lukexor/tetanes/issues/306)) - ([e1e60d1](https://github.com/lukexor/tetanes/commit/e1e60d19599ab883cbb034047519e6eb831d6c6c)) ### 📚 Documentation - Extra cpu comments - ([80f3366](https://github.com/lukexor/tetanes/commit/80f3366e3fab1257201ab0d9af673c4318edabef)) ### ⚡ Performance - Restore sprite presence check, ~2% gain - ([c6d353a](https://github.com/lukexor/tetanes/commit/c6d353a8fc12b506656a8cd70561ef1830ba9284)) - More perf and added flamegraph - ([31edf0c](https://github.com/lukexor/tetanes/commit/31edf0c63bcc30867f0049a231e7d366db4bde8d)) - Performance tweaks - ([d9a3019](https://github.com/lukexor/tetanes/commit/d9a3019ec0c0014d8850158d38c27289dc885020)) ### 🎨 Styling - Fix lints - ([bc9f6bc](https://github.com/lukexor/tetanes/commit/bc9f6bc293d413cf780a2aa0253ad7d64951d193)) - Slight cleanup - ([63e31a9](https://github.com/lukexor/tetanes/commit/63e31a9755266bec88d5c79e064506999f03aea2)) - Fixed format - ([d62ea28](https://github.com/lukexor/tetanes/commit/d62ea285cb5fe73ac41e7364f0ca3f32281a0e88)) ### ⚙️ Miscellaneous Tasks - Update deps - ([5b077c0](https://github.com/lukexor/tetanes/commit/5b077c01b1e68a60d3e295fe108732a3b8abbbd6)) - Bumped version - ([28fa93f](https://github.com/lukexor/tetanes/commit/28fa93f226447fd409b5d3846cd0f7e14a793f83)) - Update deps - ([509dbd4](https://github.com/lukexor/tetanes/commit/509dbd48a34cd6a360da0fba3786ed73445381fc)) - Fix ci - ([da64229](https://github.com/lukexor/tetanes/commit/da64229966295d85b0f62b0e3827d76767116602)) - Fix deny.toml - ([64a2401](https://github.com/lukexor/tetanes/commit/64a24010c72926c555ae74ffb4f1acb2c0aefffb)) - Updated deps - ([906c877](https://github.com/lukexor/tetanes/commit/906c877700d551fd74e0545e03f544ea2255823f)) - Updated deps - ([825719e](https://github.com/lukexor/tetanes/commit/825719e7f56ef6263f22a6da82d31f02d05af570)) - Updated deps - ([4712d6d](https://github.com/lukexor/tetanes/commit/4712d6d6de3ce7eccec8f1971fcb0f2411f91e3d)) - Restore nightly ci - ([eb2a2c5](https://github.com/lukexor/tetanes/commit/eb2a2c58ecd802810709f5e367253857d51a47d0)) - Update dependencies - ([4947a8c](https://github.com/lukexor/tetanes/commit/4947a8cf6883eda0b0c55fcd7bcf98cf8fd7dee9)) - Remove puffin_egui reference in wasm - ([16845f3](https://github.com/lukexor/tetanes/commit/16845f39e28c816c847a9d403dbedde38c815c1d)) - More dependency cleanup - ([1971e4f](https://github.com/lukexor/tetanes/commit/1971e4f2c5aaf6f8a2d6ce2a03c978362d44afe1)) - Clean up dependencies - ([254fe54](https://github.com/lukexor/tetanes/commit/254fe543293b0c96c78ce25bdaeef2f250a9fb14)) - Remove auto-assign from triage - ([9a2804b](https://github.com/lukexor/tetanes/commit/9a2804b94b1a412214159495d2e6410a63555572)) - Restrict homebrew cd to .rb files - ([3c1e390](https://github.com/lukexor/tetanes/commit/3c1e3907d7477dbe9f6953d9b8b9b0aeb1ef5966)) - Fix update homebrew formula runs-on - ([9e66a07](https://github.com/lukexor/tetanes/commit/9e66a073fa1ef9e276a2ca85ccc4e4281b50e7bc)) - Fix cd upload - ([892d184](https://github.com/lukexor/tetanes/commit/892d184cc25ca7903cb4a5f7372f47e722866125)) - Restore RELEASE_PLZ_TOKEN - ([18de294](https://github.com/lukexor/tetanes/commit/18de2946b82a44efdba96e5918eba381ad3a1a75)) - Remove need for RELEASE_PLZ_TOKEN - ([b6c8478](https://github.com/lukexor/tetanes/commit/b6c84780123ca5d9dfc841e2a3e6266b7d3cc4b9)) - Try to fix release cd - ([c7d5f51](https://github.com/lukexor/tetanes/commit/c7d5f514a84bd3b728686893e3211b63ec21a9c9)) ## [0.11.0](https://github.com/lukexor/tetanes/compare/tetanes-v0.10.0..tetanes-v0.11.0) - 2024-06-12 ### ⛰️ Features - Shader support with crt-easymode ([#285](https://github.com/lukexor/tetanes/pull/285)) - ([e5042ef](https://github.com/lukexor/tetanes/commit/e5042efd45642ac2a13d7ac695bba1cce77c69c9)) - Auto-save cfg at a set interval ([#279](https://github.com/lukexor/tetanes/pull/279)) - ([e6941d8](https://github.com/lukexor/tetanes/commit/e6941d8e47c73910cf99c5ae9d52e9d5f4b7bade)) - Add UI persistence. closes [#257](https://github.com/lukexor/tetanes/pull/257) ([#277](https://github.com/lukexor/tetanes/pull/277)) - ([4c861f7](https://github.com/lukexor/tetanes/commit/4c861f7f59d99ee536e135a238ae3b621178bd08)) - Added config and save/sram state persistence to web ([#274](https://github.com/lukexor/tetanes/pull/274)) - ([8c7f6df](https://github.com/lukexor/tetanes/commit/8c7f6df4a8894b544da1c6480659ee26ea28f342)) - Added always on top option. enabled shortcut for embed viewports - ([489f61e](https://github.com/lukexor/tetanes/commit/489f61ef668094fa8e685a592cad0ec8b46d1c38)) - Added data man demo and changed name of nebs n' debs to demo - ([d7d2bae](https://github.com/lukexor/tetanes/commit/d7d2bae10da45239cf3e8142a00e893dd95fdc23)) - Added mapper 11 - ([03d2074](https://github.com/lukexor/tetanes/commit/03d2074d3d58fcf652fecb9d77f4e96e8c007aae)) ### 🐛 Bug Fixes - Fixed a number of issues caused by the crt-shader PR - ([8c31927](https://github.com/lukexor/tetanes/commit/8c31927ee332d0593402b7c6b632dbcafe4fa964)) - Ntsc tweaks - ([3042fa7](https://github.com/lukexor/tetanes/commit/3042fa7b928faf69e10040b4eb981a4c4f8f3ce3)) - Fixed some frame clocking issues - ([80ef7b5](https://github.com/lukexor/tetanes/commit/80ef7b50df3ea00500df70f86904d0a3cfcfdb53)) - Fixed blocking checking for updates on start - ([f48c634](https://github.com/lukexor/tetanes/commit/f48c63445bf2f2be224c7632781101c9a4075dbe)) - Revert rfd features back - ([30cec26](https://github.com/lukexor/tetanes/commit/30cec26fd44d284e124e33422d2cbcccd5d0814a)) - Fixed Data Man url - ([882004a](https://github.com/lukexor/tetanes/commit/882004ac9f44aa0c51ca88bf99552f9187fad8e9)) - Cleaned up pausing, parking, and control flow. Closes [#251](https://github.com/lukexor/tetanes/pull/251) - ([72cf88a](https://github.com/lukexor/tetanes/commit/72cf88ac6991953222bd3dd1d395f7f9035c98ef)) - Remove unfocused/occluded pausing for now until a less error-prone cross-platform solution can be designed - ([a5549e6](https://github.com/lukexor/tetanes/commit/a5549e6f026d201e9e6c7f0acfc56ee734c85a95)) - Remove bold from controls - ([0cfa0e9](https://github.com/lukexor/tetanes/commit/0cfa0e9edaad86b7566763ef8444649bef462af1)) - Fix excess redraw requests - ([caf88c0](https://github.com/lukexor/tetanes/commit/caf88c002fb8f18072e5fad95c967dd79f09afff)) - Fixed wasm resizing to be restricted by browser viewport ([#243](https://github.com/lukexor/tetanes/pull/243)) - ([b59d4c9](https://github.com/lukexor/tetanes/commit/b59d4c906fbd41d95e21e58bffc28074028947c4)) - Disable rewind when low on memory. clear rewind memory when disabled - ([4d5e1c4](https://github.com/lukexor/tetanes/commit/4d5e1c4dbe43cceb9ab8d4c33ca832830b2d31d8)) - Remove redrawing every clock - ([8cea6c1](https://github.com/lukexor/tetanes/commit/8cea6c14718dfd564b1f8ec9e60fc57b2d0602d0)) - Fixed web build relative urls - ([1423bdb](https://github.com/lukexor/tetanes/commit/1423bdb75766da2d656d24a6075ee38d36002ec9)) - Fixed a number of issues with loading roms and unintentionally blocking wasm - ([e257575](https://github.com/lukexor/tetanes/commit/e257575c24cde809d156ae6451575ec7cfd70aad)) - Fix clock timing on web. closes [#234](https://github.com/lukexor/tetanes/pull/234) - ([57d323d](https://github.com/lukexor/tetanes/commit/57d323d44408269b1c72932baa4b3b534e69f70d)) - Fix frame stats when toggled via menu. closes [#233](https://github.com/lukexor/tetanes/pull/233) - ([347066b](https://github.com/lukexor/tetanes/commit/347066b8f4000dcd88e3a76e8b948b55198ecceb)) - Add scrolling to lists - ([62ff074](https://github.com/lukexor/tetanes/commit/62ff0745a846cc8aed910244448becd98f155abf)) - Fix changing slider/drag values - ([8580135](https://github.com/lukexor/tetanes/commit/8580135f6e1e14a84214ca24efd57dbaf7595997)) ### 🚜 Refactor - Removed a number of panic cases and cleaned up platform checks - ([bdb71a9](https://github.com/lukexor/tetanes/commit/bdb71a96792778cb0ad6bedf44e0ef5cbfa703e4)) - Frame timing cleanup - ([1e920fd](https://github.com/lukexor/tetanes/commit/1e920fdd4ca56010ebe380152c817025a1b4c127)) - Some initialization error handling cleanup - ([507d9a0](https://github.com/lukexor/tetanes/commit/507d9a04c439081afc585761311082b0069c7111)) - Small gui cleanup - ([880e9ee](https://github.com/lukexor/tetanes/commit/880e9ee4b33d8b12924598e688f01b5e79289590)) ### 📚 Documentation - Fixed docs and changelog - ([4c7a694](https://github.com/lukexor/tetanes/commit/4c7a6949e52b6734fd6a78f6d9567c70e12b3ae4)) ### ⚙️ Miscellaneous Tasks - Split out web build so it can run on any platform - ([dcaec14](https://github.com/lukexor/tetanes/commit/dcaec14828288d758c713861a7aef6c03e4da47b)) - Upgrade ringbuf - ([5d7abe2](https://github.com/lukexor/tetanes/commit/5d7abe291493f43b14a0239157dfed930db17d6c)) ## [0.10.0](https://github.com/lukexor/tetanes/compare/tetanes-v0.9.0..tetanes-v0.10.0) - 2024-05-16 ### ⛰️ Features - *(mapper)* Added Vrc6 mapper - ([fd2075d](https://github.com/lukexor/tetanes/commit/fd2075d98c7b4ef8643e5ea433c936201e414a04)) - Added controller support - ([7550bce](https://github.com/lukexor/tetanes/commit/7550bce09738cbfc5360c08e8323e70e68d0eb54)) - Initial re-structure of painter and viewports - ([5feabbe](https://github.com/lukexor/tetanes/commit/5feabbe197e59a9999960b40c2f069f7527eb421)) - Perf stats and ui cleanup - ([8d7d0d4](https://github.com/lukexor/tetanes/commit/8d7d0d482958d63d3b2c853dc01bf81f7bf54b84)) - Switched to lazy APU catch-up - ([4a95de3](https://github.com/lukexor/tetanes/commit/4a95de3e1eb364ac13e189b321d4cbe480e67da3)) - Add headless options, run_ahead methods, audio fixes, and performance improvements - ([a1a1b9b](https://github.com/lukexor/tetanes/commit/a1a1b9bae19530dfc4592a33cfb318f8d5e0df75)) - Added run-ahead feature - ([3349045](https://github.com/lukexor/tetanes/commit/3349045adce0ecdf9c38f0ccd41ddcbd72c9f7a8)) - Add cycle-accurate feature - ([6d0db9f](https://github.com/lukexor/tetanes/commit/6d0db9f1c0a5b1b7b83ab3c29b055b2addbad40f)) - Added rewind - ([4cc7b65](https://github.com/lukexor/tetanes/commit/4cc7b657b085666b5500c0f0c6f2eabcb7cdabd2)) ### 🐛 Bug Fixes - *(ppu)* Fixed oam read stomping on sprite0's y-byte - ([dc51191](https://github.com/lukexor/tetanes/commit/dc511914a14d5ccf2071022981a3126b9ff47b40)) - *(wasm)* Overhauled wasm build - ([2892587](https://github.com/lukexor/tetanes/commit/28925874661988423acab72be73b6ffb405924ee)) - Revert frame buffer back to u16 to fix emphasis - ([bc7f5fa](https://github.com/lukexor/tetanes/commit/bc7f5fa741a93877087e3b6f71c7ecfdacc06637)) - Made ppu warm optional, default to false - ([1693681](https://github.com/lukexor/tetanes/commit/16936817a5bd1278c835eedf6fa63d410f047c2a)) - Revert 240pee rename - ([f82f763](https://github.com/lukexor/tetanes/commit/f82f76397ea7e00afe171235e776207ed8b10c2b)) - Fixed 240pee test rom path - ([77f9702](https://github.com/lukexor/tetanes/commit/77f9702f0c2ff6caa073cdbbcf4816614daf8675)) - Fixed saving config - ([361447f](https://github.com/lukexor/tetanes/commit/361447fc1bc178498a23c2464c3f0fe353e5c165)) - Fixed setting APU sample_rate - ([ed52eb7](https://github.com/lukexor/tetanes/commit/ed52eb78d3b2cfb921622c19760d87fe72b1189e)) - Fixed selecting audio sample rate - ([24d6bfc](https://github.com/lukexor/tetanes/commit/24d6bfc78d5ce2021fcea7edbf53a9f2af3f74e9)) - Disabled webgpu since it panics on a double borrow currently - ([c93e7ad](https://github.com/lukexor/tetanes/commit/c93e7adbd2dd5ad3ba8c7fef5f60f874ccf839da)) - Remove toggling vsync and fixed wasm frame rate - ([f937396](https://github.com/lukexor/tetanes/commit/f9373964e5f91c5dc75b7bbd93c456f7253eae8d)) - Some timing and ui fixes - ([fe9d123](https://github.com/lukexor/tetanes/commit/fe9d1232df8294445d60db6ccc448f71df489d44)) - Fixed exrom irq - ([7cc2540](https://github.com/lukexor/tetanes/commit/7cc254034f40c76af6a5c444895afc34e1509e46)) - Removed unused revision - ([4376c9a](https://github.com/lukexor/tetanes/commit/4376c9a1396186dbfcd6a70f8c5d77940eb3f0b8)) - Cleaned up mappers - ([22c97e2](https://github.com/lukexor/tetanes/commit/22c97e228c8afb521be25c7a45eab5bb8b8e83cd)) - Documentation and ui updates - ([38fc88f](https://github.com/lukexor/tetanes/commit/38fc88f873bf2b5b27f0d82bbff82895747581ae)) - Fixed apu tests - ([6ff1362](https://github.com/lukexor/tetanes/commit/6ff1362a080143632a8384207c82adf9f9d244dc)) - More audio fixes - ([7e0f2f5](https://github.com/lukexor/tetanes/commit/7e0f2f5de552ef0b99a07f08e5bd47deced44df4)) - Fixed mmc5 pulse channels - ([0588103](https://github.com/lukexor/tetanes/commit/05881036c965e8ffb9231f0fd0a1738298e9b869)) - Some fixes for audio channels sounding off - ([46a773d](https://github.com/lukexor/tetanes/commit/46a773d81d86a60c6dded568bb1525fcd868468d)) - Fixed apu region sample_period - ([2a440ca](https://github.com/lukexor/tetanes/commit/2a440caa9d1cb43f9097b0a5c5e60a9ab746fa81)) - Fixed some frame rate performance issues - ([ca304b0](https://github.com/lukexor/tetanes/commit/ca304b05dc762478be54a2d365249bb4e05a8b68)) - Fixed replay and rewind - ([55dc8d7](https://github.com/lukexor/tetanes/commit/55dc8d7e06c3ec9366936d02599739119a8cc6f1)) - Fixed some path and config issues - ([ef60f1b](https://github.com/lukexor/tetanes/commit/ef60f1b9bd3f996e8bcb7435ed9da21f827cd327)) - Fixed some region, configs, and features - ([e5d4f4a](https://github.com/lukexor/tetanes/commit/e5d4f4a21cb96bf0051b8827150f76f1b3a579bc)) - Fixed chr_ram test - ([d24009b](https://github.com/lukexor/tetanes/commit/d24009bab6575a1c74cbd138745f57ad7a2c5cd7)) - Fixed apu linear counter loading - ([76ae795](https://github.com/lukexor/tetanes/commit/76ae7958df948e174cc3d9c1fe1f8ce5d3d7279a)) - Read nes2.0 region header - ([0c70e87](https://github.com/lukexor/tetanes/commit/0c70e87a4edc002e018d01598584c6bab1d0a2ae)) - Fixed chr-rom writing - ([50724a6](https://github.com/lukexor/tetanes/commit/50724a6a485cac0c4d87d941931ef2129cab6d4b)) - Improved PAL support - ([acb4db8](https://github.com/lukexor/tetanes/commit/acb4db8cc79af64c594a0990fd5d80043b2bb5cd)) ### 🚜 Refactor - *(cpu)* Moved DMA values inside CPU - ([7257d18](https://github.com/lukexor/tetanes/commit/7257d18ed2154787e8ad25fa1ea309d5442098c4)) - Various event/UI cleanup - ([bd1f984](https://github.com/lukexor/tetanes/commit/bd1f984ffbc41ca0ff73a80a000670c25d0f3361)) - Config overhaul and keybind menus - ([1fbb4ba](https://github.com/lukexor/tetanes/commit/1fbb4bafc66190b7d406434e12cf0666c3453bac)) - Thread local irq - ([cc9cbea](https://github.com/lukexor/tetanes/commit/cc9cbea644acf745ed5d99e2f80a92271c8d8d63)) - Various cleanup - ([889e41f](https://github.com/lukexor/tetanes/commit/889e41fb91f4db51f2269be7ce51d290fba48cca)) - Some audio cleanup - ([6eeff9e](https://github.com/lukexor/tetanes/commit/6eeff9e33a606869f46b29212dd06da18be26ab7)) - Major config overhaul - ([34076be](https://github.com/lukexor/tetanes/commit/34076be0266d3c3838630b57aa705371e9460c2d)) - Major platform and error handling overhaul - ([eb6e546](https://github.com/lukexor/tetanes/commit/eb6e5468ccb9a1d711658d55e6b3f0f84c154ddb)) - Audio mixer overhaul and .raw recording - ([44cc47c](https://github.com/lukexor/tetanes/commit/44cc47c932afb0ba136458fb962cf6ac39e865ac)) - Fixed audio - ([fe26c1b](https://github.com/lukexor/tetanes/commit/fe26c1bbb240ba447cd9487b5f0fb9adead6c2be)) - Clean up some wasm code - ([dd489d3](https://github.com/lukexor/tetanes/commit/dd489d38eca98e180534fe03b4920cc3b5fa4fa3)) - Inlined puffin for now - ([945ff0e](https://github.com/lukexor/tetanes/commit/945ff0e750727717dffab39528e321bf6dc8bc2e)) - Moved audio filtering/decimation to apu - ([4a38d23](https://github.com/lukexor/tetanes/commit/4a38d23df45594def155d1e11ed8869230195580)) - Major module overhaul - ([ca92f51](https://github.com/lukexor/tetanes/commit/ca92f5176855a635cb1244b912030d9c9859f7cd)) - Various updates - ([da213ae](https://github.com/lukexor/tetanes/commit/da213ae36aa0b4763c643d089e390d184d69dc19)) - Cleaned up menus - ([dd9e726](https://github.com/lukexor/tetanes/commit/dd9e726e8e3c9616aa09ff2cebf726462fe96f87)) - Made ram_state consistent - ([81d3bc9](https://github.com/lukexor/tetanes/commit/81d3bc9849c2dd30bca22933c5478b7c787259c7)) ### 📚 Documentation - Updated docs - ([806c078](https://github.com/lukexor/tetanes/commit/806c0789b65644d5a4e7d0365026fcf011d8770b)) - Updated readme - ([16db37d](https://github.com/lukexor/tetanes/commit/16db37d3fc8e1a5507e613e498bf239098e691e4)) - Fixed docs - ([3de4078](https://github.com/lukexor/tetanes/commit/3de40789ba4df94711d5dbf9361857479147e73b)) - Added temporary readme - ([bf0d5db](https://github.com/lukexor/tetanes/commit/bf0d5db99ec0baff09c0e4bdc2c2500d1025080f)) - Fixed README - ([6d48eec](https://github.com/lukexor/tetanes/commit/6d48eec5bb8fb4bfadea5f836776db1f64bbd04a)) - Fix README for real - ([a5fd672](https://github.com/lukexor/tetanes/commit/a5fd672c176c7c6ce52ae75cce656c18c62a221a)) - Fix README - ([05abf3c](https://github.com/lukexor/tetanes/commit/05abf3c89c7003559885c3b40b9d36dd591a9b2b)) - Updated README - ([ffb7b21](https://github.com/lukexor/tetanes/commit/ffb7b2129642d872b968b8017fdd7149e7e71d1d)) - Updated README roadmap - ([0941d86](https://github.com/lukexor/tetanes/commit/0941d863d6bb1a4d5b2347132127c4b350dcebbb)) ### ⚡ Performance - Improved cpu usage - ([e17a901](https://github.com/lukexor/tetanes/commit/e17a901c1a3bc0ce37ff416b19a2ec484234b87f)) ### 🧪 Testing - Fixed test - ([4beb787](https://github.com/lukexor/tetanes/commit/4beb78713c5a71eb3fb31fd293abee535b702184)) - Fixed tests - ([531683d](https://github.com/lukexor/tetanes/commit/531683d24e32ec5ebc17ba54420204c4411b531c)) - Moved tests to tetanes-core - ([3d105ff](https://github.com/lukexor/tetanes/commit/3d105ff5fcc28503844ed6187cf9560361a3f90f)) ### ⚙️ Miscellaneous Tasks - Added linux builds - ([3f9c244](https://github.com/lukexor/tetanes/commit/3f9c244c3b87817d679a91ad9e596bcad79ad498)) - Updated ci for release - ([da465d7](https://github.com/lukexor/tetanes/commit/da465d7f535e02b8526026ca4e443e2a23d58da4)) - Moved lints and added const fns - ([cfe5678](https://github.com/lukexor/tetanes/commit/cfe5678f541a125a742a01fdc157333fb60b3e7e)) - Updated deps and msrv - ([f66b75e](https://github.com/lukexor/tetanes/commit/f66b75ea2be1846c5aa70a4a94616134ebcaabe2)) - Updated deps and msrv - ([01d5f68](https://github.com/lukexor/tetanes/commit/01d5f6877b561c1bc3d5ea1e3b8fed294ee71439)) - Updated tetanes-web dependencies - ([3fe0dba](https://github.com/lukexor/tetanes/commit/3fe0dbaf59d508cddffe1b197e811d3089d22170)) - Updated ci - ([f34a125](https://github.com/lukexor/tetanes/commit/f34a125b98c164566905d9962368fff5bfb346a8)) - Increase MSRV - ([684e771](https://github.com/lukexor/tetanes/commit/684e771a488f1cb88541b62265556b0fc79664a8)) - Refactor workflows - ([41f4bc5](https://github.com/lukexor/tetanes/commit/41f4bc51bdc163704c3b481f72413c77d51a4a59)) - Update Cargo.lock - ([98168f8](https://github.com/lukexor/tetanes/commit/98168f8ef734a7fc5a5ad3592fb76fe07b4d9a62)) - Update license - ([7e84f7f](https://github.com/lukexor/tetanes/commit/7e84f7f38e54d216f5a97fac9af4d9690ed5ea40)) - Add readme badges - ([60c185c](https://github.com/lukexor/tetanes/commit/60c185c61dec39407eee8af5c069ffc456daec52)) ## [0.9.0](https://github.com/lukexor/tetanes/compare/v0.8.0..tetanes-v0.9.0) - 2023-10-31 ### ⛰️ Features - Added famicom 4-player support (fixes #78 - ([141e4ed](https://github.com/lukexor/tetanes/commit/141e4ed7b33e93d1cf183be327070d6532a16324)) - Added clock_inspect to Cpu - ([34944e6](https://github.com/lukexor/tetanes/commit/34944e63a4b0c72626c3313d2b849f0fa64a1c62)) - Added `Mapper::empty()` - ([30678c1](https://github.com/lukexor/tetanes/commit/30678c127231614316b3df97d7c95501ae77287c)) ### 🐛 Bug Fixes - *(events)* Fixed toggling menus - ([f30ade8](https://github.com/lukexor/tetanes/commit/f30ade860c3dd5ff995cadd4e409159d7fce9d91)) - Fixed wasm - ([7abd62a](https://github.com/lukexor/tetanes/commit/7abd62ad5a3178b5f77ae6c5c026d336d3237908)) - Fixed warnings - ([f88d760](https://github.com/lukexor/tetanes/commit/f88d760fa4d8c6fd9f532e70ba76f95b54273e5c)) - Fixed a number of bugs - ([5fd85af](https://github.com/lukexor/tetanes/commit/5fd85afd53efb8321f767b98372218eefc6e06a5)) - Fixed default tile attr - ([9002fa8](https://github.com/lukexor/tetanes/commit/9002fa87806fcd56009369bbcd2644400ae7c5a1)) - Fixed exram mode - ([8507984](https://github.com/lukexor/tetanes/commit/85079842d9ffe89b3e8ae61143642e09b3c471e6)) - Fix crosshair changes - ([10d843e](https://github.com/lukexor/tetanes/commit/10d843e78cb04a69ad8d40e334a21268f294861b)) - Fix audio on loading another rom - ([d7cc16c](https://github.com/lukexor/tetanes/commit/d7cc16cf7475f2b8bd632c2fc74a7b3c09447127)) - Improved wasm render performance - ([561be90](https://github.com/lukexor/tetanes/commit/561be907f652a7f4879e943b44274547c0e43172)) - Web audio tweaks - ([5184e11](https://github.com/lukexor/tetanes/commit/5184e11ba6fd104b59668e57744fb03b993483b5)) - Fixed game genie codes - ([0206d6f](https://github.com/lukexor/tetanes/commit/0206d6fd20e5aa66da716eb07c6e077bfe4c5eed)) - Fixed update rate clamping - ([2133b84](https://github.com/lukexor/tetanes/commit/2133b84ba865a0e715fa98129d9d6997540bd3e5)) - Fix resetting sprite presence - ([d219ce0](https://github.com/lukexor/tetanes/commit/d219ce02320573498c84651af1585df08ed0c44e)) - Fixed missing Reset changes - ([808fcac](https://github.com/lukexor/tetanes/commit/808fcac032d3b10731e330ecdcb9c468117a5425)) - Fixed missed clock changes - ([1b5313b](https://github.com/lukexor/tetanes/commit/1b5313bf61115457beb50d919bb1129e54adc7cc)) - Fixed toggling debugger - ([e7bcfc1](https://github.com/lukexor/tetanes/commit/e7bcfc1238fd21f957eed582b92a35e236e9884c)) - Fixed resetting output_buffer - ([0802b2b](https://github.com/lukexor/tetanes/commit/0802b2b35c14f34fc8d3e73f3bd1ce940b4a8f48)) - Fixed confirm quit - ([48d6538](https://github.com/lukexor/tetanes/commit/48d6538d25833a0817b0a27344a58a2a4918ab68)) ### 🚜 Refactor - Various updates - ([da213ae](https://github.com/lukexor/tetanes/commit/da213ae36aa0b4763c643d089e390d184d69dc19)) - Small renames - ([0dea0b6](https://github.com/lukexor/tetanes/commit/0dea0b6d15204f1fdfbe91ef8f9365993ffafaf2)) - Various cleanup - ([8d25103](https://github.com/lukexor/tetanes/commit/8d251030a9782bfb9f18fb29ce67b3853b8cf9bd)) - Cleaned up some interfaces - ([da3ba1b](https://github.com/lukexor/tetanes/commit/da3ba1b1b93f7ecacec3a93746026c0da174cbd4)) - Genie code cleanup - ([e483eb5](https://github.com/lukexor/tetanes/commit/e483eb5e9a793b8883710399a7bb39c3cc6ed3ee)) - Added region getters - ([74d4a76](https://github.com/lukexor/tetanes/commit/74d4a769fd089e3ff18295d88c5eaa60dcc208be)) - Cleaned up setting region - ([45dc2a4](https://github.com/lukexor/tetanes/commit/45dc2a42928a7d9c507351965969b7976ca7b25c)) - Flatten NTSC palette - ([792d7db](https://github.com/lukexor/tetanes/commit/792d7dbc45ec230df4de63b552d24bb4bbabc5c6)) - Converted system palette to array of tuples - ([284f54b](https://github.com/lukexor/tetanes/commit/284f54b877ccbf6103920cba483ee7d0175f4c5d)) - Condensed MapRead and MapWrite to MemMap trait - ([bce1c77](https://github.com/lukexor/tetanes/commit/bce1c7794ab0dc6ab493618f697fcc088864afb0)) - Made control methods consistent - ([f93040d](https://github.com/lukexor/tetanes/commit/f93040d25128f50c20226ba3d52d638dbdd85ac3)) - Switch u16 addresses to use from_le_bytes - ([d8936af](https://github.com/lukexor/tetanes/commit/d8936afaf8e3da54616d430cf3488f64a1aae5ef)) - Moved genie to it's own module - ([77b571f](https://github.com/lukexor/tetanes/commit/77b571f990c4f86d30e160382ff773486a8a54a9)) - Cleaned up Power and Clock traits - ([533c0c3](https://github.com/lukexor/tetanes/commit/533c0c3485cc73f880c4d43b2f937c0e606d0360)) - Cleaned up bg tile fetching - ([0710f16](https://github.com/lukexor/tetanes/commit/0710f162928964209898ef7fdf6aacd3a3e4a1a0)) - Move NTSC palette declaration - ([9edffd1](https://github.com/lukexor/tetanes/commit/9edffd1be33b3f79e1fb1a187bc89d3aede58804)) - Cleaned up memory traits - ([c98f7ff](https://github.com/lukexor/tetanes/commit/c98f7fffc59f3ac399864c7ff130cbaad99762f6)) - Swapped lazy_static for once_cell - ([cc9e67f](https://github.com/lukexor/tetanes/commit/cc9e67f643cf60ad982c88979b92f0ca843d505a)) ### ⚡ Performance - Cleaned up inlines - ([b791cc3](https://github.com/lukexor/tetanes/commit/b791cc3ef7ece4fe0b627ff7332020453aa086ce)) - Added inline to cart clock - ([eb9a0e0](https://github.com/lukexor/tetanes/commit/eb9a0e0d04fa6ec2d7f36935841dbda36a365bf8)) - Changed decoding loops - ([a181ed4](https://github.com/lukexor/tetanes/commit/a181ed46e23534294463856ca3fec3c55cf2938f)) - Performance tweaks - ([0c06758](https://github.com/lukexor/tetanes/commit/0c0675811709a2b590c273cd9010766e055b61ad)) ### 🎨 Styling - Fixed nightly lints - ([3bab00d](https://github.com/lukexor/tetanes/commit/3bab00dd1478be71f11baaed97c3aa8f3cc6d241)) ### 🧪 Testing - Disable broken test for now - ([93857cb](https://github.com/lukexor/tetanes/commit/93857cbd81d5b3f82ed157fdb87c0fa22aff1bc7)) - Remove vimspector for global config - ([56edc15](https://github.com/lukexor/tetanes/commit/56edc15af9285d27d2e180ae585ecf91cc6f1e2a)) ### ⚙️ Miscellaneous Tasks - Increase MSRV - ([684e771](https://github.com/lukexor/tetanes/commit/684e771a488f1cb88541b62265556b0fc79664a8)) ## [0.8.0](https://github.com/lukexor/tetanes/compare/v0.7.0...v0.8.0) - 2022-06-20 ### Added - Added `Mapper 024` and `Mapper 026` support. - Added `Mapper 071` support. - Added `Mapper 066` support. - Added `Mapper 155` support. [#36](https://github.com/lukexor/tetanes/pull/36) - Added configurable keybindings via `config.json`. - Added `Config` menu. - Added `Keybind` menu (still a WIP). - Added `Load ROM` menu. - Added `About` menu. - Added `Zapper` light gun support with a mouse. - Added lots of automated test roms. - Added `4-Player` support. [#32](https://github.com/lukexor/tetanes/issues/32) - Added audio `Dynamic Rate Control` feature. - Added `Cycle Accurate` feature. ### Changed - Various `README` improvements. - Default `VSync` to `true`. - Default `MMC1` to PRG RAM enable. - Changed audio filtering and playback. - Redesigned `TetaNES Web` UI and improved performance. ### Fixed - Fixed Power Cycle/Reset affecting `ppuaddr`. - Fixed reset causing segfault. [#50](https://github.com/lukexor/tetanes/issues/50) - Fixed reset and load updating the correct ROM banks. [#51](https://github.com/lukexor/tetanes/issues/51) - Fixed `OAM` emulation. [#31](https://github.com/lukexor/tetanes/issues/31) - Fixed `DMA` emulation. [#30](https://github.com/lukexor/tetanes/issues/30) - Fixed 512k `SxROM` games. - Fixed `IRQ` and `NMI` emulation. ### Removed - Removed `vcpkg` feature support due to flaky failures. ### Breaking - Major refactor of all features, affecting save and replay files. - Removed several command-line flags in favor of `config.json` and `Config` menu. ================================================ FILE: tetanes/Cargo.toml ================================================ [package] name = "tetanes" version.workspace = true edition.workspace = true license.workspace = true description = "A cross-platform NES Emulator written in Rust using wgpu" authors.workspace = true readme = "../README.md" repository.workspace = true homepage.workspace = true categories = ["emulators", "wasm"] keywords = ["nes", "emulator", "wasm"] exclude = ["/bin"] default-run = "tetanes" [[bin]] name = "tetanes" test = false bench = false [[bin]] name = "build_artifacts" test = false bench = false [lints] workspace = true [features] default = [] profiling = ["tetanes-core/profiling"] # Until webgpu is stable on all platforms webgpu = [] [dependencies] anyhow.workspace = true bincode.workspace = true bytemuck = "1.15" cfg-if.workspace = true chrono = { version = "0.4", default-features = false, features = [ "std", "clock", ] } crossbeam = "0.8" dirs.workspace = true egui = { version = "0.34", default-features = false, features = [ "bytemuck", "color-hex", "default_fonts", "persistence", "serde", ] } egui_extras = { version = "0.34", default-features = false, features = [ "image", "serde", ] } gilrs = { version = "0.11", features = ["serde-serialize"] } hound = "3.5" image.workspace = true parking_lot = "0.12" profiling = { version = "1.0", default-features = false } ringbuf = "0.4" serde.workspace = true serde_json.workspace = true sysinfo = "0.38" tetanes-core.workspace = true thingbuf = "0.1" thiserror.workspace = true tracing.workspace = true tracing-subscriber.workspace = true uuid = { version = "1.16", features = ["v4", "serde"] } webbrowser = { version = "1.0", features = ["hardened", "disable-wsl"] } wgpu = "29.0" winit = { version = "0.30", features = ["serde"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Blocked upgrading until egui upgrades accesskit accesskit = "0.24" # Blocked upgrading until accesskit can be upgraded accesskit_winit = "0.32" arboard = { version = "3.4", default-features = false, features = [ "wayland-data-control", "windows-sys", ] } clap.workspace = true cpal = "0.17" ctrlc = { version = "3.5", features = ["termination"] } egui = { version = "0.34", default-features = false } pollster = "0.4" reqwest = { version = "0.13", default-features = false, features = [ "rustls", "blocking", ] } rfd = "0.17" semver = "1" sysinfo = { version = "0.38", default-features = false, features = [ "system", "disk", "network", ] } tracing-appender = "0.2" [target.'cfg(target_arch = "wasm32")'.dependencies] base64 = "0.22" chrono = { version = "0.4", default-features = false, features = [ "std", "clock", "wasmbind", ] } console_error_panic_hook = "0.1" cpal = { version = "0.17", features = ["wasm-bindgen"] } # Required because of downstream dependencies: https://docs.rs/getrandom/latest/getrandom/#webassembly-support getrandom = { version = "0.4", features = ["wasm_js"] } tracing-web = "0.1" uuid = { version = "1.10", features = ["v4", "serde", "rng-getrandom"] } wgpu = { version = "29.0", features = [ "webgl", "fragile-send-sync-non-atomic-wasm", # Safe because we're not enabling atomics ] } web-sys = { workspace = true, features = [ "Clipboard", "ClipboardEvent", "DataTransfer", "Document", "DomTokenList", "Element", "File", "FileList", "FileReader", "HtmlAnchorElement", "HtmlCanvasElement", "HtmlElement", "HtmlInputElement", "HtmlDivElement", "Navigator", "SpeechSynthesis", "SpeechSynthesisUtterance", "Window", ] } wasm-bindgen = "0.2.118" wasm-bindgen-futures = "0.4" zip = { version = "8.0", default-features = false, features = ["deflate"] } [package.metadata.deb] extended-description = """ `TetaNES` is a cross-platform emulator for the Nintendo Entertainment System (NES) released in Japan in 1983 and North America in 1986, written in Rust using wgpu. It runs on Linux, macOS, Windows, and in a web browser with Web Assembly. It started as a personal curiosity that turned into a passion project. It is still being actively developed with new features and improvements constantly being added. It is a fairly accurate emulator that can play most NES titles. `TetaNES` is also meant to showcase using Rust's performance, memory safety, and fearless concurrency features in a large project. Features used in this project include complex enums, traits, generics, matching, iterators, channels, and threads. Try it out in your browser (https://lukeworks.tech/tetanes-web)! """ section = "game" assets = [ [ 'target/release/tetanes', '/usr/bin/', '755', ], [ "README.md", "usr/share/doc/tetanes/README", "644", ], ] ================================================ FILE: tetanes/assets/main.css ================================================ @font-face { font-family: "Pixeloid Sans"; src: local("Pixeloid Sans"), url("./pixeloid-sans.ttf") format("truetype"); } @font-face { font-family: "Pixeloid Sans Bold"; src: local("Pixeloid Sans Bold"), url("./pixeloid-sans-bold.ttf") format("truetype"); } body { --color: #e6b673; --heading: #a9491f; --background: #0f1419; background-color: var(--background); max-width: 80%; margin: auto; margin-bottom: 100px; color: var(--color); font-family: "Pixeloid Sans", "Courier New", Courier, monospace; } h1 { color: var(--heading); font-family: "Pixeloid Sans Bold", "Courier New", Courier, monospace; margin-top: 80px; margin-bottom: 40px; line-height: 0.5; text-align: center; } h1 span { color: var(--color); font-family: "Pixeloid Sans", "Courier New", Courier, monospace; font-size: 0.8rem; } h2 { color: var(--heading); font-family: "Pixeloid Sans Bold", "Courier New", Courier, monospace; text-align: center; margin-top: 40px; } p { font-size: 0.9rem; max-width: 70ch; margin: 15px 0; } table { --color: #333; border-collapse: separate; border-color: var(--color); border-spacing: 0; border: 0.5px solid var(--color); text-align: left; width: 100%; } th { color: var(--heading); } th, td { padding: 5px; border: 0.5px solid var(--color); } a { color: #36a3d9; text-decoration: none; } a:hover { text-decoration: underline; } canvas { width: fit-content; height: fit-content; outline: none; } #wrapper { display: flex; justify-content: center; flex-wrap: wrap; margin-bottom: 40px; } #content { max-width: 70ch; margin: auto; } .hidden { display: none !important; } .absolute { position: absolute; } #loading-status { position: absolute; display: flex; justify-content: center; align-items: center; width: 880px; height: 696px; border: 2px dotted #e6b673; margin: 0; } .loader { border: 4px solid #e6b673; border-top: 4px solid #a9491f; border-radius: 50%; width: 16px; height: 16px; margin: 8px; animation: spin 2s linear infinite; } #error { color: #ff3333; text-align: center; margin: auto; } .version-download { position: relative; width: max-content; margin: auto; } .version-download div { width: 100%; } .version-download a { display: block; padding: 0.8rem; background: #14191f; color: #a9491f; font-family: "Pixeloid Sans Bold", "Courier New", Courier, monospace; } .version-download a:hover { background: #212733; text-decoration: none; cursor: pointer; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } ================================================ FILE: tetanes/assets/pixeloid-license.txt ================================================ Copyright © 2020-2021 GGBot (https://ggbot.net/fonts/), with Reserved Font Name "Pixeloid". This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: https://openfontlicense.org/ ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ----------------------------------------------------------- PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. ================================================ FILE: tetanes/assets/roms/alter_ego.txt ================================================ Development notes After completing Lawn Master, I had plan to try use C compiler to make a simple NES game. From my previous experience with programming in C for micros (Genesis and ZX Spectrum), and from things that thefox did, I knew it could be very worthy in terms of development speed. The plan was to check it, and, in case of success, prove that C is an actual option to develop NES games, not just a theoretical possibility. I wanted to make a project very fast, so I decided to not do an original game this time, because design takes most of the time, and just make a port. I've seen two new ZX Spectrum games by Denis Grachev, Join and Alter ego, at WoS when they were released, and liked the combination of simplicity, playability, and sort of retro appeal in them. I made a low-level library in 6502 assembly to use in the project first. When main features of the library were implemented, I have sent a mail to Denis, asking if he would allow to port Alter Ego. It took some time, from June 8 to 17, to get the answer. Denis gave his permission, but I already was busy with other project, and only has been able to start on the port June 25. Development process took about 10 days, the game was fully completed, but without music, at July 5. This includes finishing the library, writing all the game code from scratch, reverse-engineering levels format, and beating up both the original game and port few times to test everything. The most difficult part that was not expected by me initially was redesign of all the levels from scratch. Initially I thought I can just convert them, edit a bit, and draw new graphics, but in order to be able to use more colors and make better graphics I had to completely redo all the levels to the NES attribute grid, only keeping overall design of the original levels. In other words, none of the original data get into the port, and there were some changes to make it more playable as well, so it is actually more like a remake than a port. Levels and graphics redesign took most of the time. I also made 5 graphics sets instead of 3 sets from the original. Code part was relatively easy both in assembly (low-level libary) and C (game), except for few WTF bugs that took some time to figure out. There are about 1000 lines of assembly code for library, 1000 lines of FamiTone code (has been adapted easiliy), and ~1500 lines of C code. Even total number of lines, 3500, is significally less than amount of assembly code in my previous NES games that were written in assembly, had ~5000 lines each (including FamiTone too), and were much simpler gameplay and game logic wise. As the game was a bit short on RAM, I've put FamiTone vars along with palette buffer into the stack page. Despite being written in C, the game uses ~20 bytes of the stack at most. Other part of speeding up the development process was 'outsourcing' of the music. I knew it is a risky decision, because any other person involved into a project actually increases overall time, not decreases it, but I just tired from making everything by myself all the time. It did increased time very considerably - although I've negotiated about the music with kulor even before starting any actual work on the game, by different reasons including personal busyness and some misunderstanding, he only started 15th, ten days after the game development itself was completed. This amount of music revealed a lot of bugs and problems in FamiTone, not all of which were fixed, and data of one of tracks was fixed by hand due to lack of time. Music was finished 22th, just in time for DiHalt demoparty. Initially I planned to just release game, but because the party date was now close, and there was a multiplatform game compo, I decided to release the game there to get more publicity. My conclucion regarding C usage on NES is that it is worthy indeed. It speeds up and simplifies development process a lot because it greatly reduces amount of code to be written and debugged, and the code is much more readable. However, to use C you just have to know the system and 6502 very well, because debugging is much more difficult - in case of the problems when C code does not work as expected, you need to figure out what to do by examining of the generated assembly code. So it is not easy way to program for NES, it actually requires more knowledge than programming in assembly. Execution speed is, of course, lower, but this wasn't an issue for this project, the size of the generated code was more important actually - it is much larger than it could be if programmed in assembly by hand. Please note that the game is released as freeware, not Public Domain. There are three authors involved. I personally grant you rights to do whatever you want with things I created (code, sound effects, graphics), but rights to other components (game concept, characters, title, music) are reserved to authors of these components. I.e., if you want to port it somewhere else, you need to ask Denis Grachev (and Kulor, if you need music) for permission. Software used CC65 - C compiler and assembler Notepad++ - for all the code and text works FamiTracker - to make all the music and sound effects UnrealSpeccy - playing and reversing the original version Borland Turbo Explorer - to make a level editor, but it was only used to view levels FCEUX, VirtuaNES (profiler mod) - to test everything, some others for compatibility tests NES Screen Tool - to design all the graphics, screens, and levels Inkscape, Blender, GIMP, CutePDF Writer - to make manual and label https://shiru.untergrund.net/software.shtml ================================================ FILE: tetanes/assets/roms/ao_demo.txt ================================================ AO by Second Dimension AO is a homebrew puzzle game. Each of the 33 levels consists of a fixed, single screen environment where a brick needs to be guided to a goal pit (exit point) in a maze-like environment shown through a top-down view. The brick is controlled directly and the view is in 2D, but the player needs to think in 3D to solve the puzzle. The brick takes up two spaces when it is laying on its side and one space when it is standing up. To get around in the maze the brick often needs to be brought into a specific position so it can move without falling off the edge. For instance: some passages only have room for a single space and then the brick cannot pass unless it is in a good position. When falling off the ledge the level restarts right away and there are unlimited lives. Gradually additional elements are introduced such as switches, teleportation blocks and weak floors. The game contains a competitive two-player mode where two players move through the maze simultaneously with the goal to reach the exit first. The game provides 1,000 seconds to complete the game, but it is possible to continue playing even when time has run out. https://www.second-dimension.com/catalog/ao ================================================ FILE: tetanes/assets/roms/assimilate.txt ================================================ Assimilate by Nessylum Games Ever wanted to anal probe someone? Well the wait is over! Assimilate is the game you and your dopey little friends have been waiting for. Join the super-ship Ossan for twenty plus levels of zombifying, brainwashing, human-dominating excitement. Will you succeed in conquering the entire human race? Or will you cry yourself to sleep after watching Ossan explode in a fiery ball of humiliating destruction? What happens is up to you. Begin assimilation. Pretty awesome action game, also available on actual cartridge. https://www.youtube.com/watch?v=cXCGeqvQ7Y4 https://forums.nesdev.org/viewtopic.php?t=7087&hilit=assimilate ================================================ FILE: tetanes/assets/roms/blade_buster.txt ================================================ Blade Buster by High Level Challenge It is produced within the constraints of the NES in fact, it runs on emulator and the operation of the actual machine has also been confirmed. You have 2 minutes in a caravan format to compete the score at 5 minutes. It has become a game with a sense of exhilaration that has good old shoot before being shot playing style. http://hlc6502.web.fc2.com/Bbuster.htm ================================================ FILE: tetanes/assets/roms/cheril_the_goddess.txt ================================================ Cheril the Goddess By The Mojon Twins You control Cheril. There’s plenty of things to do, and you’ll need special objects. Cheril can only carry ONE object at a time, so if you try to get a new one while you are carring an object, you will drop the one you carry in the place of the one you take. To get/interchange objects press DOWN. Besides, you’ll have to give those objects a use. To use an object, just walk to the place you want to use it and press DOWN. For Cheril to fly, she has to «push». You can make her «push» by pressing UP. But beware: «pushing» for too long drains Cheril’s vitality, so you have to do this carefully. Timing short «pushes» can make her gain momentum. Take your time to master the technique, it won’t take long. Cheril can also fire power balls. You can fire pressing FIRE. Power balls also drain Cheril, but think that enemies do quite a lot of harm. Use this feature wisely, and just when needed! Cheril can also regain her vitality by means of a special action we won’t reveal ‘cause it’s easy enough to find out! You can choose the difficulty level, but the easiest level won’t show the real ending. https://forums.nesdev.org/viewtopic.php?t=15367 ================================================ FILE: tetanes/assets/roms/dushlan.txt ================================================ //////////////////////////////////////////////////////////////////////////// // Dúshlán // // Copyright (c)2016 Peter McQuillan // // All Rights Reserved. // // Distributed under the BSD Software License (see license.txt) // //////////////////////////////////////////////////////////////////////////// This package contains the source code for the game 'Dúshlán'. Dúshlán is the Irish for 'Challenge'. This game is written in 6502 assembly for the Nintendo Entertainment System (NES). The code was developed using the ASM6 assembler. There is a NES file included in the package that can be used on an emulator, or an a real NES using a Flash cart like Powerpak or Everdrive. The game itself is based on the classic Tetris, but with a few twists on the game and some extra features that are not commonly available like ghost (where you can see where your piece would go if you dropped it) and save (where you can swap a piece in play for later usage). There are two possible button controls for the game, the normal controls are LEFT - Move left RIGHT - Move right DOWN - Drop piece UP - Swap/Save a piece for later usage A - Rotate piece clockwise B - Rotate piece anticlockwise START - Pause/Resume game SELECT - Enable/Disable ghost mode There is also an alternate control system available on the main menu, these are the button definitions for it: LEFT - move left RIGHT - move right DOWN - Rotate piece clockwise UP - Rotate piece anticlockwise A - Drop piece B - Swap/Save a piece for later usage START - Pause/Resume game SELECT - Enable/Disable ghost mode You are able to modify the behaviour of the 'Drop piece' key in the settings on the main menu. The three possible options are Full - Pressing the drop key will cause the piece to fall down the screen as far as possible While Held - This will cause the piece to drop as long as the drop key is pressed (effectively works like a down key) Mixture - If you quickly press/tap the drop key, the piece will fall down the screen as far as possible, however holding the drop key will drop the piece as long as the drop key is pressed - therefore a mixture of 'Full' and 'While Held' Thanks to Teuthida for the music and sound effects. Thanks to Derek Andrews for the ggsound sound engine Thanks also to Shiru, Drag, Damian Yerrick and Joe Granato for some code snippets used. Please direct any questions or comments to beatofthedrum@gmail.com https://github.com/soiaf/Dushlan ================================================ FILE: tetanes/assets/roms/from_below.txt ================================================ From Below by Matt Hughson FROM BELOW is a falling block puzzle game featuring: Soft Drops Hard Drops Wall Kicks T-Spins Lock Delay 3 modes of play: Kraken Battle Mode The signature mode of FROM BELOW. Battle the Kraken by clear lines across the onslaught of attacking Kraken Tentacles. The Tentacles push more blocks onto the screen every few seconds, forcing to act quickly, and strategize on an every changing board. Classic Mode The classic block falling mechanics you know and love without any new gimicks. Modernize for 2020, with Hard Drops, Lock Delay, and more, making this (hopefully) the best feeling puzzle game on the NES! Turn Based Kraken Battle Mode Similar to “Kraken Battle Mode”, but instead of the Kraken attacking every few seconds, it advances its tentacle every time you drop a piece. Make every move count, as this move favors slow, deliberate play! https://mhughson.itch.io/from-below/devlog/212679/vs-system-beta-0100 ================================================ FILE: tetanes/assets/roms/lan_master.txt ================================================ Lan Master by Shiru Development notes The project was started circa November 2010. I wanted to do a simple game that won't take too long, and expected it to be finished in 2010. It took a half of a year in the end, with some breaks and some side projects, including NES Screen Tool and FamiTone. At the very beginning I decided it going to be a simple NetWalk-like game (the NetWalk by Gamos, 1996), but without a server and with few additional elements. I had much of previous experience with games of this kind, so gameplay part was planned from the very beginning. The first thing that was done is graphics mock up made with Graphics Gale, title screen and gameplay screen. With minor changes it became actual graphics. The most difficult part of gameplay code, tracing, was done in C first, within the level editor. Then it was rewritten in 6502 assembly. Adding the sound was a difficult part as well, because most of the code game was done before I made FamiTone, and it wasn't designed properly. So sound update calls are really messy in the code. The game was developed without using real hardware at all, and only was tested on the hardware at pre-release state. There are about 5100 lines of manually written code, and also data generated by tools. One feature that I wanted to make was different palettes for levels. It was also suggested by 0xabad1dea after testing on the hardware. I actually implemented this feature, including color emphasis bits, but it just didn't look right (colors were too dark or too bright, and very different between the emulators), so it was removed. After the release, a problem was reported by mbrenaman - the ending screen was not working properly on the real hardware, and also part of the cursor was missing if the game is continued using a passcode. Series of fixes was done in order to fix the problem. Software used: Graphics Gale - both title screen and in-game graphics were made in it, converted and edited with NES Screen Tool afterwards Notepad++ - for all the code and text works NESASM v3.1 - to compile the code, no bugs everyone talking about were surfaced in this project FamiTracker - to make all the music and sound effects FCEUX, Nintendulator - to test everything Borland Turbo Explorer - to make level editor Inkscape, Blender, CutePDF Writer - to make manual and label https://shiru.untergrund.net/software.shtml ================================================ FILE: tetanes/assets/roms/lawn_mower.txt ================================================ Lawn Mower by Shiru The goal of this game is to mow all the grass before you run out of gas. Collect gas cans to keep yourself from running on empty. https://shiru.untergrund.net/software.shtml ================================================ FILE: tetanes/assets/roms/mad_wizard.txt ================================================ Mad Wizard By Sly Dog Studios Take an adventure in the world of Candelabra! The evil summoner Amondus from The Order of the Talon has taken over Prim, Hekl's once happy homeland. And nothing drives a wizard more crazy than having their territory trampled on! Can you help Hekl defeat the enemies that Amondus has populated throughout the landscape? To do so, you will need to master the art of levitation, find magic spells that will assist you in reaching new areas, and upgrade your weapons. All of these will be necessary in order to give Hekl the power he needs to restore peace to Prim. Do you have what it takes? If you dare, venture into this, the first installment of the Candelabra series! https://www.retrousb.com/product_info.php?cPath=31&products_id=137 ================================================ FILE: tetanes/assets/roms/micro_knight.txt ================================================ Micro Knight By SDM https://forums.nesdev.org/viewtopic.php?t=13450 ================================================ FILE: tetanes/assets/roms/nebs_n_debs.txt ================================================ Nebs 'n Debs By Dullahan Software Nebs 'n Debs is a new game for the original Nintendo Entertainment System. Run, jump, and dash your way through 12 levels as you search for the missing parts of Debs's ship to escape the hostile alien planet Vespasian 7MV! Nebs 'n Debs runs on the same type of game cartridge as the original Super Mario Bros. https://dullahan-software.itch.io/nebs-n-debs ================================================ FILE: tetanes/assets/roms/owlia.txt ================================================ The Legends of Owlia By Gradual Games The Legends of Owlia is Gradual Games' second release for the NES. It is an action-adventure game inspired by StarTropics, Crystalis, and the Legend of Zelda. Once upon a time. On a world far beyond imagining. Six great owls brought forth a land called Owlia. Together, they reigned in peace and wisdom for eighty thousand years. However, their pride in the beautiful land and sky of Owlia led them to forget the vast vast seas... Mermon, King of the Mermen, oft rose to the surface to view the Land of Owlia. His desire for sunlight, sky, and green forests grew until he decided the seas were not enough for him. He endeavored to summon the six great owls one by one. He began sapping their powers of flight, empowering his minions to float towards the Land of Owlia to claim it for his own. However, one great owl eluded him.. Silmaran, the White King. Soaring high above the Land of Owlia, as Mermon's forces grew in power, he searched for one who might heed his call to rescue the great owls and restore the Land of Owlia. Guide heroine Adlanniel and her owl friend Tyto to free the great owls and defeat Mermon king of the Mermen!! 12 enemy types and 5 fierce bosses! 7 full scrolling overworld maps! 5 screen by screen dungeons filled with puzzles! 8 cool techniques for your Owl Tyto to perform! Slash enemies with your trusty sword! Password system to save your progress! https://www.infiniteneslives.com/owlia.php ================================================ FILE: tetanes/assets/roms/streemerz.txt ================================================ __| __| __| _ \ _ \ __ `__ \ _ \ __|_ / \__ \ | | __/ __/ | | | __/ | / ____/\__|_| \___|\___|_| _| _|\___|_| ___| SUPER STRENGTH EMERGENCY SQUAD - ZETA ... NES port (C) 2012 Faux Game Company www.fauxgame.com Original version (C) 2010 Mr. Podunkian www.superfundungeonrun.com Info ---- "Try climbing to the top of this one by throwing streamers and climbing them. On your way up you better watch out for the various pie throwing clowns, burning candles and bouncing balls, because if they get you, you'll die a little each time." These were the orders given to you, Operative JOE when you were ordered to infiltrate the evil MASTER Y's floating fortress to destroy the TIGER ARMY's top secret weapon. Controls -------- Left/Right Move A Shoot the streamer (in normal game modes) Flip the gravity (in Streeeeeemerz mode) Start Pause the game Translations ------------ French translation of the game is now available courtesy of Feel My Geek and Lestat. Disclaimer: The translation was provided by a 3rd party, and I have no way of verifying its accuracy in comparison to the English version. License ------- Don't be a dick. Version History --------------- - v02 (2013-02-01) Fixed infinite bouncing bug in Streeeeeemerz mode. Fine-tuned controller handling during screen transitions. Added French translation. - v01 (2012-10-10) Initial release. ================================================ FILE: tetanes/assets/roms/super_painter.txt ================================================ Super Painter By RetroSouls & Kulor Trapped in a colorless world, armed with a paintbrush - there’s only one thing to do! As Super Painter, you’ll have to fill in all the missing color from the walls and ledges of 25 charming stages. Watch out for enemies and pits to the bottom, and don’t box yourself in - when you’re done painting, you’ll have to race to the magic door to the next level. It’s platform puzzling at its finest! https://www.retrosouls.net/?page_id=901 ================================================ FILE: tetanes/assets/roms/tiger_jenny.txt ================================================ Tiger Jenny By Ludosity Tiger Jenny by Ludosity is a NES game set in the same universe as “Ittle Dew” it takes place a thousand years before the events of that game. Battle your way through the forests to seek vengeance on the Turnip Witch who dwells in her castle. https://pdroms.de/files/nintendo-nintendoentertainmentsystem-nes-famicom-fc/tiger-jenny ================================================ FILE: tetanes/assets/roms/yun.txt ================================================ Yun By The Mojon Twins The main goal is helping yun capturing every single being to fill the pantry of her restaurant. The Big Marsh near Lake Potoña (province of Badajoz), formed by three areas (the marsh wood, the marsh abandoned factory and the mash desert) is full of walking flesh Yun must capture. To capture her enemies, Yun must stun them by means of hitting them with a bubble. Once they are stunned, they can be captured just touching them. Yun’s bubbles are quite resistant. You can hump on them and let them carry you upwards, which is sometimes the only way to progress in the level. Besides, there’s some points where Yun might need a key to keep going. https://www.mojontwins.com/juegos_mojonos/yun/ ================================================ FILE: tetanes/build.rs ================================================ fn main() { if let Ok(target) = std::env::var("TARGET") { println!("cargo:rustc-env=DEFAULT_TARGET={target}"); } } ================================================ FILE: tetanes/index.html ================================================ TetaNES Web

TetaNES

Loading...

TetaNES is a cross-platform emulator for the Nintendo Entertainment System (NES) released in Japan in 1983 and North America in 1986, written using Rust and wgpu. It runs on Linux, macOS, Windows, and in a web browser with WebAssembly. While the web version is playable, the desktop version is much more performant and fully featured.

Load any NES ROM which uses the iNES or NES 2.0 header format.

You can check out the code on github or download the desktop version:

Controls

Action Key
A Button Z
B Button X
A Button (Turbo) A
B Button (Turbo) S
Select Button Q
Start Button W
D-Pad Arrow Keys

Other mappings can be found and modified in the `Config -> Keybinds` menu.

================================================ FILE: tetanes/initializer.js ================================================ export default function () { const loading = document.getElementById("loading-status"); const error = document.getElementById("error"); return { onStart: () => { console.log("Loading..."); console.time("initializer"); loading.classList.remove("hidden"); error.classList.add("hidden"); }, onProgress: ({ current, total }) => { if (!total) { console.log(`Loading... ${current} bytes`); } else { console.log(`Loading... ${Math.round(current / total) * 100}%`); } }, onComplete: () => { console.log("Loading... done!"); console.timeEnd("initializer"); loading.classList.add("hidden"); }, onSuccess: () => { console.log("Loading... successful!"); error.classList.add("hidden"); }, onFailure: (error) => { console.error(`Loading... failed! ${error}`); loading.classList.add("hidden"); error.classList.remove("hidden"); error.innerText = `Loading... failed! ${error}`; }, }; } ================================================ FILE: tetanes/shaders/crt-easymode.wgsl ================================================ // CRT Shader by EasyMode // License: GPL // // A flat CRT shader ideally for 1080p or higher displays. // // Recommended Settings: // // Video // - Aspect Ratio: 4:3 // - Integer Scale: Off // // Shader // - Filter: Nearest // - Scale: Don't Care // // Example RGB Mask Parameter Settings: // // Aperture Grille (Default) // - Dot Width: 1 // - Dot Height: 1 // - Stagger: 0 // // Lottes' Shadow Mask // - Dot Width: 2 // - Dot Height: 1 // - Stagger: 3 // // Adapted from https://github.com/libretro/glsl-shaders/blob/master/crt/shaders/crt-easymode.glsl var vertices: array, 3> = array, 3>( vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0), ); // Vertex shader struct VertexOutput { @builtin(position) position: vec4, @location(0) tex_dims: vec2, @location(1) inv_tex_dims: vec2, @location(2) v_uv: vec2, }; @vertex fn vs_main( @builtin(vertex_index) v_idx: u32 ) -> VertexOutput { var out: VertexOutput; let vert = vertices[v_idx]; // Convert x from -1.0..1.0 to 0.0..1.0 and y from -1.0..1.0 to 1.0..0.0 out.position = vec4(vert, 0.0, 1.0); out.tex_dims = vec2(textureDimensions(tex)); out.inv_tex_dims = 1.0 / out.tex_dims; out.v_uv = fma(vert, vec2(0.5, -0.5), vec2(0.5, 0.5)); return out; } // Fragment shader struct Output { screen_size: vec2, // Uniform buffers need to be at least 16 bytes in WebGL. // See https://github.com/gfx-rs/wgpu/issues/2072 _padding: vec2, } @group(0) @binding(0) var out: Output; @group(1) @binding(0) var tex: texture_2d; @group(1) @binding(1) var tex_sampler: sampler; const PI = 3.141592653589; const SHARPNESS_H = 0.5; const SHARPNESS_V = 1.0; const MASK_STRENGTH = 0.3; const MASK_DOT_WIDTH = 1.0; const MASK_DOT_HEIGHT = 1.0; const MASK_STAGGER = 0.0; const MASK_SIZE = 1.0; const SCANLINE_STRENGTH = 1.0; const SCANLINE_BEAM_WIDTH_MIN = 1.5; const SCANLINE_BEAM_WIDTH_MAX = 1.5; const SCANLINE_BRIGHT_MIN = 0.35; const SCANLINE_BRIGHT_MAX = 0.65; const SCANLINE_CUTOFF = 2000.0; const GAMMA_INPUT = 2.4; const GAMMA_OUTPUT = 2.2; const BRIGHT_BOOST = 1.3; const DILATION = 1.0; // apply half-circle s-curve to distance for sharper (more pixelated) interpolation fn curve_distance(x: f32, sharp: f32) -> f32 { let x_step = step(0.5, x); let curve = 0.5 - sqrt(0.25 - (x - x_step) * (x - x_step)) * sign(0.5 - x); return mix(x, curve, sharp); } fn filter_lanczos(coeffs: vec4, color_matrix: mat4x4) -> vec3 { var col = color_matrix * coeffs; let sample_min = min(color_matrix[1], color_matrix[2]); let sample_max = max(color_matrix[1], color_matrix[2]); col = clamp(col, sample_min, sample_max); return col.rgb; } fn dilate(col: vec4) -> vec4 { let x = mix(vec4(1.0), col, DILATION); return col * x; } fn tex2d(c: vec2) -> vec4 { return dilate(textureSample(tex, tex_sampler, c)); } fn get_color_matrix(co: vec2, dx: vec2) -> mat4x4 { return mat4x4(tex2d(co - dx), tex2d(co), tex2d(co + dx), tex2d(co + 2.0 * dx)); } const NES_HEIGHT = 240.0; @fragment fn fs_main( @location(0) tex_dims: vec2, @location(1) inv_tex_dims: vec2, @location(2) v_uv: vec2 ) -> @location(0) vec4 { let pix_co = v_uv * tex_dims - vec2(0.5, 0.5); let tex_co = (floor(pix_co) + vec2(0.5, 0.5)) * inv_tex_dims; let dist = fract(pix_co); var curve_x = curve_distance(dist.x, SHARPNESS_H * SHARPNESS_H); var coeffs = PI * vec4(1.0 + curve_x, curve_x, 1.0 - curve_x, 2.0 - curve_x); coeffs = max(abs(coeffs), vec4(1e-5)); coeffs = 2.0 * sin(coeffs) * sin(coeffs * 0.5) / (coeffs * coeffs); coeffs /= dot(coeffs, vec4(1.0)); let dx = vec2(inv_tex_dims.x, 0.0); let dy = vec2(0.0, inv_tex_dims.y); var col = filter_lanczos(coeffs, get_color_matrix(tex_co, dx)); var col2 = filter_lanczos(coeffs, get_color_matrix(tex_co + dy, dx)); col = mix(col, col2, curve_distance(dist.y, SHARPNESS_V)); col = pow(col, vec3(GAMMA_INPUT / (DILATION + 1.0))); let luma = dot(vec3(0.2126, 0.7152, 0.0722), col); let bright = (max(col.r, max(col.g, col.b)) + luma) * 0.5; let scan_bright = clamp(bright, SCANLINE_BRIGHT_MIN, SCANLINE_BRIGHT_MAX); let scan_beam = clamp(bright * SCANLINE_BEAM_WIDTH_MAX, SCANLINE_BEAM_WIDTH_MIN, SCANLINE_BEAM_WIDTH_MAX); var scan_weight = 1.0 - pow(cos(v_uv.y * 2.0 * PI * NES_HEIGHT) * 0.5 + 0.5, scan_beam) * SCANLINE_STRENGTH; let mask = 1.0 - MASK_STRENGTH; let mod_fac = floor(v_uv * out.screen_size * tex_dims / (tex_dims * vec2(MASK_SIZE, MASK_DOT_HEIGHT * MASK_SIZE))); let dot_no = i32(((mod_fac.x + (mod_fac.y % 2.0) * MASK_STAGGER) / MASK_DOT_WIDTH % 3.0)); var mask_weight: vec3; if dot_no == 0 { mask_weight = vec3(1.0, mask, mask); } else if dot_no == 1 { mask_weight = vec3(mask, 1.0, mask); } else { mask_weight = vec3(mask, mask, 1.0); } if tex_dims.y >= SCANLINE_CUTOFF { scan_weight = 1.0; } col2 = col.rgb; col *= vec3(scan_weight); col = mix(col, col2, scan_bright); col *= mask_weight; col = pow(col, vec3(1.0 / GAMMA_OUTPUT)); return vec4(col * BRIGHT_BOOST, 1.0); } ================================================ FILE: tetanes/shaders/gui.wgsl ================================================ // Vertex shader struct VertexOutput { @builtin(position) position: vec4, @location(0) v_uv: vec2, @location(1) v_color: vec4, // gamma 0-1 }; struct Output { screen_size: vec2, // Uniform buffers need to be at least 16 bytes in WebGL. // See https://github.com/gfx-rs/wgpu/issues/2072 _padding: vec2, }; @group(0) @binding(0) var out: Output; // 0-1 linear from 0-1 sRGB gamma fn linear_from_gamma_rgb(srgb: vec3) -> vec3 { let cutoff = srgb < vec3(0.04045); let lower = srgb / vec3(12.92); let higher = pow((srgb + vec3(0.055)) / vec3(1.055), vec3(2.4)); return select(higher, lower, cutoff); } // 0-1 sRGB gamma from 0-1 linear fn gamma_from_linear_rgb(rgb: vec3) -> vec3 { let cutoff = rgb < vec3(0.0031308); let lower = rgb * vec3(12.92); let higher = vec3(1.055) * pow(rgb, vec3(1.0 / 2.4)) - vec3(0.055); return select(higher, lower, cutoff); } // 0-1 sRGBA gamma from 0-1 linear fn gamma_from_linear_rgba(linear_rgba: vec4) -> vec4 { return vec4(gamma_from_linear_rgb(linear_rgba.rgb), linear_rgba.a); } // [u8; 4] SRGB as u32 -> [r, g, b, a] in 0.-1 fn unpack_color(color: u32) -> vec4 { return vec4( f32(color & 255u), f32((color >> 8u) & 255u), f32((color >> 16u) & 255u), f32((color >> 24u) & 255u), ) / 255.0; } fn position_from_screen(screen_pos: vec2) -> vec4 { return vec4( 2.0 * screen_pos.x / out.screen_size.x - 1.0, 1.0 - 2.0 * screen_pos.y / out.screen_size.y, 0.0, 1.0, ); } @vertex fn vs_main( @location(0) v_pos: vec2, @location(1) v_uv: vec2, @location(2) v_color: u32, ) -> VertexOutput { var out: VertexOutput; out.v_uv = v_uv; out.v_color = unpack_color(v_color); out.position = position_from_screen(v_pos); return out; } // Fragment shader @group(1) @binding(0) var tex: texture_2d; @group(1) @binding(1) var tex_sampler: sampler; @fragment fn fs_main( @location(0) v_uv: vec2, @location(1) v_color: vec4 ) -> @location(0) vec4 { let tex = textureSample(tex, tex_sampler, v_uv); let tex_gamma = gamma_from_linear_rgba(tex); return v_color * tex_gamma; } ================================================ FILE: tetanes/src/bin/build_artifacts.rs ================================================ #![allow(unused)] use anyhow::Context; use cfg_if::cfg_if; use clap::Parser; use std::{ env, ffi::OsStr, fs, path::{Path, PathBuf}, process::{Command, ExitStatus, Output}, }; /// CLI options #[derive(Parser, Debug)] #[must_use] pub struct Args { /// Target platform to build for. e.g. `x86_64-unknown-linux-gnu`. #[clap(long, default_value = env!("DEFAULT_TARGET"))] target: String, /// Build for a target platform different from the host using /// `cross`. e.g. `aarch64-unknown-linux-gnu`. #[clap(long)] cross: bool, /// Clean `dist` directory before building. #[clap(long)] clean: bool, } /// Build context with required variables and platform targets. #[derive(Debug)] #[must_use] struct Build { version: &'static str, bin_name: &'static str, bin_path: PathBuf, app_name: &'static str, target_arch: String, #[cfg(target_os = "linux")] cross: bool, cargo_target_dir: PathBuf, dist_dir: PathBuf, } fn main() -> anyhow::Result<()> { let args = Args::parse(); let build = Build::new(args)?; println!("building artifacts: {build:?}..."); if build.target_arch == "wasm32-unknown-unknown" { build.make(["build-web"])?; build.compress_web_artifacts()?; } else { let build_args = ["build", "--target", &build.target_arch]; cfg_if! { if #[cfg(target_os = "linux")] { let build_args = if build.cross { vec!["build-cross"] } else { build_args.to_vec() }; build.make(build_args)?; build.create_linux_artifacts()?; } else if #[cfg(target_os = "macos")] { build.make(build_args)?; build.create_macos_app()?; } else if #[cfg(target_os = "windows")] { build.create_windows_installer()?; } } } Ok(()) } impl Build { /// Create a new build context by cleaning up any previous artifacts and ensuring the /// dist directory is created. fn new(args: Args) -> anyhow::Result { let bin_name = env!("CARGO_PKG_NAME"); let dist_dir = PathBuf::from(bin_name).join("dist"); if args.clean { let _ = remove_dir_all(&dist_dir); // ignore if not found } create_dir_all(&dist_dir)?; let cargo_target_dir = PathBuf::from(env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string())); let target_arch = args.target; Ok(Build { version: env!("CARGO_PKG_VERSION"), bin_name, bin_path: cargo_target_dir .join(&target_arch) .join("release") .join(bin_name), app_name: "TetaNES", target_arch, #[cfg(target_os = "linux")] cross: args.cross, cargo_target_dir, dist_dir, }) } /// Run `cargo make` to build binary. /// /// Note: Wix on Windows bakes in the build step fn make( &self, args: impl IntoIterator>, ) -> anyhow::Result { let mut cmd = Command::new("cargo"); cmd.arg("make"); for arg in args { cmd.arg(arg); } // TODO: disable lto and make pgo build cmd_spawn_wait(&mut cmd) } /// Create a dist directory for artifacts. fn create_build_dir(&self, dir: impl AsRef) -> anyhow::Result { let build_dir = self.cargo_target_dir.join(dir); println!("creating build directory: {build_dir:?}"); let _ = remove_dir_all(&build_dir); // ignore if not found create_dir_all(&build_dir)?; Ok(build_dir) } /// Write out a SHA256 checksum for a file. fn write_sha256(&self, file: impl AsRef, output: impl AsRef) -> anyhow::Result<()> { let file = file.as_ref(); let output = output.as_ref(); let shasum = { cfg_if! { if #[cfg(target_os = "windows")] { cmd_output(Command::new("powershell") .args(["-Command", "$ErrorActionPreference = 'Stop';"]) .arg(format!("Get-FileHash -Algorithm SHA256 {} | select-object -ExpandProperty Hash", file.display())))? } else { cmd_output(Command::new("shasum") .current_dir(file.parent().with_context(|| format!("no parent directory for {file:?}"))?) .args(["-a", "256"]) .arg(file.file_name().with_context(|| format!("no file_name for {file:?}"))?))? } } }; let sha256 = std::str::from_utf8(&shasum.stdout) .with_context(|| format!("invalid sha output for {file:?}"))? .trim() .to_owned(); println!("sha256: {sha256}"); write(output, shasum.stdout) } /// Create a Gzipped tarball. fn tar_gz( &self, tgz_name: impl AsRef, directory: impl AsRef, files: impl IntoIterator>, ) -> anyhow::Result<()> { let directory = directory.as_ref(); let tgz_name = tgz_name.as_ref(); let tgz_path = self.dist_dir.join(tgz_name); let mut cmd = Command::new("tar"); cmd.arg("-czvf") .arg(&tgz_path) .arg(format!("--directory={}", directory.display())); for file in files { cmd.arg(file.as_ref()); } cmd_spawn_wait(&mut cmd)?; self.write_sha256( tgz_path, self.dist_dir.join(format!("{tgz_name}-sha256.txt")), ) } /// Create linux artifacts (.tar.gz, .deb and .AppImage). #[cfg(target_os = "linux")] fn create_linux_artifacts(&self) -> anyhow::Result<()> { println!("creating linux artifacts..."); let build_dir = self.create_build_dir("linux")?; // Binary .tar.gz copy("README.md", build_dir.join("README.md"))?; copy("LICENSE-MIT", build_dir.join("LICENSE-MIT"))?; copy("LICENSE-APACHE", build_dir.join("LICENSE-APACHE"))?; let bin_path_build = build_dir.join(self.bin_name); copy(&self.bin_path, &bin_path_build)?; self.tar_gz( format!( "{}-{}-{}.tar.gz", self.bin_name, self.version, self.target_arch ), &build_dir, ["."], )?; // TODO: Fix deb/AppImage for cross builds if !self.cross { // Debian .deb // NOTE: 1- is the deb revision number let deb_name = format!("{}-{}-1-amd64.deb", self.bin_name, self.version); let deb_path_dist = self.dist_dir.join(&deb_name); cmd_spawn_wait( Command::new("cargo") .args([ "deb", "-v", "-p", "tetanes", "--target", &self.target_arch, "--no-build", // already built "--no-strip", // already stripped "-o", ]) .arg(&deb_path_dist), )?; self.write_sha256( &deb_path_dist, self.dist_dir.join(format!("{deb_name}-sha256.txt")), )?; // AppImage let arch = if self.target_arch.starts_with("x86_64") { "x86_64" } else if self.target_arch.starts_with("aarch64") { "aarch64" } else { anyhow::bail!("invalid linux target_arch: {}", self.target_arch); }; let linuxdeploy_cmd = format!("vendored/linuxdeploy-{arch}.AppImage"); let app_dir = build_dir.join("AppDir"); let desktop_name = format!("assets/linux/{}.desktop", self.bin_name); cmd_spawn_wait( Command::new(&linuxdeploy_cmd) .arg("-e") .arg(&self.bin_path) .args([ "-i", "assets/linux/icon.png", "-d", &desktop_name, "--appdir", ]) .arg(&app_dir) .args(["--output", "appimage"]), )?; // NOTE: AppImage name is derived from tetanes.desktop // Rename to lowercase let app_image_name = format!( "{}-{}-{}.AppImage", self.bin_name, self.version, self.target_arch ); let app_image_path = PathBuf::from(format!("{}-{}.AppImage", self.app_name, self.target_arch)); let app_image_path_dist = self.dist_dir.join(&app_image_name); rename(&app_image_path, &app_image_path_dist)?; self.write_sha256( &app_image_path_dist, self.dist_dir.join(format!("{app_image_name}-sha256.txt")), )?; } Ok(()) } /// Create macOS artifacts (.app in a .tar.gz and separate .dmg). #[cfg(target_os = "macos")] fn create_macos_app(&self) -> anyhow::Result<()> { println!("creating macos app..."); let build_dir = self.create_build_dir("macos")?; let artifact_name = format!("{}-{}-{}", self.bin_name, self.version, self.target_arch); let volume = PathBuf::from("/Volumes").join(&artifact_name); let app_name = format!("{}.app", self.app_name); let dmg_name = format!("{artifact_name}-uncompressed.dmg"); let dmg_path = build_dir.join(dmg_name); let dmg_name_compressed = format!("{artifact_name}.dmg"); let dmg_path_compressed = build_dir.join(&dmg_name_compressed); let dmg_path_dist = self.dist_dir.join(&dmg_name_compressed); if let Err(err) = cmd_status(Command::new("hdiutil").arg("detach").arg(&volume)) { eprintln!("failed to detach volume: {err:?}"); } cmd_spawn_wait( Command::new("hdiutil") .args(["create", "-size", "50m", "-volname", &artifact_name]) .arg(&dmg_path), )?; cmd_spawn_wait(Command::new("hdiutil").arg("attach").arg(&dmg_path))?; let _ = cmd_status(Command::new("mdutil").args(["-i", "off"]).arg(&volume)); let app_dir = volume.join(&app_name); create_dir_all(app_dir.join("Contents/MacOS"))?; create_dir_all(app_dir.join("Contents/Resources"))?; create_dir_all(volume.join(".Picture"))?; println!("updating Info.plist version: {}", self.version); let mut info_plist = read_to_string("assets/macos/Info.plist")?; info_plist = info_plist.replace("%VERSION%", self.version); write(app_dir.join("Contents/Info.plist"), info_plist)?; // TODO: maybe include readme/license? copy( "assets/macos/Icon.icns", app_dir.join("Contents/Resources/Icon.icns"), )?; copy( "assets/macos/background.png", volume.join(".Picture/background.png"), )?; copy("assets/macos/.DS_Store", volume.join(".DS_Store"))?; copy( &self.bin_path, app_dir.join("Contents/MacOS").join(self.bin_name), )?; symlink("/Applications", volume.join("Applications"))?; println!("configuring app bundle window..."); let app_bin_path = app_dir.join("Contents/MacOS").join(self.bin_name); cmd_spawn_wait( Command::new("codesign") .args(["--force", "--sign", "-"]) .arg(&app_bin_path), )?; // TODO: fix // ensure spctl --assess --type execute "${VOLUME}/${APP_NAME}.app" cmd_spawn_wait( Command::new("codesign") .args(["--verify", "--strict", "--verbose=2"]) .arg(&app_bin_path), )?; self.tar_gz( format!( "{}-{}-{}.tar.gz", self.bin_name, self.version, self.target_arch ), &volume, [&app_name], )?; std::thread::sleep(std::time::Duration::from_secs(2)); if let Err(err) = cmd_spawn_wait( Command::new("hdiutil") .args(["detach", "-force"]) .arg(&volume), ) { eprintln!("first detach failed, retrying: {err:?}"); std::thread::sleep(std::time::Duration::from_secs(3)); cmd_spawn_wait( Command::new("hdiutil") .args(["detach", "-force"]) .arg(&volume), )?; } cmd_spawn_wait( Command::new("hdiutil") .args(["convert", "-format", "UDBZ", "-o"]) .arg(&dmg_path_compressed) .arg(&dmg_path), )?; rename(&dmg_path_compressed, &dmg_path_dist)?; self.write_sha256( &dmg_path_dist, self.dist_dir .join(format!("{dmg_name_compressed}-sha256.txt")), ) } /// Create Windows artifacts (.msi). #[cfg(target_os = "windows")] fn create_windows_installer(&self) -> anyhow::Result<()> { println!("creating windows installer..."); let build_dir = self.create_build_dir("windows")?; let artifact_name = format!("{}-{}-{}", self.bin_name, self.version, self.target_arch); let installer_name = format!("{artifact_name}.msi"); let installer_path_build = build_dir.join(&installer_name); let zip_name = format!("{artifact_name}.zip"); let zip_path_dist = self.dist_dir.join(&zip_name); cmd_spawn_wait( Command::new("cargo") .args([ "wix", "-v", "-p", "tetanes", "--target", &self.target_arch, "--nocapture", "-o", ]) .arg(&installer_path_build), )?; cmd_spawn_wait(Command::new("powershell").args([ "-Command", "$ErrorActionPreference = 'Stop';", "Compress-Archive", "-Force", "-Path", &installer_path_build.to_string_lossy(), "-DestinationPath", &zip_path_dist.to_string_lossy(), ]))?; self.write_sha256( &zip_path_dist, self.dist_dir.join(format!("{zip_name}-sha256.txt")), ) } /// Compress web artifacts (.tar.gz). fn compress_web_artifacts(&self) -> anyhow::Result<()> { println!("compressing web artifacts..."); let build_dir = self.dist_dir.join("web"); self.tar_gz( format!( "{}-{}-{}.tar.gz", self.bin_name, self.version, self.target_arch ), &build_dir, ["."], )?; remove_dir_all(&build_dir) } } /// Helper function to `copy` a file and report contextual errors. fn copy(src: impl AsRef, dst: impl AsRef) -> anyhow::Result { let src = src.as_ref(); let dst = dst.as_ref(); println!("copying: {src:?} to {dst:?}"); fs::copy(src, dst).with_context(|| format!("failed to copy {src:?} to {dst:?}")) } /// Helper function to `rename` a file and report contextual errors. fn rename(src: impl AsRef, dst: impl AsRef) -> anyhow::Result<()> { let src = src.as_ref(); let dst = dst.as_ref(); println!("renaming: {src:?} to {dst:?}"); fs::rename(src, dst).with_context(|| format!("failed to rename {src:?} to {dst:?}")) } /// Helper function to `create_dir_all` a directory and report contextual errors. fn create_dir_all(dir: impl AsRef) -> anyhow::Result<()> { let dir = dir.as_ref(); println!("creating dir: {dir:?}"); fs::create_dir_all(dir).with_context(|| format!("failed to create {dir:?}")) } /// Helper function to `remove_dir_all` a directory and report contextual errors. fn remove_dir_all(dir: impl AsRef) -> anyhow::Result<()> { let dir = dir.as_ref(); println!("removing dir: {dir:?}"); fs::remove_dir_all(dir).with_context(|| format!("failed to remove {dir:?}")) } /// Helper function to `write` to a file and report contextual errors. fn write(path: impl AsRef, contents: impl AsRef<[u8]>) -> anyhow::Result<()> { let path = path.as_ref(); println!("writing to path: {path:?}"); let contents = contents.as_ref(); fs::write(path, contents).with_context(|| format!("failed to write to {path:?}")) } /// Helper function to `read_to_string` and report contextual errors. #[cfg(target_os = "macos")] fn read_to_string(path: impl AsRef) -> anyhow::Result { let path = path.as_ref(); println!("reading to string: {path:?}"); fs::read_to_string(path).with_context(|| format!("failed to read {path:?}")) } /// Helper function to `symlink` and report contextual errors. #[cfg(unix)] fn symlink(src: impl AsRef, dst: impl AsRef) -> anyhow::Result<()> { use std::os::unix::fs::symlink; let src = src.as_ref(); let dst = dst.as_ref(); println!("symlinking: {src:?} to {dst:?}"); symlink(src, dst).with_context(|| format!("failed to symlink {src:?} to {dst:?}")) } /// Helper function to `spawn` [`Command`] and `wait` while reporting contextual errors. fn cmd_spawn_wait(cmd: &mut Command) -> anyhow::Result { println!("running: {cmd:?}"); cmd.spawn() .with_context(|| format!("failed to spawn {cmd:?}"))? .wait() .with_context(|| format!("failed to run {cmd:?}")) } /// Helper function to run [`Command`] with `output` while reporting contextual errors. fn cmd_output(cmd: &mut Command) -> anyhow::Result { println!("running: {cmd:?}"); cmd.output() .with_context(|| format!("failed to run {cmd:?}")) } /// Helper function to run [`Command`] with `status` while reporting contextual errors. fn cmd_status(cmd: &mut Command) -> anyhow::Result { println!("running: {cmd:?}"); cmd.status() .with_context(|| format!("failed to run {cmd:?}")) } ================================================ FILE: tetanes/src/error.rs ================================================ pub type Error = anyhow::Error; pub type Result = anyhow::Result; pub use Error as NesError; pub use Result as NesResult; ================================================ FILE: tetanes/src/lib.rs ================================================ #![doc = include_str!("../README.md")] #![doc( html_favicon_url = "https://github.com/lukexor/tetanes/blob/main/assets/linux/icon.png?raw=true", html_logo_url = "https://github.com/lukexor/tetanes/blob/main/assets/linux/icon.png?raw=true" )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod error; pub mod logging; pub mod nes; pub mod platform; pub mod sys; pub mod thread; ================================================ FILE: tetanes/src/logging.rs ================================================ use crate::sys::logging; use std::env; use tracing_subscriber::{ Registry, filter::Targets, layer::{Layered, SubscriberExt}, util::SubscriberInitExt, }; fn create_registry() -> Layered { let default_log = if cfg!(debug_assertions) { "warn,tetanes=debug,tetanes-core=debug" } else { "warn,tetanes=info,tetanes-core=info" }; let default_filter = default_log.parse::().unwrap_or_default(); tracing_subscriber::registry().with( env::var("RUST_LOG") .ok() .and_then(|filter| filter.parse::().ok()) .unwrap_or(default_filter), ) } /// Initialize logging. pub fn init() -> anyhow::Result { let (registry, log) = logging::init_impl(create_registry())?; if let Err(err) = registry.try_init() { anyhow::bail!("setting tracing default failed: {err:?}"); } Ok(log) } ================================================ FILE: tetanes/src/main.rs ================================================ //! A NES Emulator written in Rust with `WebAssembly` support //! //! USAGE: //! tetanes [FLAGS] [OPTIONS] [path] //! //! FLAGS: //! -f, --fullscreen Start fullscreen. //! -h, --help Prints help information //! -V, --version Prints version information //! //! OPTIONS: //! -s, --scale Window scale [default: 3.0] //! //! ARGS: //! The NES ROM to load, a directory containing `.nes` ROM files, or a recording //! playback `.playback` file. [default: current directory] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use cfg_if::cfg_if; use tetanes::{ logging, nes::{Nes, config::Config}, }; #[cfg(not(target_arch = "wasm32"))] mod opts; cfg_if! { if #[cfg(target_arch = "wasm32")] { fn load_config() -> anyhow::Result { Ok(Config::load(None)) } } else { fn load_config() -> anyhow::Result { use clap::Parser; let opts = opts::Opts::parse(); tracing::debug!("CLI Options: {opts:?}"); opts.load() } } } fn main() -> anyhow::Result<()> { let log = logging::init(); if let Err(err) = log { eprintln!("failed to initialize logging: {err:?}"); } Nes::run(load_config()?) } ================================================ FILE: tetanes/src/nes/action.rs ================================================ //! An [`Action`] is an enumerated list of possible state changes to `TetaNES`. //! //! It allows for event handling and test abstractions such as being able to map a custom keybind //! to a given state change. use crate::nes::renderer::{gui::Menu, shader::Shader}; use serde::{Deserialize, Serialize}; use tetanes_core::{ action::Action as DeckAction, apu::Channel, common::{NesRegion, ResetKind}, input::{FourPlayer, JoypadBtn, Player}, mapper::{Bf909Revision, MapperRevision, Mmc3Revision}, video::VideoFilter, }; #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Action { Ui(Ui), Menu(Menu), Feature(Feature), Setting(Setting), Deck(DeckAction), Debug(Debug), } impl PartialOrd for Action { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Action { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.as_ref().cmp(other.as_ref()) } } impl Action { pub const BINDABLE: [Self; 112] = [ Self::Ui(Ui::Quit), Self::Ui(Ui::TogglePause), Self::Ui(Ui::LoadRom), Self::Ui(Ui::UnloadRom), Self::Ui(Ui::LoadReplay), Self::Menu(Menu::About), Self::Menu(Menu::Keybinds), Self::Menu(Menu::PerfStats), Self::Menu(Menu::Preferences), Self::Feature(Feature::ToggleReplayRecording), Self::Feature(Feature::ToggleAudioRecording), Self::Feature(Feature::VisualRewind), Self::Feature(Feature::InstantRewind), Self::Feature(Feature::TakeScreenshot), Self::Setting(Setting::ToggleFullscreen), Self::Setting(Setting::ToggleEmbedViewports), Self::Setting(Setting::ToggleAlwaysOnTop), Self::Setting(Setting::ToggleAudio), Self::Setting(Setting::ToggleRewinding), Self::Setting(Setting::ToggleOverscan), Self::Setting(Setting::ToggleMenubar), Self::Setting(Setting::ToggleMessages), Self::Setting(Setting::ToggleFps), Self::Setting(Setting::FastForward), Self::Setting(Setting::IncrementScale), Self::Setting(Setting::DecrementScale), Self::Setting(Setting::IncrementSpeed), Self::Setting(Setting::DecrementSpeed), Self::Setting(Setting::SetShader(Shader::Default)), Self::Setting(Setting::SetShader(Shader::CrtEasymode)), Self::Deck(DeckAction::Reset(ResetKind::Soft)), Self::Deck(DeckAction::Reset(ResetKind::Hard)), Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::Left))), Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::Right))), Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::Up))), Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::Down))), Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::A))), Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::B))), Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::TurboA))), Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::TurboB))), Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::Select))), Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::Start))), Self::Deck(DeckAction::Joypad((Player::Two, JoypadBtn::Left))), Self::Deck(DeckAction::Joypad((Player::Two, JoypadBtn::Right))), Self::Deck(DeckAction::Joypad((Player::Two, JoypadBtn::Up))), Self::Deck(DeckAction::Joypad((Player::Two, JoypadBtn::Down))), Self::Deck(DeckAction::Joypad((Player::Two, JoypadBtn::A))), Self::Deck(DeckAction::Joypad((Player::Two, JoypadBtn::B))), Self::Deck(DeckAction::Joypad((Player::Two, JoypadBtn::TurboA))), Self::Deck(DeckAction::Joypad((Player::Two, JoypadBtn::TurboB))), Self::Deck(DeckAction::Joypad((Player::Two, JoypadBtn::Select))), Self::Deck(DeckAction::Joypad((Player::Two, JoypadBtn::Start))), Self::Deck(DeckAction::Joypad((Player::Three, JoypadBtn::Left))), Self::Deck(DeckAction::Joypad((Player::Three, JoypadBtn::Right))), Self::Deck(DeckAction::Joypad((Player::Three, JoypadBtn::Up))), Self::Deck(DeckAction::Joypad((Player::Three, JoypadBtn::Down))), Self::Deck(DeckAction::Joypad((Player::Three, JoypadBtn::A))), Self::Deck(DeckAction::Joypad((Player::Three, JoypadBtn::B))), Self::Deck(DeckAction::Joypad((Player::Three, JoypadBtn::TurboA))), Self::Deck(DeckAction::Joypad((Player::Three, JoypadBtn::TurboB))), Self::Deck(DeckAction::Joypad((Player::Three, JoypadBtn::Select))), Self::Deck(DeckAction::Joypad((Player::Three, JoypadBtn::Start))), Self::Deck(DeckAction::Joypad((Player::Four, JoypadBtn::Left))), Self::Deck(DeckAction::Joypad((Player::Four, JoypadBtn::Right))), Self::Deck(DeckAction::Joypad((Player::Four, JoypadBtn::Up))), Self::Deck(DeckAction::Joypad((Player::Four, JoypadBtn::Down))), Self::Deck(DeckAction::Joypad((Player::Four, JoypadBtn::A))), Self::Deck(DeckAction::Joypad((Player::Four, JoypadBtn::B))), Self::Deck(DeckAction::Joypad((Player::Four, JoypadBtn::TurboA))), Self::Deck(DeckAction::Joypad((Player::Four, JoypadBtn::TurboB))), Self::Deck(DeckAction::Joypad((Player::Four, JoypadBtn::Select))), Self::Deck(DeckAction::Joypad((Player::Four, JoypadBtn::Start))), Self::Deck(DeckAction::ToggleZapperConnected), // Self::Deck(DeckAction::ZapperAim), // Binding doesn't make sense Self::Deck(DeckAction::ZapperTrigger), Self::Deck(DeckAction::FourPlayer(FourPlayer::Disabled)), Self::Deck(DeckAction::FourPlayer(FourPlayer::FourScore)), Self::Deck(DeckAction::FourPlayer(FourPlayer::Satellite)), // Only allow bindings up to 8 slots Self::Deck(DeckAction::SetSaveSlot(1)), Self::Deck(DeckAction::SetSaveSlot(2)), Self::Deck(DeckAction::SetSaveSlot(3)), Self::Deck(DeckAction::SetSaveSlot(4)), Self::Deck(DeckAction::SetSaveSlot(5)), Self::Deck(DeckAction::SetSaveSlot(6)), Self::Deck(DeckAction::SetSaveSlot(7)), Self::Deck(DeckAction::SetSaveSlot(8)), Self::Deck(DeckAction::SaveState), Self::Deck(DeckAction::LoadState), Self::Deck(DeckAction::ToggleApuChannel(Channel::Pulse1)), Self::Deck(DeckAction::ToggleApuChannel(Channel::Pulse2)), Self::Deck(DeckAction::ToggleApuChannel(Channel::Triangle)), Self::Deck(DeckAction::ToggleApuChannel(Channel::Noise)), Self::Deck(DeckAction::ToggleApuChannel(Channel::Dmc)), Self::Deck(DeckAction::ToggleApuChannel(Channel::Mapper)), Self::Deck(DeckAction::MapperRevision(MapperRevision::Mmc3( Mmc3Revision::A, ))), Self::Deck(DeckAction::MapperRevision(MapperRevision::Mmc3( Mmc3Revision::BC, ))), Self::Deck(DeckAction::MapperRevision(MapperRevision::Mmc3( Mmc3Revision::Acc, ))), Self::Deck(DeckAction::MapperRevision(MapperRevision::Bf909( Bf909Revision::Bf909x, ))), Self::Deck(DeckAction::MapperRevision(MapperRevision::Bf909( Bf909Revision::Bf9097, ))), Self::Deck(DeckAction::SetNesRegion(NesRegion::Auto)), Self::Deck(DeckAction::SetNesRegion(NesRegion::Ntsc)), Self::Deck(DeckAction::SetNesRegion(NesRegion::Pal)), Self::Deck(DeckAction::SetNesRegion(NesRegion::Dendy)), Self::Deck(DeckAction::SetVideoFilter(VideoFilter::Pixellate)), Self::Deck(DeckAction::SetVideoFilter(VideoFilter::Ntsc)), Self::Debug(Debug::Toggle(DebugKind::Cpu)), Self::Debug(Debug::Toggle(DebugKind::Ppu)), Self::Debug(Debug::Toggle(DebugKind::Apu)), Self::Debug(Debug::Step(DebugStep::Into)), Self::Debug(Debug::Step(DebugStep::Out)), Self::Debug(Debug::Step(DebugStep::Over)), Self::Debug(Debug::Step(DebugStep::Scanline)), Self::Debug(Debug::Step(DebugStep::Frame)), ]; pub const fn is_joypad(&self) -> bool { matches!(self, Action::Deck(DeckAction::Joypad(_))) } pub fn joypad_player(&self, player: Player) -> bool { matches!(self, Action::Deck(DeckAction::Joypad((p, _))) if p == &player) } } impl std::fmt::Display for Action { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_ref()) } } impl AsRef for Action { fn as_ref(&self) -> &str { match self { Action::Ui(ui) => match ui { Ui::Quit => "Quit", Ui::TogglePause => "Toggle Pause", Ui::LoadRom => "Load ROM", Ui::UnloadRom => "Unload ROM", Ui::LoadReplay => "Load Replay", }, Action::Menu(menu) => match menu { Menu::About => "Toggle About", Menu::Keybinds => "Toggle Keybinds", Menu::PerfStats => "Toggle Performance Stats", Menu::PpuViewer => "Toggle PPU Viewer", Menu::Preferences => "Toggle Preferences", }, Action::Feature(feature) => match feature { Feature::ToggleReplayRecording => "Toggle Replay Recording", Feature::ToggleAudioRecording => "Toggle Audio Recording", Feature::VisualRewind => "Visual Rewind", Feature::InstantRewind => "Instant Rewind", Feature::TakeScreenshot => "Take Screenshot", }, Action::Setting(setting) => match setting { Setting::ToggleFullscreen => "Toggle Fullscreen", Setting::ToggleEmbedViewports => "Toggle Embed Viewports", Setting::ToggleAlwaysOnTop => "Toggle Always On Top", Setting::ToggleAudio => "Toggle Audio", Setting::ToggleRewinding => "Toggle Rewinding", Setting::ToggleOverscan => "Toggle Overscan", Setting::ToggleMenubar => "Toggle Menubar", Setting::ToggleMessages => "Toggle Messages", Setting::ToggleScreenReader => "Toggle Screen Reader", Setting::ToggleFps => "Toggle FPS", Setting::FastForward => "Fast Forward", Setting::IncrementScale => "Increment Scale", Setting::DecrementScale => "Decrement Scale", Setting::IncrementSpeed => "Increment Speed", Setting::DecrementSpeed => "Decrement Speed", Setting::SetShader(shader) => match shader { Shader::Default => "Set Default Shader", Shader::CrtEasymode => "Set Shader to CRT Easymode", }, }, Action::Deck(deck) => match deck { DeckAction::Reset(kind) => match kind { ResetKind::Soft => "Reset", ResetKind::Hard => "Power Cycle", }, DeckAction::Joypad((_, joypad)) => match joypad { JoypadBtn::Left => "Joypad Left", JoypadBtn::Right => "Joypad Right", JoypadBtn::Up => "Joypad Up", JoypadBtn::Down => "Joypad Down", JoypadBtn::A => "Joypad A", JoypadBtn::B => "Joypad B", JoypadBtn::TurboA => "Joypad Turbo A", JoypadBtn::TurboB => "Joypad Turbo B", JoypadBtn::Select => "Joypad Select", JoypadBtn::Start => "Joypad Start", }, DeckAction::ToggleZapperConnected => "Zapper Gun Toggle", DeckAction::ZapperAim(_) => "Zapper Aim", DeckAction::ZapperAimOffscreen => "Zapper Aim Offscreen (Hold)", DeckAction::ZapperTrigger => "Zapper Trigger", DeckAction::FourPlayer(FourPlayer::Disabled) => "4-Player Disable", DeckAction::FourPlayer(FourPlayer::FourScore) => "4-Player Enable (FourScore)", DeckAction::FourPlayer(FourPlayer::Satellite) => "4-Player Enable (Satellite)", DeckAction::SetSaveSlot(1) => "Set Save Slot 1", DeckAction::SetSaveSlot(2) => "Set Save Slot 2", DeckAction::SetSaveSlot(3) => "Set Save Slot 3", DeckAction::SetSaveSlot(4) => "Set Save Slot 4", DeckAction::SetSaveSlot(5) => "Set Save Slot 5", DeckAction::SetSaveSlot(6) => "Set Save Slot 6", DeckAction::SetSaveSlot(7) => "Set Save Slot 7", DeckAction::SetSaveSlot(8) => "Set Save Slot 8", DeckAction::SetSaveSlot(_) => "Set Save Slot N", DeckAction::SaveState => "Save State", DeckAction::LoadState => "Load State", DeckAction::ToggleApuChannel(channel) => match channel { Channel::Pulse1 => "Toggle Pulse1 Channel", Channel::Pulse2 => "Toggle Pulse2 Channel", Channel::Triangle => "Toggle Triangle Channel", Channel::Noise => "Toggle Noise Channel", Channel::Dmc => "Toggle DMC Channel", Channel::Mapper => "Toggle Mapper Channel", }, DeckAction::MapperRevision(rev) => match rev { MapperRevision::Mmc3(mmc3) => match mmc3 { Mmc3Revision::A => "Set Mapper to MMC3A", Mmc3Revision::BC => "Set Mapper to MMC3B/C", Mmc3Revision::Acc => "Set Mapper to MC-ACC", }, MapperRevision::Bf909(bf909) => match bf909 { Bf909Revision::Bf909x => "Set Mapper to BF909x", Bf909Revision::Bf9097 => "Set Mapper to BF9097", }, }, DeckAction::SetNesRegion(region) => match region { NesRegion::Auto => "Set Region to Auto", NesRegion::Ntsc => "Set Region to NTSC", NesRegion::Pal => "Set Region to PAL", NesRegion::Dendy => "Set Region to Dendy", }, DeckAction::SetVideoFilter(filter) => match filter { VideoFilter::Pixellate => "Set Filter to Pixellate", VideoFilter::Ntsc => "Set Filter to NTSC", }, }, Action::Debug(debug) => match debug { Debug::Toggle(debugger) => match debugger { DebugKind::Cpu => "Toggle Debugger", DebugKind::Ppu => "Toggle PPU Viewer", DebugKind::Apu => "Toggle APU Mixer", }, Debug::Step(step) => match step { DebugStep::Into => "Debug Step", DebugStep::Out => "Debug Step Out", DebugStep::Over => "Debug Step Over", DebugStep::Scanline => "Debug Step Scanline", DebugStep::Frame => "Debug Step Frame", }, }, } } } impl TryFrom<&str> for Action { type Error = anyhow::Error; fn try_from(s: &str) -> Result { Ok(match s { "Quit" => Self::Ui(Ui::Quit), "Toggle Pause" => Self::Ui(Ui::TogglePause), "Load ROM" => Self::Ui(Ui::LoadRom), "Unload ROM" => Self::Ui(Ui::UnloadRom), "Load Replay" => Self::Ui(Ui::LoadReplay), "Toggle About Window" => Self::Menu(Menu::About), "Toggle Keybinds Menu" => Self::Menu(Menu::Keybinds), "Toggle Performance Stats Window" => Self::Menu(Menu::PerfStats), "Toggle PPU Viewer" => Self::Menu(Menu::PpuViewer), "Toggle Preferences Menu" => Self::Menu(Menu::Preferences), "Toggle Replay Recording" => Self::Feature(Feature::ToggleReplayRecording), "Toggle Audio Recording" => Self::Feature(Feature::ToggleAudioRecording), "Visual Rewind" => Self::Feature(Feature::VisualRewind), "Instant Rewind" => Self::Feature(Feature::InstantRewind), "Take Screenshot" => Self::Feature(Feature::TakeScreenshot), "Toggle Fullscreen" => Self::Setting(Setting::ToggleFullscreen), "Toggle Embed Viewports" => Self::Setting(Setting::ToggleEmbedViewports), "Toggle Always On Top" => Self::Setting(Setting::ToggleAlwaysOnTop), "Toggle Audio" => Self::Setting(Setting::ToggleAudio), "Toggle Rewinding" => Self::Setting(Setting::ToggleRewinding), "Toggle Overscan" => Self::Setting(Setting::ToggleOverscan), "Toggle Menubar" => Self::Setting(Setting::ToggleMenubar), "Toggle Messages" => Self::Setting(Setting::ToggleMessages), "Toggle FPS" => Self::Setting(Setting::ToggleFps), "Fast Forward" => Self::Setting(Setting::FastForward), "Increment Scale" => Self::Setting(Setting::IncrementScale), "Decrement Scale" => Self::Setting(Setting::DecrementScale), "Increment Speed" => Self::Setting(Setting::IncrementSpeed), "Decrement Speed" => Self::Setting(Setting::DecrementSpeed), "Set Default Shader" => Self::Setting(Setting::SetShader(Shader::Default)), "Set Shader to CRT Easymode" => Self::Setting(Setting::SetShader(Shader::CrtEasymode)), "Reset" => Self::Deck(DeckAction::Reset(ResetKind::Soft)), "Power Cycle" => Self::Deck(DeckAction::Reset(ResetKind::Hard)), "Joypad Left (P1)" => Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::Left))), "Joypad Right (P1)" => Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::Right))), "Joypad Up (P1)" => Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::Up))), "Joypad Down (P1)" => Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::Down))), "Joypad A (P1)" => Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::A))), "Joypad B (P1)" => Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::B))), "Joypad Turbo A (P1)" => { Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::TurboA))) } "Joypad Turbo B (P1)" => { Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::TurboB))) } "Joypad Select (P1)" => { Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::Select))) } "Joypad Start (P1)" => Self::Deck(DeckAction::Joypad((Player::One, JoypadBtn::Start))), "Toggle Zapper Connected" => Self::Deck(DeckAction::ToggleZapperConnected), "Zapper Aim" => Self::Deck(DeckAction::ZapperAim((0, 0))), "Zapper Aim Offscreen (Hold)" => Self::Deck(DeckAction::ZapperAimOffscreen), "Zapper Trigger" => Self::Deck(DeckAction::ZapperTrigger), "Disable Four Player Mode" => Self::Deck(DeckAction::FourPlayer(FourPlayer::Disabled)), "Enable Four Player (FourScore)" => { Self::Deck(DeckAction::FourPlayer(FourPlayer::FourScore)) } "Enable Four Player (Satellite)" => { Self::Deck(DeckAction::FourPlayer(FourPlayer::Satellite)) } "Set Save Slot 1" => Self::Deck(DeckAction::SetSaveSlot(1)), "Set Save Slot 2" => Self::Deck(DeckAction::SetSaveSlot(2)), "Set Save Slot 3" => Self::Deck(DeckAction::SetSaveSlot(3)), "Set Save Slot 4" => Self::Deck(DeckAction::SetSaveSlot(4)), "Set Save Slot 5" => Self::Deck(DeckAction::SetSaveSlot(5)), "Set Save Slot 6" => Self::Deck(DeckAction::SetSaveSlot(6)), "Set Save Slot 7" => Self::Deck(DeckAction::SetSaveSlot(7)), "Set Save Slot 8" => Self::Deck(DeckAction::SetSaveSlot(8)), "Save State" => Self::Deck(DeckAction::SaveState), "Load State" => Self::Deck(DeckAction::LoadState), "Toggle Pulse1 Channel" => Self::Deck(DeckAction::ToggleApuChannel(Channel::Pulse1)), "Toggle Pulse2 Channel" => Self::Deck(DeckAction::ToggleApuChannel(Channel::Pulse2)), "Toggle Triangle Channel" => { Self::Deck(DeckAction::ToggleApuChannel(Channel::Triangle)) } "Toggle Noise Channel" => Self::Deck(DeckAction::ToggleApuChannel(Channel::Noise)), "Toggle DMC Channel" => Self::Deck(DeckAction::ToggleApuChannel(Channel::Dmc)), "Toggle Mapper Channel" => Self::Deck(DeckAction::ToggleApuChannel(Channel::Mapper)), "Set Mapper Rev. to MMC3A" => Self::Deck(DeckAction::MapperRevision( MapperRevision::Mmc3(Mmc3Revision::A), )), "Set Mapper Rev. to MMC3B/C" => Self::Deck(DeckAction::MapperRevision( MapperRevision::Mmc3(Mmc3Revision::BC), )), "Set Mapper Rev. to MC-ACC" => Self::Deck(DeckAction::MapperRevision( MapperRevision::Mmc3(Mmc3Revision::Acc), )), "Set Mapper Rev. to BF909x" => Self::Deck(DeckAction::MapperRevision( MapperRevision::Bf909(Bf909Revision::Bf909x), )), "Set Mapper Rev. to BF9097" => Self::Deck(DeckAction::MapperRevision( MapperRevision::Bf909(Bf909Revision::Bf9097), )), "Set Region to Auto-Detect" => Self::Deck(DeckAction::SetNesRegion(NesRegion::Auto)), "Set Region to NTSC" => Self::Deck(DeckAction::SetNesRegion(NesRegion::Ntsc)), "Set Region to PAL" => Self::Deck(DeckAction::SetNesRegion(NesRegion::Pal)), "Set Region to Dendy" => Self::Deck(DeckAction::SetNesRegion(NesRegion::Dendy)), "Set Filter to Pixellate" => { Self::Deck(DeckAction::SetVideoFilter(VideoFilter::Pixellate)) } "Set Filter to NTSC" => Self::Deck(DeckAction::SetVideoFilter(VideoFilter::Ntsc)), "Toggle CPU Debugger" => Self::Debug(Debug::Toggle(DebugKind::Cpu)), "Toggle PPU Debugger" => Self::Debug(Debug::Toggle(DebugKind::Ppu)), "Toggle APU Debugger" => Self::Debug(Debug::Toggle(DebugKind::Apu)), "Step Into (CPU Debugger)" => Self::Debug(Debug::Step(DebugStep::Into)), "Step Out (CPU Debugger)" => Self::Debug(Debug::Step(DebugStep::Out)), "Step Over (CPU Debugger)" => Self::Debug(Debug::Step(DebugStep::Over)), "Step Scanline (CPU Debugger)" => Self::Debug(Debug::Step(DebugStep::Scanline)), "Step Frame (CPU Debugger)" => Self::Debug(Debug::Step(DebugStep::Frame)), _ => return Err(anyhow::anyhow!("Invalid action string")), }) } } impl From for Action { fn from(state: Ui) -> Self { Self::Ui(state) } } impl From for Action { fn from(menu: Menu) -> Self { Self::Menu(menu) } } impl From for Action { fn from(feature: Feature) -> Self { Self::Feature(feature) } } impl From for Action { fn from(setting: Setting) -> Self { Self::Setting(setting) } } impl From<(Player, JoypadBtn)> for Action { fn from((player, btn): (Player, JoypadBtn)) -> Self { Self::Deck(DeckAction::Joypad((player, btn))) } } impl From for Action { fn from(deck: DeckAction) -> Self { Self::Deck(deck) } } impl From for Action { fn from(action: Debug) -> Self { Self::Debug(action) } } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Ui { Quit, TogglePause, LoadRom, LoadReplay, UnloadRom, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Feature { ToggleReplayRecording, ToggleAudioRecording, VisualRewind, InstantRewind, TakeScreenshot, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Setting { ToggleFullscreen, ToggleEmbedViewports, ToggleAlwaysOnTop, ToggleAudio, ToggleRewinding, ToggleOverscan, ToggleMenubar, ToggleMessages, ToggleScreenReader, ToggleFps, FastForward, IncrementScale, DecrementScale, IncrementSpeed, DecrementSpeed, SetShader(Shader), } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[must_use] pub enum DebugKind { Cpu, Ppu, Apu, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[must_use] pub enum DebugStep { Into, Out, Over, Scanline, Frame, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Debug { Toggle(DebugKind), Step(DebugStep), } ================================================ FILE: tetanes/src/nes/audio.rs ================================================ use crate::nes::config::Config; use anyhow::{Context, anyhow}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use ringbuf::{ CachingCons, CachingProd, HeapRb, producer::Producer, traits::{Consumer, Observer, Split}, }; use std::{fs::File, io::BufWriter, iter, path::PathBuf, sync::Arc}; use tetanes_core::time::Duration; use tracing::{debug, error, info, trace, warn}; type SampleRb = Arc>; type SampleProducer = CachingProd; type SampleConsumer = CachingCons; /// Represents the state of the audio stream. #[derive(Debug)] #[must_use] pub enum State { /// Audio is disabled. Disabled, /// No audio output device was found or no devices found to support desired configuration. NoOutputDevice, /// Audio output stream has been started. Started, /// Audio output stream has been stopped. Stopped, } #[derive(Debug)] #[must_use] pub enum CallbackMsg { NewSamples, UpdateResampleRatio(f32), Enable(bool), Record(bool), } #[must_use] pub struct Audio { pub enabled: bool, pub sample_rate: f32, pub latency: Duration, pub buffer_size: usize, pub host: cpal::Host, output: Option, } impl std::fmt::Debug for Audio { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Audio") .field("enabled", &self.enabled) .field("sample_rate", &self.sample_rate) .field("latency", &self.latency) .field("buffer_size", &self.buffer_size) .field("output", &self.output) .finish_non_exhaustive() } } impl Audio { /// Creates a new audio mixer. /// /// # Errors /// /// Returns an error if the audio device fails to be opened. pub fn new(enabled: bool, mut sample_rate: f32, latency: Duration, buffer_size: usize) -> Self { let host = cpal::default_host(); let output = Output::create(&host, sample_rate, latency, buffer_size); if let Some(output) = &output { let desired_sample_rate = sample_rate as u32; if output.config.sample_rate != desired_sample_rate { sample_rate = output.config.sample_rate as f32; debug!( "Unable to match desired sample_rate: {desired_sample_rate}. Using {sample_rate} instead", ); } } Self { enabled, sample_rate, latency, buffer_size, host, output, } } /// Whether the audio mixer is currently enabled. pub fn enabled(&self) -> bool { self.enabled && self .output .as_ref() .and_then(|output| output.mixer.as_ref()) .is_some_and(|mixer| !mixer.paused) } /// Returns the current audio device, if any. pub fn device(&self) -> Option<&cpal::Device> { self.output.as_ref().map(|output| &output.device) } /// Set whether the audio mixer is enabled. Returns [`State`] representing the state of /// the audio stream as a result of being enabled/disabled. pub fn set_enabled(&mut self, enabled: bool) -> anyhow::Result { self.enabled = enabled; if self.enabled { self.start() } else { Ok(self.stop()) } } /// Processes generated audio samples. pub fn process(&mut self, samples: &[f32]) { if let Some(mixer) = &mut self .output .as_mut() .and_then(|output| output.mixer.as_mut()) { mixer.process(samples); } } /// Returns the number of audio channels. #[must_use] pub fn channels(&self) -> u16 { self.output .as_ref() .map_or(0, |output| output.config.channels) } /// Returns the `Duration` of audio queued for playback. #[must_use] pub fn queued_time(&self) -> Duration { self.output .as_ref() .and_then(|output| output.mixer.as_ref()) .map_or(Duration::default(), |mixer| { let queued_seconds = mixer.producer.occupied_len() as f32 / self.sample_rate / mixer.channels as f32; Duration::from_secs_f32(queued_seconds) }) } /// Pause or resume the audio output stream. If `paused` is false and the stream is not started /// yet, it will be started. pub fn pause(&mut self, paused: bool) { if let Some(mixer) = &mut self .output .as_mut() .and_then(|output| output.mixer.as_mut()) { mixer.pause(paused); } } /// Recreate audio output device. fn recreate_output(&mut self) -> anyhow::Result { let _ = self.stop(); self.output = Output::create(&self.host, self.sample_rate, self.latency, self.buffer_size); self.start() } /// Set the output sample rate that the audio device uses. Requires restarting the audio stream /// and so may fail. pub fn set_sample_rate(&mut self, sample_rate: f32) -> anyhow::Result { self.sample_rate = sample_rate; self.recreate_output() } /// Set the buffer size used by the audio device for playback. Requires restarting the audio /// stream and so may fail. pub fn set_buffer_size(&mut self, buffer_size: usize) -> anyhow::Result { self.buffer_size = buffer_size; self.recreate_output() } /// Set the latency used by the audio device for playback. Requires restarting the audio /// stream and so may fail. pub fn set_latency(&mut self, latency: Duration) -> anyhow::Result { self.latency = latency; self.recreate_output() } /// Whether the mixer is currently recording samples to a file. pub fn is_recording(&self) -> bool { self.output .as_ref() .and_then(|output| output.mixer.as_ref()) .is_some_and(|mixer| mixer.recording.is_some()) } /// Start recording audio to a file. pub fn start_recording(&mut self) -> anyhow::Result<()> { if let Some(mixer) = &mut self .output .as_mut() .and_then(|output| output.mixer.as_mut()) { mixer.start_recording() } else { Ok(()) } } /// Stop recording audio to a file. pub fn stop_recording(&mut self) -> anyhow::Result> { self.output .as_mut() .and_then(|output| output.mixer.as_mut()) .map_or(Ok(None), |mixer| mixer.stop_recording()) } /// Start the audio output stream. Returns [`State`] representing the state of the audio stream. /// /// # Errors /// /// Returns an error if the audio stream could not be started. pub fn start(&mut self) -> anyhow::Result { if self.enabled { if let Some(output) = &mut self.output { output.start()?; Ok(State::Started) } else { Ok(State::NoOutputDevice) } } else { Ok(State::Disabled) } } /// Stop the audio output stream. pub fn stop(&mut self) -> State { if let Some(output) = &mut self.output { output.stop(); State::Stopped } else { State::NoOutputDevice } } /// Returns a list of available hosts for the current platform. pub fn available_hosts(&self) -> Vec { cpal::available_hosts() } /// Returns an iterator over the audio devices available to the host on the system. If no /// devices are available, `None` is returned. /// /// # Errors /// /// If the device is no longer valid (i.e. has been disconnected), an error is returned. pub fn available_devices(&self) -> anyhow::Result { Ok(self.host.devices()?) } /// Return an iterator over supported device configurations. If no devices are available, `None` is /// returned. /// /// # Errors /// /// If the device is no longer valid (i.e. has been disconnected), an error is returned. pub fn supported_configs(&self) -> Option> { self.output.as_ref().map(|output| { output .device .supported_output_configs() .context("failed to get supported configurations") }) } } #[must_use] struct Output { device: cpal::Device, config: cpal::StreamConfig, sample_format: cpal::SampleFormat, latency: Duration, mixer: Option, } impl std::fmt::Debug for Output { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Audio") .field("config", &self.config) .field("sample_format", &self.sample_format) .field("mixer", &self.mixer) .finish_non_exhaustive() } } impl Output { fn create( host: &cpal::Host, sample_rate: f32, latency: Duration, buffer_size: usize, ) -> Option { let Some(device) = host.default_output_device() else { warn!("no available audio devices found"); return None; }; debug!( "device name: {}", device .description() .as_ref() .map(|desc| desc.name()) .unwrap_or("unknown") ); let (config, sample_format) = match Self::choose_config(&device, sample_rate, buffer_size) { Ok(config) => config, Err(err) => { warn!("failed to find a matching device configuration: {err:?}"); return None; } }; Some(Self { device, config, sample_format, latency, mixer: None, }) } /// Choose the best audio configuration for the given device and sample_rate. fn choose_config( device: &cpal::Device, sample_rate: f32, buffer_size: usize, ) -> anyhow::Result<(cpal::StreamConfig, cpal::SampleFormat)> { let mut supported_configs = device.supported_output_configs()?; let desired_sample_rate = sample_rate as u32; let desired_buffer_size = buffer_size as u32; debug!("desired: sample rate: {desired_sample_rate}, buffer_size: {buffer_size}"); let chosen_config = supported_configs .find(|config| { let supports_sample_rate = config.max_sample_rate() >= desired_sample_rate && config.min_sample_rate() <= desired_sample_rate; let supports_sample_format = config.sample_format() == cpal::SampleFormat::F32; let supports_buffer_size = match config.buffer_size() { cpal::SupportedBufferSize::Range { min, max } => { (*min..=*max).contains(&desired_buffer_size) } cpal::SupportedBufferSize::Unknown => false, }; let supported = supports_sample_rate && supports_sample_format && supports_buffer_size; if supported { debug!("supported config: {config:?}",); } else { trace!("unsupported config: {config:?}",); } supported }) .or_else(|| { let config = device .supported_output_configs() .ok() .and_then(|mut c| c.next()); debug!("falling back to first supported config: {config:?}"); config }) .map(|config| { debug!("chosen config: {config:?}"); let min_sample_rate = config.min_sample_rate(); let max_sample_rate = config.max_sample_rate(); config.with_sample_rate(desired_sample_rate.clamp(min_sample_rate, max_sample_rate)) }) .ok_or_else(|| anyhow!("no supported audio configurations found"))?; let sample_format = chosen_config.sample_format(); let buffer_size = match chosen_config.buffer_size() { cpal::SupportedBufferSize::Range { min, max } => { desired_buffer_size.min(*max).max(*min) } cpal::SupportedBufferSize::Unknown => desired_buffer_size, }; let mut config = cpal::StreamConfig::from(chosen_config); config.buffer_size = cpal::BufferSize::Fixed(buffer_size); Ok((config, sample_format)) } fn start(&mut self) -> anyhow::Result<()> { if let Some(ref mixer) = self.mixer { mixer.stream.play()?; return Ok(()); } info!("starting audio stream with config: {:?}", self.config); self.mixer = Some(Mixer::start( &self.device, &self.config, self.latency, self.sample_format, )?); Ok(()) } fn stop(&mut self) { if let Some(mut mixer) = self.mixer.take() { mixer.pause(true); } } } #[must_use] pub(crate) struct Mixer { stream: cpal::Stream, paused: bool, channels: u16, sample_rate: u32, sample_latency: usize, producer: SampleProducer, processed_samples: Vec, recording: Option<(PathBuf, hound::WavWriter>)>, } impl std::fmt::Debug for Mixer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Audio") .field("paused", &self.paused) .field("channels", &self.channels) .field("sample_rate", &self.sample_rate) .field("sample_latency", &self.sample_latency) .field("queued_len", &self.producer.occupied_len()) .field("processed_len", &self.processed_samples.len()) .field("recording", &self.recording.is_some()) .finish_non_exhaustive() } } impl Mixer { fn start( device: &cpal::Device, config: &cpal::StreamConfig, latency: Duration, sample_format: cpal::SampleFormat, ) -> anyhow::Result { use cpal::SampleFormat; let channels = config.channels; let sample_rate = config.sample_rate; let sample_latency = (latency.as_secs_f32() * sample_rate as f32 * channels as f32).ceil() as usize; let processed_samples = Vec::with_capacity(2 * sample_latency); let buffer = HeapRb::::new(2 * sample_latency); let (producer, consumer) = buffer.split(); let stream = match sample_format { SampleFormat::I8 => Self::make_stream::(device, config, consumer), SampleFormat::I16 => Self::make_stream::(device, config, consumer), SampleFormat::I32 => Self::make_stream::(device, config, consumer), SampleFormat::I64 => Self::make_stream::(device, config, consumer), SampleFormat::U8 => Self::make_stream::(device, config, consumer), SampleFormat::U16 => Self::make_stream::(device, config, consumer), SampleFormat::U32 => Self::make_stream::(device, config, consumer), SampleFormat::U64 => Self::make_stream::(device, config, consumer), SampleFormat::F32 => Self::make_stream::(device, config, consumer), SampleFormat::F64 => Self::make_stream::(device, config, consumer), sample_format => Err(anyhow!("Unsupported sample format {sample_format}")), }?; stream.play()?; Ok(Self { stream, paused: false, channels, sample_rate, sample_latency, producer, processed_samples, recording: None, }) } /// Pause or resume the audio output stream. If `paused` is false and the stream is not started /// yet, it will be started. fn pause(&mut self, paused: bool) { if paused && !self.paused { let _ = self.stop_recording(); self.processed_samples.clear(); // FIXME: Currently cpal doesn't let the underyling audio device empty samples before // pausing which leads to the remaining audio playing again upon resume. The only work // around is to leave the stream playing // if let Err(err) = self.stream.pause() { // error!("failed to pause audio stream: {err:?}"); // } } else if !paused && self.paused { // if let Err(err) = self.stream.play() { // error!("failed to resume audio stream: {err:?}"); // } } self.paused = paused; } fn start_recording(&mut self) -> anyhow::Result<()> { let _ = self.stop_recording(); let path = Config::default_audio_dir() .join( chrono::Local::now() .format("recording_%Y-%m-%d_at_%H_%M_%S") .to_string(), ) .with_extension("wav"); if let Some(parent) = path.parent() && !parent.exists() { std::fs::create_dir_all(parent).with_context(|| { format!( "failed to create audio recording directory: {}", parent.display() ) })?; } let spec = hound::WavSpec { channels: self.channels, sample_rate: self.sample_rate, bits_per_sample: 32, sample_format: hound::SampleFormat::Float, }; let writer = hound::WavWriter::create(&path, spec).context("failed to create audio recording")?; self.recording = Some((path, writer)); Ok(()) } fn stop_recording(&mut self) -> anyhow::Result> { if let Some((path, mut recording)) = self.recording.take() { match recording.flush() { Ok(_) => Ok(Some(path)), Err(err) => Err(anyhow!("failed to flush audio recording: {err:?}")), } } else { Ok(None) } } fn make_stream( device: &cpal::Device, config: &cpal::StreamConfig, mut consumer: SampleConsumer, ) -> anyhow::Result where T: cpal::SizedSample + cpal::FromSample, { Ok(device.build_output_stream( config, move |out: &mut [T], _info| { for (sample, value) in out .iter_mut() .zip(consumer.pop_iter().chain(iter::repeat(0.0))) { *sample = T::from_sample(value); } }, |err| error!("an error occurred on stream: {err}"), None, )?) } fn process(&mut self, samples: &[f32]) { if self.paused { return; } for sample in samples { for _ in 0..self.channels { self.processed_samples.push(*sample); } if let Some((_, recording)) = &mut self.recording { // TODO: push slice to recording thread if let Err(err) = recording.write_sample(*sample) { error!("failed to write audio sample: {err:?}"); let _ = self.stop_recording(); } } } let processed_len = self.processed_samples.len(); let len = self.producer.vacant_len().min(processed_len); let queued_len = self .producer .push_iter(&mut self.processed_samples.drain(..len)); trace!( "processed: {processed_len}, queued: {queued_len}, buffer len: {}", self.producer.occupied_len() ); } } ================================================ FILE: tetanes/src/nes/config.rs ================================================ use crate::nes::{ action::Action, input::{ActionBindings, Gamepads, Input}, renderer::shader::Shader, rom::HOMEBREW_ROMS, }; use anyhow::Context; use egui::ahash::HashSet; use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeMap, VecDeque}, path::PathBuf, }; use tetanes_core::{ action::Action as DeckAction, common::NesRegion, control_deck::Config as DeckConfig, fs, input::Player, ppu, time::Duration, }; use tracing::{error, info}; use uuid::Uuid; /// The maximum number of recent ROM entries to keep. const MAX_RECENT_ROMS: usize = 10; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[must_use] #[serde(default)] // Ensures new fields don't break existing configurations pub struct AudioConfig { pub enabled: bool, pub buffer_size: usize, pub latency: Duration, } impl Default for AudioConfig { fn default() -> Self { Self { enabled: true, buffer_size: if cfg!(target_arch = "wasm32") { // Too low a value for wasm causes audio underruns in Chrome 2048 } else { 512 }, latency: if cfg!(target_arch = "wasm32") { Duration::from_millis(80) } else { Duration::from_millis(50) }, } } } #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] #[must_use] #[serde(default)] // Ensures new fields don't break existing configurations pub struct EmulationConfig { pub auto_load: bool, pub auto_save: bool, pub auto_save_interval: Duration, pub rewind: bool, pub rewind_seconds: u32, pub rewind_interval: u32, pub run_ahead: usize, pub save_slot: u8, pub speed: f32, pub threaded: bool, } impl Default for EmulationConfig { fn default() -> Self { Self { auto_load: true, auto_save: true, auto_save_interval: Duration::from_secs(5), rewind: true, rewind_seconds: 30, rewind_interval: 2, // WASM struggles to run fast enough with run-ahead and low latency is not needed in // debug builds. run_ahead: if cfg!(any(debug_assertions, target_arch = "wasm32")) { 0 } else { 1 }, save_slot: 1, speed: 1.0, threaded: true, } } } /// Recently loaded ROM. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[must_use] pub enum RecentRom { /// Path to a local file. Path(PathBuf), /// Included Homebrew title. Homebrew { name: String }, } impl RecentRom { /// Return the name or title of this ROM. pub fn name(&self) -> &str { match self { RecentRom::Path(path) => fs::filename(path).split('.').next().unwrap_or("??"), RecentRom::Homebrew { name } => name, } } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[must_use] #[serde(default)] // Ensures new fields don't break existing configurations pub struct RendererConfig { pub fullscreen: bool, pub always_on_top: bool, pub hide_overscan: bool, pub scale: f32, pub zoom: f32, pub recent_roms: VecDeque, pub roms_path: Option, pub show_perf_stats: bool, pub show_messages: bool, pub show_menubar: bool, pub embed_viewports: bool, pub dark_theme: bool, pub shader: Shader, #[serde(default)] pub show_updates: bool, } impl Default for RendererConfig { fn default() -> Self { Self { fullscreen: false, always_on_top: false, hide_overscan: true, scale: 3.0, zoom: 1.0, recent_roms: VecDeque::default(), roms_path: std::env::current_dir().ok(), show_perf_stats: false, show_messages: true, show_menubar: true, embed_viewports: false, dark_theme: true, shader: Shader::default(), show_updates: true, } } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[must_use] #[serde(default)] // Ensures new fields don't break existing configurations pub struct InputConfig { pub action_bindings: Vec, pub gamepad_assignments: [(Player, Option); 4], #[serde(skip)] pub shortcuts: BTreeMap, #[serde(skip)] pub joypads: [BTreeMap; 4], } impl Default for InputConfig { fn default() -> Self { let shortcuts = ActionBindings::default_shortcuts(); let joypads = [Player::One, Player::Two, Player::Three, Player::Four] .map(ActionBindings::default_player_bindings); let action_bindings = shortcuts .iter() .chain(joypads.iter().flatten()) .map(|(_, bindings)| *bindings) .collect(); Self { action_bindings, shortcuts, joypads, gamepad_assignments: std::array::from_fn(|i| { (Player::try_from(i).expect("valid player assignment"), None) }), } } } impl InputConfig { pub fn set_binding(&mut self, action: Action, input: Input, binding: usize) { // Clear existing binding, if any self.clear_binding(input); match self .action_bindings .iter_mut() .find(|bind| bind.action == action) { Some(bind) => bind.bindings[binding] = Some(input), None => { let mut bindings = [None; 3]; bindings[binding] = Some(input); self.action_bindings .push(ActionBindings { action, bindings }); } } let keybinds = if let Action::Deck(DeckAction::Joypad((player, _))) = action { &mut self.joypads[player as usize] } else { &mut self.shortcuts }; keybinds .entry(action) .and_modify(|bind| bind.bindings[binding] = Some(input)) .or_insert_with(|| { let mut bindings = [None; 3]; bindings[binding] = Some(input); ActionBindings { action, bindings } }); } pub fn clear_binding(&mut self, input: Input) { for bind in &mut self.action_bindings { if let Some((binding, existing_input)) = bind .bindings .iter_mut() .enumerate() .find(|(_, i)| **i == Some(input)) { let keybinds = if let Action::Deck(DeckAction::Joypad((player, _))) = bind.action { &mut self.joypads[player as usize] } else { &mut self.shortcuts }; keybinds .entry(bind.action) .and_modify(|bind| bind.bindings[binding] = None); *existing_input = None; } } } pub fn update_gamepad_assignments(&mut self, gamepads: &Gamepads) { let assigned = self .gamepad_assignments .iter() .filter_map(|(_, uuid)| *uuid) .collect::>(); let mut available = gamepads.connected_uuids(); for (_, assigned_uuid) in &mut self.gamepad_assignments { match assigned_uuid { Some(uuid) => { if !gamepads.is_connected(uuid) { *assigned_uuid = None; } } None => { if let Some(uuid) = available.next() && !assigned.contains(uuid) { *assigned_uuid = Some(*uuid); } } } } } pub fn next_gamepad_unassigned(&mut self) -> Option { self.gamepad_assignments .iter() .find(|(_, u)| u.is_none()) .map(|(player, _)| *player) } pub const fn gamepad_assigned_to(&self, player: Player) -> Option { self.gamepad_assignments[player as usize].1 } pub fn gamepad_assignment(&self, uuid: &Uuid) -> Option { self.gamepad_assignments .iter() .find(|(_, u)| u.as_ref().is_some_and(|u| u == uuid)) .map(|(player, _)| *player) } pub const fn assign_gamepad(&mut self, player: Player, uuid: Uuid) { self.gamepad_assignments[player as usize].1 = Some(uuid); } pub fn unassign_gamepad(&mut self, player: Player) -> Option { std::mem::take(&mut self.gamepad_assignments[player as usize].1) } pub fn unassign_gamepad_name(&mut self, uuid: &Uuid) -> Option { if let Some((player, uuid)) = self .gamepad_assignments .iter_mut() .find(|(_, u)| u.as_ref() == Some(uuid)) { *uuid = None; Some(*player) } else { None } } } /// NES emulation configuration settings. /// /// # Config JSON /// /// Configuration for `TetaNES` is stored (by default) in `~/.config/tetanes/config.json` /// with defaults that can be customized in the `TetaNES` config menu. #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[must_use] #[serde(default)] // Ensures new fields don't break existing configurations pub struct Config { pub deck: DeckConfig, pub emulation: EmulationConfig, pub audio: AudioConfig, pub renderer: RendererConfig, pub input: InputConfig, } impl Config { pub const SAVE_DIR: &'static str = "save"; pub const SAVE_EXTENSION: &'static str = "sav"; pub const WINDOW_TITLE: &'static str = "TetaNES"; pub const FILENAME: &'static str = "config.json"; #[must_use] pub fn default_config_dir() -> PathBuf { dirs::config_local_dir().map_or_else( || PathBuf::from("config"), |dir| dir.join(DeckConfig::BASE_DIR), ) } #[must_use] pub fn default_data_dir() -> PathBuf { dirs::data_local_dir().map_or_else( || PathBuf::from("data"), |dir| dir.join(DeckConfig::BASE_DIR), ) } #[must_use] pub fn default_picture_dir() -> PathBuf { dirs::picture_dir().map_or_else( || PathBuf::from("pictures"), |dir| dir.join(DeckConfig::BASE_DIR), ) } #[must_use] pub fn default_audio_dir() -> PathBuf { dirs::audio_dir().map_or_else( || PathBuf::from("music"), |dir| dir.join(DeckConfig::BASE_DIR), ) } #[must_use] pub fn config_path() -> PathBuf { Self::default_config_dir().join(Self::FILENAME) } #[must_use] pub fn save_path(name: &str, slot: u8) -> PathBuf { Self::default_data_dir() .join(Self::SAVE_DIR) .join(name) .join(format!("slot-{slot}")) .with_extension(Self::SAVE_EXTENSION) } pub fn reset(&mut self) { *self = Self::default(); } pub fn save(&self) -> anyhow::Result<()> { let path = Config::config_path(); let data = serde_json::to_vec_pretty(&self).context("failed to serialize config")?; fs::save_raw(path, &data).context("failed to save config")?; Ok(()) } pub fn load(path: Option) -> Self { let path = path.unwrap_or_else(Config::config_path); let mut config = if fs::exists(&path) { info!("Loading saved configuration"); fs::load_raw(&path) .context("failed to load config") .and_then(|data| Ok(serde_json::from_slice::(&data)?)) .with_context(|| format!("failed to parse {path:?}")) .unwrap_or_else(|err| { error!("Invalid config: {path:?}, reverting to defaults. Error: {err:?}",); Self::default() }) } else { info!("Loading default configuration"); Self::default() }; for binding in &config.input.action_bindings { if let Action::Deck(DeckAction::Joypad((player, _))) = binding.action { config.input.joypads[player as usize].insert(binding.action, *binding); } else { config.input.shortcuts.insert(binding.action, *binding); } } // Only keep recent Homebrew ROMs that are still available. let homebrew_roms = HOMEBREW_ROMS .iter() .map(|rom| rom.name) .collect::>(); config.renderer.recent_roms.retain(|rom| match rom { RecentRom::Path(_) => true, RecentRom::Homebrew { name } => homebrew_roms.contains(name.as_str()), }); config } pub fn increment_speed(&mut self) -> f32 { self.emulation.speed = self.next_increment_speed(); self.emulation.speed } pub fn next_increment_speed(&self) -> f32 { if self.emulation.speed <= 1.75 { self.emulation.speed + 0.25 } else { self.emulation.speed } } pub fn decrement_speed(&mut self) -> f32 { self.emulation.speed = self.next_decrement_speed(); self.emulation.speed } pub fn next_decrement_speed(&self) -> f32 { if self.emulation.speed >= 0.50 { self.emulation.speed - 0.25 } else { self.emulation.speed } } pub fn increment_scale(&mut self) -> f32 { self.renderer.scale = self.next_increment_scale(); self.renderer.scale } pub fn next_increment_scale(&self) -> f32 { if self.renderer.scale <= 4.0 { self.renderer.scale + 1.0 } else { self.renderer.scale } } pub fn decrement_scale(&mut self) -> f32 { self.renderer.scale = self.next_decrement_scale(); self.renderer.scale } pub fn next_decrement_scale(&self) -> f32 { if self.renderer.scale >= 2.0 { self.renderer.scale - 1.0 } else { self.renderer.scale } } #[must_use] pub fn window_size(&self, aspect_ratio: f32) -> egui::Vec2 { self.window_size_for_scale(aspect_ratio, self.renderer.scale) } #[must_use] pub fn window_size_for_scale(&self, aspect_ratio: f32, scale: f32) -> egui::Vec2 { let texture_size = self.texture_size(); egui::Vec2::new( (scale * aspect_ratio * texture_size.x).ceil(), (scale * texture_size.y).ceil(), ) } #[must_use] pub const fn texture_size(&self) -> egui::Vec2 { let width = ppu::size::WIDTH; let height = if self.renderer.hide_overscan { ppu::size::HEIGHT - 16 } else { ppu::size::HEIGHT }; egui::Vec2::new(width as f32, height as f32) } pub fn shortcut(&self, action: impl Into) -> String { let action = action.into(); self.input .shortcuts .get(&action) .or_else(|| self.input.joypads[0].get(&action)) .and_then(|bind| bind.bindings[0]) .map(Input::fmt) .unwrap_or_default() } pub fn action_input(&self, action: impl Into) -> Option { let action = action.into(); self.input .shortcuts .get(&action) .or_else(|| { self.input .joypads .iter() .map(|bind| bind.get(&action)) .next() .flatten() }) .and_then(|bind| bind.bindings[0]) } // Add a recently loaded ROM. pub fn add_recent_rom(&mut self, rom: RecentRom) { self.renderer.recent_roms.retain(|r| r != &rom); self.renderer.recent_roms.push_front(rom); if self.renderer.recent_roms.len() > MAX_RECENT_ROMS { self.renderer.recent_roms.pop_back(); } } } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum FrameRate { X50, X59, #[default] X60, } impl FrameRate { pub const MIN: Self = Self::X50; pub const MAX: Self = Self::X60; pub fn duration(&self) -> Duration { Duration::from_secs_f32(f32::from(self).recip()) } } impl From for u32 { fn from(frame_rate: FrameRate) -> Self { match frame_rate { FrameRate::X50 => 50, FrameRate::X59 => 59, FrameRate::X60 => 60, } } } impl From<&FrameRate> for u32 { fn from(frame_rate: &FrameRate) -> Self { Self::from(*frame_rate) } } impl From for f32 { fn from(frame_rate: FrameRate) -> Self { u32::from(frame_rate) as f32 } } impl From<&FrameRate> for f32 { fn from(frame_rate: &FrameRate) -> Self { Self::from(*frame_rate) } } impl From for FrameRate { fn from(region: NesRegion) -> Self { match region { NesRegion::Auto | NesRegion::Ntsc => Self::X60, NesRegion::Pal => Self::X50, NesRegion::Dendy => Self::X59, } } } impl From<&NesRegion> for FrameRate { fn from(region: &NesRegion) -> Self { Self::from(*region) } } impl AsRef for FrameRate { fn as_ref(&self) -> &str { match self { Self::X50 => "50 Hz", Self::X59 => "59 Hz", Self::X60 => "60 Hz", } } } impl std::fmt::Display for FrameRate { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_ref()) } } ================================================ FILE: tetanes/src/nes/emulation/replay.rs ================================================ use crate::nes::{config::Config, event::EmulationEvent}; use chrono::Local; use serde::{Deserialize, Serialize}; use std::{ cmp::Ordering, io::Read, path::{Path, PathBuf}, }; use tetanes_core::{ cpu::Cpu, fs, input::{JoypadBtn, Player}, }; use tracing::warn; use winit::event::ElementState; #[derive(Debug, Serialize, Deserialize)] pub struct State((Cpu, Vec)); #[derive(Debug, Serialize, Deserialize)] pub enum ReplayEvent { Joypad((Player, JoypadBtn, ElementState)), ZapperAim((u16, u16)), ZapperTrigger, } impl From for EmulationEvent { fn from(event: ReplayEvent) -> Self { match event { ReplayEvent::Joypad(state) => Self::Joypad(state), ReplayEvent::ZapperAim(pos) => Self::ZapperAim(pos), ReplayEvent::ZapperTrigger => Self::ZapperTrigger, } } } impl TryFrom for ReplayEvent { type Error = anyhow::Error; fn try_from(event: EmulationEvent) -> Result { Ok(match event { EmulationEvent::Joypad(state) => Self::Joypad(state), EmulationEvent::ZapperAim(pos) => Self::ZapperAim(pos), EmulationEvent::ZapperTrigger => Self::ZapperTrigger, _ => return Err(anyhow::anyhow!("invalid replay event: {event:?}")), }) } } #[derive(Debug, Serialize, Deserialize)] #[must_use] pub struct ReplayFrame { pub frame: u32, pub event: ReplayEvent, } #[derive(Default, Debug)] #[must_use] pub struct Record { pub start: Option, pub events: Vec, } impl Record { pub fn new() -> Self { Self::default() } pub fn start(&mut self, cpu: Cpu) { self.start = Some(cpu); self.events.clear(); } pub fn stop(&mut self, name: &str) -> anyhow::Result> { self.save(name) } pub fn push(&mut self, frame: u32, event: EmulationEvent) { if self.start.is_some() && let Ok(event) = ReplayEvent::try_from(event) { self.events.push(ReplayFrame { frame, event }); } } /// Saves the replay recording out to a file. pub fn save(&mut self, name: &str) -> anyhow::Result> { let Some(start) = self.start.take() else { return Ok(None); }; if self.events.is_empty() { tracing::debug!("not saving - no replay events"); return Ok(None); } let replay_path = Config::default_data_dir() .join( Local::now() .format(&format!("tetanes_replay_{name}_%Y-%m-%d_%H.%M.%S")) .to_string(), ) .with_extension("replay"); let events = std::mem::take(&mut self.events); fs::save(&replay_path, &State((start, events)))?; Ok(Some(replay_path)) } } #[derive(Default, Debug)] #[must_use] pub struct Replay { pub events: Vec, } impl Replay { pub fn new() -> Self { Self::default() } /// Loads a replay recording file. pub fn load_path(&mut self, path: impl AsRef) -> anyhow::Result { let path = path.as_ref(); let State((cpu, mut events)) = fs::load(path)?; events.reverse(); // So we can pop off the end self.events = events; Ok(cpu) } /// Loads a replay from a reader. pub fn load(&mut self, mut replay: impl Read) -> anyhow::Result { let mut events = Vec::new(); replay.read_to_end(&mut events)?; let State((cpu, mut events)) = fs::load_bytes(&events)?; events.reverse(); // So we can pop off the end self.events = events; Ok(cpu) } pub fn next(&mut self, frame: u32) -> Option { if let Some(event) = self.events.last() { match event.frame.cmp(&frame) { Ordering::Less | Ordering::Equal => { if event.frame < frame { warn!("out of order replay event: {} < {frame}", event.frame); } return self.events.pop().map(|event| event.event).map(Into::into); } Ordering::Greater => (), } } None } } ================================================ FILE: tetanes/src/nes/emulation/rewind.rs ================================================ use crate::nes::{emulation::State, renderer::gui::MessageType}; use tetanes_core::{ cpu::Cpu, fs::{Error, Result}, ppu::frame::Buffer, }; use tracing::error; #[derive(Default, Debug, Clone)] #[must_use] pub struct Frame { pub buffer: Buffer, pub state: Vec, } #[derive(Default, Debug)] #[must_use] pub struct Rewind { pub enabled: bool, pub interval_counter: usize, pub index: usize, pub count: usize, pub interval: usize, pub seconds: usize, pub frames: Vec>, } impl Rewind { const TARGET_FPS: usize = 60; pub fn new(enabled: bool, seconds: u32, interval: u32) -> Self { let interval = interval as usize; let seconds = seconds as usize; Self { enabled, interval_counter: 0, index: 0, count: 0, interval, seconds, frames: vec![None; Self::frame_size(seconds, interval)], } } const fn frame_size(seconds: usize, interval: usize) -> usize { Self::TARGET_FPS * seconds / interval } pub fn set_enabled(&mut self, enabled: bool) { self.enabled = enabled; if !enabled { self.clear(); } } pub fn set_seconds(&mut self, seconds: u32) { self.seconds = seconds as usize; self.frames .resize(Self::frame_size(self.seconds, self.interval), None); } pub fn set_interval(&mut self, interval: u32) { self.interval = interval as usize; self.frames .resize(Self::frame_size(self.seconds, self.interval), None); } pub fn push(&mut self, cpu: &Cpu) -> Result<()> { if !self.enabled { return Ok(()); } self.interval_counter += 1; if self.interval_counter >= self.interval { self.interval_counter = 0; let config = bincode::config::legacy(); let state = bincode::serde::encode_to_vec(cpu, config) .map_err(|err| Error::SerializationFailed(err.to_string()))?; self.frames[self.index] = Some(Frame { buffer: cpu.bus.ppu.frame.buffer.clone(), state, }); self.count += 1; self.index += 1; if self.index >= self.frames.len() { self.index = 0; } } Ok(()) } pub fn pop(&mut self) -> Option { if !self.enabled { return None; } if self.count > 0 { self.count -= 1; self.index -= 1; if self.index == 0 { self.index = self.frames.len() - 1; } let frame = self.frames[self.index].take()?; let config = bincode::config::legacy(); bincode::serde::decode_from_slice::(&frame.state, config) .map(|(mut cpu, _)| { cpu.bus.input.clear(); // Discard inputs while rewinding cpu.bus.ppu.frame.buffer = frame.buffer; cpu }) .map_err(|err| error!("Failed to deserialize CPU state: {err:?}")) .ok() } else { None } } pub fn clear(&mut self) { self.interval_counter = 0; self.index = 0; self.count = 0; self.frames.fill(None); } } impl State { pub fn rewind_disabled(&mut self) { self.add_message( MessageType::Warn, "Rewind disabled. You can enable it in the Preferences menu.", ); } pub fn instant_rewind(&mut self) { if !self.rewind.enabled { return self.rewind_disabled(); } // ~2 seconds worth of frames @ 60 FPS let mut rewind_frames = 120 / self.rewind.interval; while let Some(mut cpu) = self.rewind.pop() { cpu.bus.input.clear(); // Discard inputs while rewinding self.control_deck.load_cpu(cpu); rewind_frames -= 1; if rewind_frames == 0 { break; } } } } ================================================ FILE: tetanes/src/nes/emulation.rs ================================================ use crate::{ nes::{ RunState, action::DebugStep, audio::{Audio, State as AudioState}, config::{Config, FrameRate}, emulation::{replay::Record, rewind::Rewind}, event::{ConfigEvent, EmulationEvent, NesEvent, NesEventProxy, RendererEvent, UiEvent}, renderer::{FrameRecycle, gui::MessageType}, }, thread, }; use anyhow::{Context, anyhow}; use chrono::Local; use crossbeam::channel; use egui::ViewportId; use replay::Replay; use std::{ collections::VecDeque, io::{self, Read}, path::{Path, PathBuf}, thread::JoinHandle, }; use tetanes_core::{ apu::Apu, common::{NesRegion, Regional, Reset, ResetKind}, control_deck::{self, ControlDeck, LoadedRom}, cpu::Cpu, ppu, time::{Duration, Instant}, video::Frame, }; use thingbuf::mpsc::{blocking::Sender as BufSender, errors::TrySendError}; use tracing::{debug, error, trace}; use winit::event::ElementState; pub mod replay; pub mod rewind; #[derive(Debug, Copy, Clone, PartialEq)] #[must_use] pub struct FrameStats { pub timestamp: Instant, pub fps: f32, pub fps_min: f32, pub frame_time: f32, pub frame_time_max: f32, pub frame_count: usize, } impl Default for FrameStats { fn default() -> Self { Self { timestamp: Instant::now(), fps: 0.0, fps_min: 0.0, frame_time: 0.0, frame_time_max: 0.0, frame_count: 0, } } } impl FrameStats { pub fn new() -> Self { Self::default() } } #[derive(Debug)] #[must_use] pub struct FrameTimeDiag { frame_count: usize, history: VecDeque, sum: f32, avg: f32, last_update: Instant, } impl FrameTimeDiag { const MAX_HISTORY: usize = 120; const UPDATE_INTERVAL: Duration = Duration::from_millis(300); fn new() -> Self { Self { frame_count: 0, history: VecDeque::with_capacity(Self::MAX_HISTORY), sum: 0.0, avg: 1.0 / 60.0, last_update: Instant::now(), } } fn push(&mut self, frame_time: f32) { self.frame_count += 1; // Ignore the first few frames to allow the average to stabilize if frame_time.is_finite() && self.frame_count >= 10 { if self.history.len() >= Self::MAX_HISTORY && let Some(oldest) = self.history.pop_front() { self.sum -= oldest; } self.sum += frame_time; self.history.push_back(frame_time); } } fn avg(&mut self) -> f32 { if !self.history.is_empty() { let now = Instant::now(); if now > self.last_update + Self::UPDATE_INTERVAL { self.last_update = now; self.avg = self.sum / self.history.len() as f32; } } self.avg } fn history(&self) -> impl Iterator { self.history.iter() } fn reset(&mut self) { self.frame_count = 0; self.history.clear(); self.sum = 0.0; self.avg = 1.0 / 60.0; self.last_update = Instant::now(); } } fn shutdown(tx: &NesEventProxy, err: impl std::fmt::Display) { error!("{err}"); tx.event(UiEvent::Terminate); } #[derive(Debug)] #[must_use] enum Threads { Single(Box), Multi(Multi), } #[derive(Debug)] #[must_use] struct Single { state: State, } #[derive(Debug)] #[must_use] struct Multi { tx: channel::Sender, handle: JoinHandle<()>, } impl Multi { fn spawn( proxy_tx: NesEventProxy, frame_tx: BufSender, cfg: &Config, ) -> anyhow::Result { let (tx, rx) = channel::bounded(128); Ok(Self { tx, handle: std::thread::Builder::new() .name("emulation".into()) .spawn({ let cfg = cfg.clone(); move || Self::main(proxy_tx, rx, frame_tx, &cfg) })?, }) } fn main( tx: NesEventProxy, rx: channel::Receiver, frame_tx: BufSender, cfg: &Config, ) { debug!("emulation thread started"); let mut state = State::new(tx, frame_tx, cfg); // Has to be created on the thread, since loop { while let Ok(event) = rx.try_recv() { state.on_event(&event); } state.try_clock_frame(); } } } #[derive(Debug)] #[must_use] pub struct Emulation { threads: Threads, } impl Emulation { /// Initializes the renderer in a platform-agnostic way. pub fn new( tx: NesEventProxy, frame_tx: BufSender, cfg: &Config, ) -> anyhow::Result { let threaded = cfg.emulation.threaded && std::thread::available_parallelism().is_ok_and(|count| count.get() > 1); let backend = if threaded { Threads::Multi(Multi::spawn(tx, frame_tx, cfg)?) } else { Threads::Single(Box::new(Single { state: State::new(tx, frame_tx, cfg), })) }; Ok(Self { threads: backend }) } /// Handle event. pub fn on_event(&mut self, event: &NesEvent) { match &mut self.threads { Threads::Single(single) => single.state.on_event(event), Threads::Multi(Multi { tx, handle }) => { handle.thread().unpark(); if let Err(err) = tx.try_send(event.clone()) { error!("failed to send emulation event: {event:?}. {err:?}"); } } } } pub fn try_clock_frame(&mut self) { match &mut self.threads { Threads::Single(single) => single.state.try_clock_frame(), // Multi-threaded emulation handles it's own clock timing and redraw requests Threads::Multi(Multi { handle, .. }) => handle.thread().unpark(), } } pub fn terminate(&mut self) { match &mut self.threads { Threads::Single(_) => (), Threads::Multi(Multi { tx, handle }) => { handle.thread().unpark(); if let Err(err) = tx.try_send(NesEvent::Ui(UiEvent::Terminate)) { error!("failed to send termination event. {err:?}"); } } } } } #[derive(Debug)] #[must_use] pub struct State { tx: NesEventProxy, control_deck: ControlDeck, audio: Audio, frame_tx: BufSender, frame_latency: usize, target_frame_duration: Duration, last_clock_time: Instant, clock_time_accumulator: f32, last_frame_time: Instant, frame_time_diag: FrameTimeDiag, run_state: RunState, threaded: bool, rewinding: bool, rewind: Rewind, record: Record, replay: Replay, save_slot: u8, auto_save: bool, auto_save_interval: Duration, last_auto_save: Instant, auto_load: bool, speed: f32, run_ahead: usize, show_frame_stats: bool, } impl Drop for State { fn drop(&mut self) { self.unload_rom(); } } impl State { fn new(tx: NesEventProxy, frame_tx: BufSender, cfg: &Config) -> Self { let mut control_deck = ControlDeck::with_config(cfg.deck.clone()); let audio = Audio::new( cfg.audio.enabled, Apu::DEFAULT_SAMPLE_RATE, cfg.audio.latency, cfg.audio.buffer_size, ); if cfg.audio.enabled && audio.device().is_none() { tx.event(ConfigEvent::AudioEnabled(false)); tx.event(UiEvent::Message(( MessageType::Warn, "No audio device found.".into(), ))); } if Apu::DEFAULT_SAMPLE_RATE != audio.sample_rate { control_deck.set_sample_rate(audio.sample_rate); } let rewind = Rewind::new( cfg.emulation.rewind, cfg.emulation.rewind_seconds, cfg.emulation.rewind_interval, ); let target_frame_duration = FrameRate::from(cfg.deck.region).duration(); let mut state = Self { tx, control_deck, audio, frame_tx, frame_latency: 1, target_frame_duration, last_clock_time: Instant::now(), clock_time_accumulator: 0.0, last_frame_time: Instant::now(), frame_time_diag: FrameTimeDiag::new(), run_state: RunState::AutoPaused, threaded: cfg.emulation.threaded && std::thread::available_parallelism().is_ok_and(|count| count.get() > 1), rewinding: false, rewind, record: Record::new(), replay: Replay::new(), save_slot: cfg.emulation.save_slot, auto_save: cfg.emulation.auto_save, auto_save_interval: cfg.emulation.auto_save_interval, last_auto_save: Instant::now(), auto_load: cfg.emulation.auto_load, speed: cfg.emulation.speed, run_ahead: cfg.emulation.run_ahead, show_frame_stats: false, }; state.update_region(cfg.deck.region); state } pub(crate) fn add_message(&mut self, ty: MessageType, msg: S) { self.tx.event(UiEvent::Message((ty, msg.to_string()))); } fn write_deck( &mut self, writer: impl FnOnce(&mut ControlDeck) -> control_deck::Result, ) -> Option { writer(&mut self.control_deck) .map_err(|err| self.on_error(err)) .ok() } fn on_error(&mut self, err: impl Into) { use tetanes_core::mem::Read; let err = err.into(); error!("Emulation error: {err:?}"); if self.control_deck.cpu_corrupted() { let cpu = self.control_deck.cpu(); let opcode = cpu.peek(cpu.pc.wrapping_sub(1)); self.tx.event(EmulationEvent::CpuCorrupted { instr: Cpu::INSTR_REF[usize::from(opcode)], }); } else { self.add_message(MessageType::Error, err); } } /// Handle event. fn on_event(&mut self, event: &NesEvent) { match event { NesEvent::Ui(UiEvent::Terminate) => { self.unload_rom(); debug!("emulation stopped"); } NesEvent::Emulation(event) => self.on_emulation_event(event), NesEvent::Config(event) => self.on_config_event(event), _ => (), } } /// Handle emulation event. fn on_emulation_event(&mut self, event: &EmulationEvent) { match event { EmulationEvent::AddDebugger(debugger) => { self.control_deck.add_debugger(debugger.clone()); } EmulationEvent::RemoveDebugger(debugger) => { self.control_deck.remove_debugger(debugger.clone()); } EmulationEvent::AudioRecord(recording) => { if self.control_deck.is_running() { self.audio_record(*recording); } } EmulationEvent::CpuCorrupted { .. } => (), // Ignore, as only this module emits this // event EmulationEvent::DebugStep(step) => { if self.control_deck.is_running() { match step { DebugStep::Into => { self.write_deck(|deck| deck.clock_instr()); self.send_frame(); } DebugStep::Out => { // TODO: track stack frames list on jsr, irq, brk // while stack frame == previous stack frame, clock_instr, send_frame self.send_frame(); } DebugStep::Over => { // TODO: track stack frames list on jsr, irq, brk // while stack frame != previous stack frame, clock_instr, send_frame self.send_frame(); } DebugStep::Scanline => { if self.write_deck(|deck| deck.clock_scanline()).is_some() { self.send_frame(); } } DebugStep::Frame => { if self.write_deck(|deck| deck.clock_frame()).is_some() { self.send_frame(); } } } } } EmulationEvent::InstantRewind => { if self.control_deck.is_running() { self.instant_rewind(); } } EmulationEvent::Joypad((player, button, state)) => { if self.control_deck.is_running() { let pressed = *state == ElementState::Pressed; let joypad = self.control_deck.joypad_mut(*player); joypad.set_button(*button, pressed); self.record .push(self.control_deck.frame_number(), event.clone()); } } EmulationEvent::LoadReplay((name, replay)) => { if self.control_deck.is_running() { self.load_replay(name, &mut io::Cursor::new(replay)); } } EmulationEvent::LoadReplayPath(path) => { if self.control_deck.is_running() { self.load_replay_path(path); } } EmulationEvent::LoadRom((name, rom)) => { self.load_rom(name, &mut io::Cursor::new(rom)); } EmulationEvent::LoadRomPath(path) => self.load_rom_path(path), EmulationEvent::LoadState(slot) => self.load_state(*slot), EmulationEvent::RunState(mode) => self.set_run_state(*mode), EmulationEvent::ReplayRecord(recording) => { if self.control_deck.is_running() { self.replay_record(*recording); } } EmulationEvent::Reset(kind) => { self.frame_time_diag.reset(); if self.control_deck.is_running() || self.control_deck.cpu_corrupted() { self.control_deck.reset(*kind); match kind { ResetKind::Soft => self.add_message(MessageType::Info, "Reset"), ResetKind::Hard => self.add_message(MessageType::Info, "Power Cycled"), } } } EmulationEvent::RequestFrame => self.send_frame(), EmulationEvent::Rewinding(rewind) => { if self.control_deck.is_running() { if self.rewind.enabled { self.rewinding = *rewind; if self.rewinding { self.add_message(MessageType::Info, "Rewinding..."); } } else { self.rewind_disabled(); } } } EmulationEvent::SaveState(slot) => self.save_state(*slot, false), EmulationEvent::ShowFrameStats(show) => { self.frame_time_diag.reset(); self.show_frame_stats = *show; } EmulationEvent::Screenshot => { if self.control_deck.is_running() { match self.save_screenshot() { Ok(filename) => { self.add_message( MessageType::Info, format!("Screenshot Saved: {}", filename.display()), ); } Err(err) => self.on_error(err), } } } EmulationEvent::UnloadRom => self.unload_rom(), EmulationEvent::ZapperAim((x, y)) => { self.control_deck.aim_zapper(*x, *y); self.record .push(self.control_deck.frame_number(), event.clone()); } EmulationEvent::ZapperTrigger => { self.control_deck.trigger_zapper(); self.record .push(self.control_deck.frame_number(), event.clone()); } } } /// Handle config event. fn on_config_event(&mut self, event: &ConfigEvent) { match event { ConfigEvent::ApuChannelEnabled((channel, enabled)) => { let prev_enabled = self.control_deck.channel_enabled(*channel); self.control_deck .set_apu_channel_enabled(*channel, *enabled); if prev_enabled != *enabled { let enabled_text = if *enabled { "Enabled" } else { "Disabled" }; self.add_message( MessageType::Info, format!("{enabled_text} APU Channel {channel:?}"), ); } } ConfigEvent::AudioBuffer(buffer_size) => { if let Err(err) = self.audio.set_buffer_size(*buffer_size) { self.on_error(err); } } ConfigEvent::AudioEnabled(enabled) => match self.audio.set_enabled(*enabled) { Ok(state) => match state { AudioState::Started => self.add_message(MessageType::Info, "Audio Enabled"), AudioState::Disabled | AudioState::Stopped => { self.add_message(MessageType::Info, "Audio Disabled") } AudioState::NoOutputDevice => (), }, Err(err) => self.on_error(err), }, ConfigEvent::AudioLatency(latency) => { if let Err(err) = self.audio.set_latency(*latency) { self.on_error(err); } } ConfigEvent::AutoLoad(enabled) => self.auto_load = *enabled, ConfigEvent::AutoSave(enabled) => self.auto_save = *enabled, ConfigEvent::AutoSaveInterval(interval) => self.auto_save_interval = *interval, ConfigEvent::ConcurrentDpad(enabled) => { self.control_deck.set_concurrent_dpad(*enabled); } ConfigEvent::EmulatePpuWarmup(enabled) => { self.control_deck.set_emulate_ppu_warmup(*enabled); } ConfigEvent::FourPlayer(four_player) => { self.control_deck.set_four_player(*four_player); } ConfigEvent::GenieCodeAdded(genie_code) => { self.control_deck .cpu_mut() .bus .add_genie_code(genie_code.clone()); } ConfigEvent::GenieCodeRemoved(code) => { self.control_deck.remove_genie_code(code); } ConfigEvent::RamState(ram_state) => { self.control_deck.set_ram_state(*ram_state); } ConfigEvent::Region(region) => { self.control_deck.set_region(*region); self.update_region(*region); } ConfigEvent::RewindEnabled(enabled) => self.rewind.set_enabled(*enabled), ConfigEvent::RewindInterval(interval) => self.rewind.set_interval(*interval), ConfigEvent::RewindSeconds(seconds) => self.rewind.set_seconds(*seconds), ConfigEvent::RunAhead(run_ahead) => self.run_ahead = *run_ahead, ConfigEvent::MapperRevisions(revs) => { self.control_deck.set_mapper_revisions(*revs); } ConfigEvent::SaveSlot(slot) => self.save_slot = *slot, ConfigEvent::Speed(speed) => { self.speed = *speed; self.control_deck.set_frame_speed(*speed); } ConfigEvent::VideoFilter(filter) => self.control_deck.set_filter(*filter), ConfigEvent::ZapperConnected(connected) => { self.control_deck.connect_zapper(*connected); } _ => (), } } fn update_frame_stats(&mut self) { if !self.show_frame_stats { return; } self.frame_time_diag .push(self.last_frame_time.elapsed().as_secs_f32()); self.last_frame_time = Instant::now(); let frame_time = self.frame_time_diag.avg(); let frame_time_max = self .frame_time_diag .history() .fold(-f32::INFINITY, |a, b| a.max(*b)); let mut fps = 1.0 / frame_time; let mut fps_min = 1.0 / frame_time_max; if !fps.is_finite() { fps = 0.0; } if !fps_min.is_finite() { fps_min = 0.0; } self.tx.event(RendererEvent::FrameStats(FrameStats { timestamp: Instant::now(), fps, fps_min, frame_time: frame_time * 1000.0, frame_time_max: frame_time_max * 1000.0, frame_count: self.frame_time_diag.frame_count, })); } fn send_frame(&mut self) { match self.frame_tx.try_send_ref() { Ok(mut frame) => self.control_deck.frame_buffer_into(&mut frame), Err(TrySendError::Full(_)) => trace!("dropped frame"), Err(_) => shutdown(&self.tx, "failed to get frame"), } } fn set_run_state(&mut self, mode: RunState) { if !self.control_deck.cpu_corrupted() { self.run_state = mode; if self.run_state.paused() { if let Some(rom) = self.control_deck.loaded_rom() && let Err(err) = self.record.stop(&rom.name) { self.on_error(err); } } else { self.last_auto_save = Instant::now(); // To avoid having a large dip in frame stats when unpausing self.last_frame_time = Instant::now(); } self.audio.pause(self.run_state.paused()); } } fn save_state(&mut self, slot: u8, auto: bool) { if let Some(rom) = self.control_deck.loaded_rom() { let data_dir = Config::save_path(&rom.name, slot); match self.control_deck.save_state(data_dir) { Ok(_) => { if !auto { self.add_message(MessageType::Info, format!("State {slot} Saved")); } } Err(err) => self.on_error(err), } } } fn load_state(&mut self, slot: u8) { if let Some(rom) = self.control_deck.loaded_rom() { let save_path = Config::save_path(&rom.name, slot); match self.control_deck.load_state(save_path) { Ok(_) => self.add_message(MessageType::Info, format!("State {slot} Loaded")), Err(control_deck::Error::NoSaveStateFound) => { self.add_message(MessageType::Warn, format!("State {slot} Not Found")); } Err(err) => { self.on_error(err); } } } } fn unload_rom(&mut self) { if let Some(rom) = self.control_deck.loaded_rom() { if self.auto_save { let save_path = Config::save_path(&rom.name, self.save_slot); if let Err(err) = self.control_deck.save_state(save_path) { self.on_error(err); } } self.replay_record(false); self.rewind.clear(); let _ = self.audio.stop(); if let Err(err) = self.control_deck.unload_rom() { self.on_error(err); } self.tx.event(RendererEvent::RomUnloaded); self.tx.event(RendererEvent::RequestRedraw { viewport_id: ViewportId::ROOT, when: Instant::now(), }); self.frame_time_diag.reset(); } } fn on_load_rom(&mut self, rom: LoadedRom) { if self.auto_load { let save_path = Config::save_path(&rom.name, self.save_slot); if let Err(err) = self.control_deck.load_state(save_path) && !matches!(err, control_deck::Error::NoSaveStateFound) { error!("failed to load state: {err:?}"); } } if let Err(err) = self.audio.start() { self.tx.event(ConfigEvent::AudioEnabled(false)); self.on_error(err); } self.tx.event(RendererEvent::RomLoaded(rom)); self.tx.event(RendererEvent::RequestRedraw { viewport_id: ViewportId::ROOT, when: Instant::now(), }); self.frame_time_diag.reset(); self.last_auto_save = Instant::now(); // To avoid having a large dip in frame stats after loading self.last_frame_time = Instant::now(); } fn load_rom_path(&mut self, path: impl AsRef) { let path = path.as_ref(); self.unload_rom(); match self.control_deck.load_rom_path(path) { Ok(rom) => self.on_load_rom(rom), Err(err) => self.on_error(err), } } fn load_rom(&mut self, name: &str, rom: &mut impl Read) { self.unload_rom(); match self.control_deck.load_rom(name, rom) { Ok(rom) => self.on_load_rom(rom), Err(err) => self.on_error(err), } } fn on_load_replay(&mut self, start: Cpu, name: impl AsRef) { self.add_message( MessageType::Info, format!("Loaded Replay Recording {:?}", name.as_ref()), ); self.control_deck.load_cpu(start); self.tx.event(RendererEvent::ReplayLoaded); self.tx.event(RendererEvent::RequestRedraw { viewport_id: ViewportId::ROOT, when: Instant::now(), }); } fn load_replay_path(&mut self, path: impl AsRef) { let path = path.as_ref(); match self.replay.load_path(path) { Ok(start) => self.on_load_replay(start, path.to_string_lossy()), Err(err) => self.on_error(err), } } fn load_replay(&mut self, name: &str, replay: &mut impl Read) { match self.replay.load(replay) { Ok(start) => self.on_load_replay(start, name), Err(err) => self.on_error(err), } } fn update_region(&mut self, region: NesRegion) { self.target_frame_duration = FrameRate::from(region).duration(); self.frame_latency = (self.audio.latency.as_secs_f32() / self.target_frame_duration.as_secs_f32()) .ceil() as usize; } fn audio_record(&mut self, recording: bool) { if self.control_deck.is_running() { if !recording && self.audio.is_recording() { match self.audio.stop_recording() { Ok(Some(filename)) => { self.add_message( MessageType::Info, format!("Saved Replay Recording {filename:?}"), ); } Err(err) => self.on_error(err), _ => (), } } else if recording && let Err(err) = self.audio.start_recording() { self.on_error(err); } } } fn replay_record(&mut self, recording: bool) { if self.control_deck.is_running() { if recording { self.record.start(self.control_deck.cpu().clone()); } else if let Some(rom) = self.control_deck.loaded_rom() { match self.record.stop(&rom.name) { Ok(Some(filename)) => { self.add_message( MessageType::Info, format!("Saved Replay Recording {filename:?}"), ); } Err(err) => self.on_error(err), _ => (), } } } } fn save_screenshot(&mut self) -> anyhow::Result { let picture_dir = Config::default_picture_dir(); let filename = picture_dir .join( Local::now() .format("screenshot_%Y-%m-%d_at_%H_%M_%S") .to_string(), ) .with_extension("png"); let image = image::ImageBuffer::, &[u8]>::from_raw( u32::from(ppu::size::WIDTH), u32::from(ppu::size::HEIGHT), self.control_deck.frame_buffer(), ) .ok_or_else(|| anyhow!("failed to create image buffer"))?; if !picture_dir.exists() { std::fs::create_dir_all(&picture_dir) .with_context(|| format!("failed to create screenshot dir: {picture_dir:?}"))?; } // TODO: provide wasm download image .save(&filename) .map(|_| filename.clone()) .with_context(|| format!("failed to save screenshot: {filename:?}")) } fn park_duration(&self) -> Option { let park_epsilon = Duration::from_millis(1); // Park if we're paused, occluded, or not running let duration = if self.run_state.paused() || !self.control_deck.is_running() { Some(self.target_frame_duration - park_epsilon) } else if self.rewinding || !self.audio.enabled() { (self.clock_time_accumulator < self.target_frame_duration.as_secs_f32()).then(|| { Duration::from_secs_f32( self.target_frame_duration.as_secs_f32() - self.clock_time_accumulator, ) .saturating_sub(park_epsilon) }) } else { (self.audio.queued_time() > self.audio.latency) // Even though we just did a comparison, audio is still being consumed so this // could underflow .then(|| self.audio.queued_time().saturating_sub(self.audio.latency)) }; duration.map(|duration| { // Parking thread is only required for Multi-threaded emulation to save CPU cycles. if self.threaded { duration } else { Duration::ZERO } }) } fn try_clock_frame(&mut self) { let last_clock_duration = self.last_clock_time.elapsed(); self.last_clock_time = Instant::now(); self.clock_time_accumulator += last_clock_duration.as_secs_f32(); if self.clock_time_accumulator > 0.02 { self.clock_time_accumulator = 0.02; } // If any frames are still pending, request a redraw if !self.frame_tx.is_empty() { self.tx.event(RendererEvent::RequestRedraw { viewport_id: ViewportId::ROOT, when: Instant::now(), }); } if let Some(park_timeout) = self.park_duration() { thread::park_timeout(park_timeout); return; } if self.rewinding { match self.rewind.pop() { Some(cpu) => { self.control_deck.load_cpu(cpu); self.send_frame(); self.update_frame_stats(); } None => self.rewinding = false, } } else { if let Some(event) = self.replay.next(self.control_deck.frame_number()) { self.on_emulation_event(&event); } let run_ahead = if self.speed > 1.0 { 0 } else { self.run_ahead }; let res = self.control_deck .clock_frame_ahead(run_ahead, |frame_buffer, audio_samples| { self.audio.process(audio_samples); match self.frame_tx.try_send_ref() { Ok(mut frame) => { frame.clear(); frame.extend_from_slice(frame_buffer); } Err(TrySendError::Full(_)) => debug!("dropped frame"), Err(_) => shutdown(&self.tx, "failed to get frame"), } }); match res { Ok(()) => { self.update_frame_stats(); if let Err(err) = self.rewind.push(self.control_deck.cpu()) { self.rewind.set_enabled(false); self.on_error(err); } if self.auto_save && self.last_auto_save.elapsed() > self.auto_save_interval { self.last_auto_save = Instant::now(); self.save_state(self.save_slot, true); } } Err(err) => self.on_error(err), } } self.clock_time_accumulator -= self.target_frame_duration.as_secs_f32(); // Request to draw this frame self.tx.event(RendererEvent::RequestRedraw { viewport_id: ViewportId::ROOT, when: Instant::now(), }); } } ================================================ FILE: tetanes/src/nes/event.rs ================================================ use crate::{ feature, nes::{ Nes, RunState, Running, State, action::{Action, Debug, DebugKind, DebugStep, Feature, Setting, Ui}, config::{Config, RecentRom}, emulation::FrameStats, input::{ActionBindings, AxisDirection, Gamepads, Input, InputBindings}, renderer::{ gui::{Menu, MessageType}, shader::Shader, }, rom::RomData, }, platform::open_file_dialog, }; use anyhow::anyhow; use egui::ViewportId; use std::{path::PathBuf, sync::Arc}; use tetanes_core::{ action::Action as DeckAction, apu::{Apu, Channel}, common::{NesRegion, ResetKind}, control_deck::{LoadedRom, MapperRevisionsConfig}, cpu::instr::InstrRef, debug::Debugger, genie::GenieCode, input::{FourPlayer, JoypadBtn, Player}, mem::RamState, ppu::Ppu, time::{Duration, Instant}, video::VideoFilter, }; use tracing::{debug, error, trace, warn}; use uuid::Uuid; use winit::{ application::ApplicationHandler, event::{DeviceEvent, DeviceId, ElementState, WindowEvent}, event_loop::{ActiveEventLoop, ControlFlow, DeviceEvents, EventLoop, EventLoopProxy}, keyboard::PhysicalKey, window::WindowId, }; #[derive(Default, Debug, Copy, Clone)] #[must_use] pub struct Response { pub consumed: bool, pub repaint: bool, } #[derive(Debug, Clone)] pub struct NesEventProxy(EventLoopProxy); impl NesEventProxy { pub fn new(event_loop: &EventLoop) -> Self { Self(event_loop.create_proxy()) } pub fn event(&self, event: impl Into) { let event = event.into(); trace!("sending event: {event:?}"); if self.0.send_event(event).is_err() { warn!("event loop closed"); } } pub const fn inner(&self) -> &EventLoopProxy { &self.0 } } #[derive(Debug, Clone)] #[must_use] pub enum NesEvent { // For some reason accesskit_winit::Event isn't Clone #[cfg(not(target_arch = "wasm32"))] AccessKit { window_id: WindowId, event: AccessKitWindowEvent, }, Config(ConfigEvent), Debug(DebugEvent), Emulation(EmulationEvent), Renderer(RendererEvent), Ui(UiEvent), } #[cfg(not(target_arch = "wasm32"))] #[derive(Debug, Clone)] pub enum AccessKitWindowEvent { InitialTreeRequested, ActionRequested(accesskit::ActionRequest), AccessibilityDeactivated, } #[cfg(not(target_arch = "wasm32"))] impl From for NesEvent { fn from(event: accesskit_winit::Event) -> Self { use accesskit_winit::WindowEvent; Self::AccessKit { window_id: event.window_id, event: match event.window_event { WindowEvent::InitialTreeRequested => AccessKitWindowEvent::InitialTreeRequested, WindowEvent::ActionRequested(request) => { AccessKitWindowEvent::ActionRequested(request) } WindowEvent::AccessibilityDeactivated => { AccessKitWindowEvent::AccessibilityDeactivated } }, } } } #[derive(Debug, Clone, PartialEq)] #[must_use] pub enum ConfigEvent { ActionBindings(Vec), ActionBindingSet((Action, Input, usize)), ActionBindingClear(Input), AlwaysOnTop(bool), ApuChannelEnabled((Channel, bool)), ApuChannelsEnabled([bool; Apu::MAX_CHANNEL_COUNT]), AudioBuffer(usize), AudioEnabled(bool), AudioLatency(Duration), AutoLoad(bool), AutoSave(bool), AutoSaveInterval(Duration), ConcurrentDpad(bool), DarkTheme(bool), EmbedViewports(bool), EmulatePpuWarmup(bool), FourPlayer(FourPlayer), Fullscreen(bool), GamepadAssign((Player, Uuid)), GamepadAssignments([(Player, Option); 4]), GamepadUnassign(Player), GenieCodeAdded(GenieCode), GenieCodeClear, GenieCodeRemoved(String), HideOverscan(bool), MapperRevisions(MapperRevisionsConfig), RamState(RamState), RecentRomsClear, Region(NesRegion), RewindEnabled(bool), RewindInterval(u32), RewindSeconds(u32), RunAhead(usize), SaveSlot(u8), Scale(f32), Shader(Shader), ShowMenubar(bool), ShowMessages(bool), ShowUpdates(bool), Speed(f32), VideoFilter(VideoFilter), ZapperConnected(bool), } impl From for NesEvent { fn from(event: ConfigEvent) -> Self { Self::Config(event) } } #[derive(Debug, Clone)] #[must_use] pub enum DebugEvent { Ppu(Box), } impl From for NesEvent { fn from(event: DebugEvent) -> Self { Self::Debug(event) } } #[derive(Debug, Clone, PartialEq)] #[must_use] pub enum EmulationEvent { AddDebugger(Debugger), RemoveDebugger(Debugger), AudioRecord(bool), CpuCorrupted { instr: InstrRef }, DebugStep(DebugStep), InstantRewind, Joypad((Player, JoypadBtn, ElementState)), LoadReplay((String, ReplayData)), LoadReplayPath(PathBuf), LoadRom((String, RomData)), LoadRomPath(PathBuf), LoadState(u8), RunState(RunState), ReplayRecord(bool), Reset(ResetKind), RequestFrame, Rewinding(bool), SaveState(u8), ShowFrameStats(bool), Screenshot, UnloadRom, ZapperAim((u16, u16)), ZapperTrigger, } impl From for NesEvent { fn from(event: EmulationEvent) -> Self { Self::Emulation(event) } } #[derive(Debug, Clone)] #[must_use] pub enum RendererEvent { ViewportResized((f32, f32)), FrameStats(FrameStats), ShowMenubar(bool), ToggleFullscreen, ReplayLoaded, ResizeTexture, ResizeWindow, ResourcesReady, RequestRedraw { viewport_id: ViewportId, when: Instant, }, RomLoaded(LoadedRom), RomUnloaded, Menu(Menu), } impl From for NesEvent { fn from(event: RendererEvent) -> Self { Self::Renderer(event) } } #[derive(Debug, Clone, PartialEq)] #[must_use] pub enum UiEvent { Error(String), Message((MessageType, String)), UpdateAvailable(String), LoadRomDialog, LoadReplayDialog, FileDialogCancelled, Terminate, } impl From for NesEvent { fn from(event: UiEvent) -> Self { Self::Ui(event) } } #[derive(Clone, PartialEq)] pub struct ReplayData(pub Vec); impl std::fmt::Debug for ReplayData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "ReplayData({} bytes)", self.0.len()) } } impl AsRef<[u8]> for ReplayData { fn as_ref(&self) -> &[u8] { &self.0 } } impl ApplicationHandler for Nes { fn user_event(&mut self, event_loop: &ActiveEventLoop, event: NesEvent) { if self.state.is_exiting() { return; } trace!("user event: {event:?}"); match event { NesEvent::Renderer(RendererEvent::ResourcesReady) => { if let Err(err) = self.init_running(event_loop) { error!("failed to create window: {err:?}"); event_loop.exit(); return; } // Disable device events to save some cpu as they're mostly duplicated in // WindowEvents event_loop.listen_device_events(DeviceEvents::Never); if let State::Running(state) = &mut self.state && let Some(window) = state.renderer.root_window() { if window.is_visible().unwrap_or(true) { state.repaint_times.insert(window.id(), Instant::now()); } else { // Immediately redraw the root window on start if not // visible. Fixes a bug where `window.request_redraw()` events // may not be sent if the window isn't visible, which is the // case until the first frame is drawn. if let Err(err) = state.renderer.redraw( window.id(), event_loop, &mut state.gamepads, &mut state.cfg, ) { state.renderer.on_error(err); } } } } NesEvent::Ui(UiEvent::Terminate) => event_loop.exit(), _ => (), } if let State::Running(state) = &mut self.state { state.user_event(event_loop, event); } } fn resumed(&mut self, event_loop: &ActiveEventLoop) { if self.state.is_exiting() { return; } debug!("resumed event"); if let State::Running(state) = &mut self.state { if feature!(Suspend) { state.renderer.recreate_window(event_loop); } if let Some(window_id) = state.renderer.root_window_id() { state.repaint_times.insert(window_id, Instant::now()); } } else if let State::Suspended { should_terminate } = &self.state && let Err(err) = self.request_renderer_resources(event_loop, Arc::clone(should_terminate)) { error!("failed to request renderer resources: {err:?}"); event_loop.exit(); } } fn window_event( &mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent, ) { if self.state.is_exiting() { return; } trace!("window event: {window_id:?} {event:?}"); if let State::Running(state) = &mut self.state { state.window_event(event_loop, window_id, event); } } fn device_event( &mut self, event_loop: &ActiveEventLoop, device_id: DeviceId, event: DeviceEvent, ) { if self.state.is_exiting() { return; } trace!("device event: {device_id:?} {event:?}"); if let State::Running(state) = &mut self.state { state.device_event(event_loop, device_id, event); } } fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { if self.state.is_exiting() { return; } else if self.should_terminate() { event_loop.exit(); } if let State::Running(state) = &mut self.state { state.about_to_wait(event_loop); } } fn suspended(&mut self, event_loop: &ActiveEventLoop) { if self.state.is_exiting() { return; } debug!("suspended event"); if let State::Running(state) = &mut self.state { state.suspended(event_loop); } } fn exiting(&mut self, event_loop: &ActiveEventLoop) { if self.state.is_exiting() { return; } debug!("exiting"); if let State::Running(state) = &mut self.state { state.exiting(event_loop); } else if feature!(AbortOnExit) { panic!("exited unexpectedly"); } self.state = State::Exiting; } } impl ApplicationHandler for Running { fn user_event(&mut self, _event_loop: &ActiveEventLoop, mut event: NesEvent) { match event { NesEvent::Config(ref event) => { let Config { deck, emulation, audio, renderer, input, } = &mut self.cfg; match event { ConfigEvent::ActionBindings(bindings) => { input.action_bindings.clone_from(bindings); self.input_bindings = InputBindings::from_input_config(input); } ConfigEvent::ActionBindingSet((action, set_input, binding)) => { input.set_binding(*action, *set_input, *binding); self.input_bindings.insert(*set_input, *action); } ConfigEvent::ActionBindingClear(clear_input) => { input.clear_binding(*clear_input); self.input_bindings.remove(clear_input); } ConfigEvent::AlwaysOnTop(always_on_top) => { renderer.always_on_top = *always_on_top; self.renderer .set_always_on_top(self.cfg.renderer.always_on_top); } ConfigEvent::ApuChannelEnabled((channel, enabled)) => { deck.channels_enabled[*channel as usize] = *enabled; } ConfigEvent::ApuChannelsEnabled(enabled) => { deck.channels_enabled = *enabled; } ConfigEvent::AudioBuffer(buffer_size) => { audio.buffer_size = *buffer_size; } ConfigEvent::AudioEnabled(enabled) => audio.enabled = *enabled, ConfigEvent::AudioLatency(latency) => audio.latency = *latency, ConfigEvent::AutoLoad(enabled) => emulation.auto_load = *enabled, ConfigEvent::AutoSave(enabled) => emulation.auto_save = *enabled, ConfigEvent::AutoSaveInterval(interval) => { emulation.auto_save_interval = *interval; } ConfigEvent::ConcurrentDpad(enabled) => deck.concurrent_dpad = *enabled, ConfigEvent::DarkTheme(enabled) => renderer.dark_theme = *enabled, ConfigEvent::EmbedViewports(embed) => renderer.embed_viewports = *embed, ConfigEvent::EmulatePpuWarmup(enabled) => deck.emulate_ppu_warmup = *enabled, ConfigEvent::FourPlayer(four_player) => deck.four_player = *four_player, ConfigEvent::Fullscreen(fullscreen) => renderer.fullscreen = *fullscreen, ConfigEvent::GamepadAssign((player, uuid)) => { input.assign_gamepad(*player, *uuid); if let Some(name) = self.gamepads.gamepad_name_by_uuid(uuid) { self.tx.event(UiEvent::Message(( MessageType::Info, format!("Assigned gamepad `{name}` to player {player:?}.",), ))); } } ConfigEvent::GamepadUnassign(player) => { if let Some(uuid) = input.unassign_gamepad(*player) && let Some(name) = self.gamepads.gamepad_name_by_uuid(&uuid) { self.tx.event(UiEvent::Message(( MessageType::Info, format!("Unassigned gamepad `{name}` from player {player:?}."), ))); } } ConfigEvent::GamepadAssignments(assignments) => { input.gamepad_assignments = *assignments; } ConfigEvent::GenieCodeAdded(genie_code) => { deck.genie_codes.push(genie_code.clone()); } ConfigEvent::GenieCodeClear => deck.genie_codes.clear(), ConfigEvent::GenieCodeRemoved(code) => { deck.genie_codes.retain(|genie| genie.code() != code); } ConfigEvent::HideOverscan(hide) => renderer.hide_overscan = *hide, ConfigEvent::MapperRevisions(revs) => deck.mapper_revisions = *revs, ConfigEvent::RamState(ram_state) => deck.ram_state = *ram_state, ConfigEvent::RecentRomsClear => renderer.recent_roms.clear(), ConfigEvent::Region(region) => deck.region = *region, ConfigEvent::RewindEnabled(enabled) => emulation.rewind = *enabled, ConfigEvent::RewindInterval(interval) => { emulation.rewind_interval = *interval; } ConfigEvent::RewindSeconds(seconds) => { emulation.rewind_seconds = *seconds; } ConfigEvent::RunAhead(run_ahead) => emulation.run_ahead = *run_ahead, ConfigEvent::SaveSlot(slot) => emulation.save_slot = *slot, ConfigEvent::Scale(scale) => renderer.scale = *scale, ConfigEvent::Shader(shader) => renderer.shader = *shader, ConfigEvent::ShowMenubar(show) => renderer.show_menubar = *show, ConfigEvent::ShowMessages(show) => renderer.show_messages = *show, ConfigEvent::ShowUpdates(show) => renderer.show_updates = *show, ConfigEvent::Speed(speed) => emulation.speed = *speed, ConfigEvent::VideoFilter(filter) => deck.filter = *filter, ConfigEvent::ZapperConnected(connected) => deck.zapper = *connected, } // Root viewport needs repainting when Config changes to update deferred viewports if let Some(window_id) = self.renderer.root_window_id() { self.repaint_times.insert(window_id, Instant::now()); } } NesEvent::Renderer(RendererEvent::RequestRedraw { viewport_id, when }) => { if let Some(window_id) = self.renderer.window_id_for_viewport(viewport_id) { self.repaint_times.insert( window_id, self.repaint_times .get(&window_id) .map_or(when, |last| (*last).min(when)), ); } } NesEvent::Emulation(EmulationEvent::LoadRom((ref name, _))) => { self.cfg .add_recent_rom(RecentRom::Homebrew { name: name.clone() }); } NesEvent::Ui(ref event) => self.on_ui_event(event), _ => (), } // Only wake emulation of relevant events if matches!(event, NesEvent::Emulation(_) | NesEvent::Config(_)) { self.emulation.on_event(&event); } self.renderer.on_event(&mut event, &self.cfg); } fn resumed(&mut self, _event_loop: &ActiveEventLoop) {} fn window_event( &mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent, ) { let res = self.renderer.on_window_event(window_id, &event); if res.repaint && event != WindowEvent::RedrawRequested { self.repaint_times.insert(window_id, Instant::now()); } if !res.consumed { match event { WindowEvent::RedrawRequested => { self.emulation.try_clock_frame(); if let Err(err) = self.renderer.redraw( window_id, event_loop, &mut self.gamepads, &mut self.cfg, ) { self.renderer.on_error(err); } self.repaint_times.remove(&window_id); } WindowEvent::Resized(_) if Some(window_id) == self.renderer.root_window_id() => { self.cfg.renderer.fullscreen = self.renderer.fullscreen(); } WindowEvent::Focused(focused) => { if focused && !self.occluded { self.repaint_times.insert(window_id, Instant::now()); if self.renderer.rom_loaded() && self.run_state().auto_paused() { self.set_run_state(RunState::Running); } } else { let time_since_last_save = Instant::now() - self.renderer.last_save_time; if time_since_last_save > Duration::from_secs(30) && let Err(err) = self.renderer.save(&self.cfg) { error!("failed to save rendererer state: {err:?}"); } if self .renderer .window(window_id) .and_then(|win| win.is_minimized()) .unwrap_or(false) { self.repaint_times.remove(&window_id); if self.renderer.rom_loaded() && !self.run_state().paused() { self.set_run_state(RunState::AutoPaused); } } } } WindowEvent::Occluded(occluded) => { self.occluded = occluded; // Note: Does not trigger on all platforms (e.g. linux) if occluded { self.repaint_times.remove(&window_id); if self.renderer.rom_loaded() && !self.run_state().paused() { self.set_run_state(RunState::AutoPaused); } } else { self.repaint_times.insert(window_id, Instant::now()); if self.renderer.rom_loaded() && self.run_state().auto_paused() { self.set_run_state(RunState::Running); } } } WindowEvent::KeyboardInput { event, is_synthetic, .. } => { // Winit generates fake "synthetic" KeyboardInput events when the focus // is changed to the window, or away from it. Synthetic key presses // represent no real key presses and should be ignored. // See https://github.com/rust-windowing/winit/issues/3543 if (!is_synthetic || event.state != ElementState::Pressed) && let PhysicalKey::Code(key) = event.physical_key { self.on_input( window_id, Input::Key(key, self.modifiers.state()), event.state, event.repeat, ); } } WindowEvent::ModifiersChanged(modifiers) => { self.modifiers = modifiers; } WindowEvent::MouseInput { button, state, .. } => { self.on_input(window_id, Input::Mouse(button), state, false); } WindowEvent::DroppedFile(path) if Some(window_id) == self.renderer.root_window_id() => { self.event(EmulationEvent::LoadRomPath(path)); } _ => (), } } } fn device_event( &mut self, _event_loop: &ActiveEventLoop, _device_id: DeviceId, event: DeviceEvent, ) { if let DeviceEvent::MouseMotion { delta } = event { self.renderer.on_mouse_motion(delta); } } fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { self.gamepads.update_events(); if let Some(window_id) = self.renderer.root_window_id() { let res = self.renderer.on_gamepad_update(&self.gamepads); self.gamepads.set_ui_consumes(res.consumed); if res.repaint { self.repaint_times.insert(window_id, Instant::now()); } if res.consumed { let now = Instant::now(); for wid in self.renderer.all_window_ids() { self.repaint_times.insert(wid, now); } } else { while let Some(event) = self.gamepads.next_event() { self.on_gamepad_event(window_id, event); self.repaint_times.insert(window_id, Instant::now()); } } // Always repaint when single threaded if !self.cfg.emulation.threaded { self.repaint_times.insert(window_id, Instant::now()); } } self.update_repaint_times(event_loop); } fn suspended(&mut self, event_loop: &ActiveEventLoop) { if feature!(Suspend) && let Err(err) = self.renderer.drop_window() { error!("failed to suspend window: {err:?}"); event_loop.exit(); } } fn exiting(&mut self, _event_loop: &ActiveEventLoop) { if let Err(err) = self.renderer.save(&self.cfg) { error!("failed to save rendererer state: {err:?}"); } self.emulation.terminate(); self.renderer.destroy(); if feature!(AbortOnExit) { panic!("exited unexpectedly"); } } fn memory_warning(&mut self, _event_loop: &ActiveEventLoop) { self.renderer .add_message(MessageType::Warn, "Your system memory is running low..."); if self.cfg.emulation.rewind { self.cfg.emulation.rewind = false; self.event(ConfigEvent::RewindEnabled(false)); } } } impl Running { fn run_state(&self) -> RunState { self.renderer.gui.borrow().run_state } fn set_run_state(&mut self, state: RunState) { self.renderer.gui.borrow_mut().run_state = state; self.event(EmulationEvent::RunState(state)); } pub fn update_repaint_times(&mut self, event_loop: &ActiveEventLoop) { let mut next_repaint_time = self.repaint_times.values().min().copied(); self.repaint_times.retain(|window_id, when| { if *when > Instant::now() { return true; } next_repaint_time = None; if let Some(window) = self.renderer.window(*window_id) { if !window.is_minimized().unwrap_or(false) { window.request_redraw(); } // Repaint time will get removed as soon as we receive the RequestRedraw event true } else { false } }); event_loop.set_control_flow(ControlFlow::WaitUntil(match next_repaint_time { Some(next_repaint_time) => next_repaint_time, None => Instant::now() + Duration::from_millis(16), })); } pub fn on_ui_event(&mut self, event: &UiEvent) { match event { UiEvent::Message((ty, msg)) => self.renderer.add_message(*ty, msg), UiEvent::Error(err) => self.renderer.on_error(anyhow!(err.clone())), UiEvent::LoadRomDialog => { match open_file_dialog( "Load ROM", "NES ROMs", &["nes"], self.cfg.renderer.roms_path.as_ref(), ) { Ok(maybe_path) => { if let Some(path) = maybe_path { self.event(EmulationEvent::LoadRomPath(path)); } } Err(err) => { error!("failed to open rom dialog: {err:?}"); self.event(UiEvent::Error("failed to open rom dialog".to_string())); } } } UiEvent::LoadReplayDialog => { match open_file_dialog( "Load Replay", "Replay Recording", &["replay"], Some(Config::default_data_dir()), ) { Ok(maybe_path) => { if let Some(path) = maybe_path { self.event(EmulationEvent::LoadReplayPath(path)); } } Err(err) => { error!("failed to open replay dialog: {err:?}"); self.event(UiEvent::Error("failed to open replay dialog".to_string())); } } } UiEvent::FileDialogCancelled => { if self.renderer.rom_loaded() && self.run_state().auto_paused() { self.set_run_state(RunState::Running); } } UiEvent::UpdateAvailable(_) | UiEvent::Terminate => (), } } /// Trigger a custom event. pub fn event(&mut self, event: impl Into) { let mut event = event.into(); trace!("Nes event: {event:?}"); self.emulation.on_event(&event); self.renderer.on_event(&mut event, &self.cfg); match event { NesEvent::Ui(event) => self.on_ui_event(&event), NesEvent::Emulation(event) => match event { EmulationEvent::LoadRomPath(path) => { if let Ok(path) = path.canonicalize() { self.cfg.add_recent_rom(RecentRom::Path(path)); } } EmulationEvent::Reset(_) => self.set_run_state(RunState::Running), _ => (), }, _ => (), } } /// Handle gamepad event. pub fn on_gamepad_event(&mut self, window_id: WindowId, event: gilrs::Event) { use gilrs::EventType; // Connect first because we may not have a name set yet if event.event == EventType::Connected { self.gamepads.connect(event.id); } if let Some(uuid) = self.gamepads.gamepad_uuid(event.id) { match event.event { EventType::ButtonPressed(button, _) => { if let Some(player) = self.cfg.input.gamepad_assignment(&uuid) { self.on_input( window_id, Input::Button(player, button), ElementState::Pressed, false, ); } } EventType::ButtonRepeated(button, _) => { if let Some(player) = self.cfg.input.gamepad_assignment(&uuid) { self.on_input( window_id, Input::Button(player, button), ElementState::Pressed, true, ); } } EventType::ButtonReleased(button, _) => { if let Some(player) = self.cfg.input.gamepad_assignment(&uuid) { self.on_input( window_id, Input::Button(player, button), ElementState::Released, false, ); } } EventType::AxisChanged(axis, value, _) => { if let Some(player) = self.cfg.input.gamepad_assignment(&uuid) { if let (Some(direction), state) = Gamepads::axis_state(value) { self.on_input( window_id, Input::Axis(player, axis, direction), state, false, ); } else { for direction in [AxisDirection::Positive, AxisDirection::Negative] { self.on_input( window_id, Input::Axis(player, axis, direction), ElementState::Released, false, ); } } } } EventType::Connected => { let saved_assignment = self.cfg.input.gamepad_assignment(&uuid); if let Some(player) = saved_assignment.or_else(|| self.cfg.input.next_gamepad_unassigned()) && let Some(name) = self.gamepads.gamepad_name_by_uuid(&uuid) { self.renderer.add_message( MessageType::Info, format!("Assigned gamepad `{name}` to player {player:?}."), ); self.cfg.input.assign_gamepad(player, uuid); } } EventType::Disconnected => { self.gamepads.disconnect(event.id); if let Some(player) = self.cfg.input.unassign_gamepad_name(&uuid) && let Some(name) = self.gamepads.gamepad_name_by_uuid(&uuid) { self.renderer.add_message( MessageType::Info, format!("Unassigned gamepad `{name}` from player {player:?}."), ); } } _ => (), } } } /// Handle user input mapped to key bindings. pub fn on_input( &mut self, window_id: WindowId, input: Input, state: ElementState, repeat: bool, ) { if let Some(action) = self.input_bindings.get(&input).copied() { trace!("action: {action:?}, state: {state:?}, repeat: {repeat:?}"); let released = state == ElementState::Released; let is_root_window = Some(window_id) == self.renderer.root_window_id(); match action { Action::Ui(ui_state) if released => match ui_state { Ui::Quit => self.tx.event(UiEvent::Terminate), Ui::TogglePause => { if is_root_window && self.renderer.rom_loaded() { self.set_run_state(match self.run_state() { RunState::Running => RunState::ManuallyPaused, RunState::ManuallyPaused | RunState::AutoPaused => { RunState::Running } }); } } Ui::LoadRom => { if self.renderer.rom_loaded() && !self.run_state().paused() { self.set_run_state(RunState::AutoPaused); } // NOTE: Due to some platforms file dialogs blocking the event loop, // loading requires a round-trip in order for the above pause to // get processed. self.tx.event(UiEvent::LoadRomDialog); } Ui::UnloadRom => { if self.renderer.rom_loaded() { self.event(EmulationEvent::UnloadRom); } } Ui::LoadReplay => { if self.renderer.rom_loaded() { if !self.run_state().paused() { self.set_run_state(RunState::AutoPaused); } // NOTE: Due to some platforms file dialogs blocking the event loop, // loading requires a round-trip in order for the above pause to // get processed. self.tx.event(UiEvent::LoadReplayDialog); } } }, Action::Menu(menu) if released => self.event(RendererEvent::Menu(menu)), Action::Feature(feature) if is_root_window => match feature { Feature::ToggleReplayRecording if released => { if feature!(Filesystem) { if self.renderer.rom_loaded() { self.replay_recording = !self.replay_recording; self.event(EmulationEvent::ReplayRecord(self.replay_recording)); } } else { self.renderer.add_message( MessageType::Warn, "Replay recordings are not supported yet on this platform.", ); } } Feature::ToggleAudioRecording if released => { if feature!(Filesystem) { if self.renderer.rom_loaded() { self.audio_recording = !self.audio_recording; self.event(EmulationEvent::AudioRecord(self.audio_recording)); } } else { self.renderer.add_message( MessageType::Warn, "Audio recordings are not supported yet on this platform.", ); } } Feature::TakeScreenshot if released => { if feature!(Filesystem) { if self.renderer.rom_loaded() { self.event(EmulationEvent::Screenshot); } } else { self.renderer.add_message( MessageType::Warn, "Screenshots are not supported yet on this platform.", ); } } Feature::VisualRewind => { if !self.rewinding { if repeat { self.rewinding = true; self.event(EmulationEvent::Rewinding(self.rewinding)); } else if released { self.event(EmulationEvent::InstantRewind); } } else if released { self.rewinding = false; self.event(EmulationEvent::Rewinding(self.rewinding)); } } _ => (), }, Action::Setting(setting) => match setting { Setting::ToggleFullscreen if released => { self.cfg.renderer.fullscreen = !self.cfg.renderer.fullscreen; self.renderer.set_fullscreen( self.cfg.renderer.fullscreen, self.cfg.renderer.embed_viewports, ); } Setting::ToggleEmbedViewports if released => { self.cfg.renderer.embed_viewports = !self.cfg.renderer.embed_viewports; self.renderer .set_embed_viewports(self.cfg.renderer.embed_viewports); } Setting::ToggleAlwaysOnTop if released => { self.cfg.renderer.always_on_top = !self.cfg.renderer.always_on_top; self.renderer .set_always_on_top(self.cfg.renderer.always_on_top); } Setting::ToggleAudio if released => { self.cfg.audio.enabled = !self.cfg.audio.enabled; self.event(ConfigEvent::AudioEnabled(self.cfg.audio.enabled)); } Setting::ToggleMenubar if released => { self.cfg.renderer.show_menubar = !self.cfg.renderer.show_menubar; self.event(RendererEvent::ShowMenubar(self.cfg.renderer.show_menubar)); } Setting::IncrementScale if released => { let scale = self.cfg.renderer.scale; let new_scale = self.cfg.increment_scale(); if scale != new_scale { self.event(ConfigEvent::Scale(new_scale)); } } Setting::DecrementScale if released => { let scale = self.cfg.renderer.scale; let new_scale = self.cfg.decrement_scale(); if scale != new_scale { self.event(ConfigEvent::Scale(new_scale)); } } Setting::IncrementSpeed if released => { let speed = self.cfg.emulation.speed; let new_speed = self.cfg.increment_speed(); if speed != new_speed { self.event(ConfigEvent::Speed(self.cfg.emulation.speed)); self.renderer.add_message( MessageType::Info, format!("Increased Emulation Speed to {new_speed}"), ); } } Setting::DecrementSpeed if released => { let speed = self.cfg.emulation.speed; let new_speed = self.cfg.decrement_speed(); if speed != new_speed { self.event(ConfigEvent::Speed(self.cfg.emulation.speed)); self.renderer.add_message( MessageType::Info, format!("Decreased Emulation Speed to {new_speed}"), ); } } Setting::FastForward if !repeat && is_root_window && self.renderer.rom_loaded() => { let new_speed = if released { 1.0 } else { 2.0 }; let speed = self.cfg.emulation.speed; if speed != new_speed { self.cfg.emulation.speed = new_speed; self.event(ConfigEvent::Speed(self.cfg.emulation.speed)); if new_speed == 2.0 { self.renderer .add_message(MessageType::Info, "Fast forwarding"); } } } Setting::SetShader(shader) if released => { let shader = if self.cfg.renderer.shader == shader { Shader::Default } else { shader }; self.cfg.renderer.shader = shader; self.event(ConfigEvent::Shader(shader)); } _ => (), }, Action::Deck(action) => match action { DeckAction::Reset(kind) if released => { self.event(EmulationEvent::Reset(kind)); } DeckAction::Joypad((player, button)) if !repeat && is_root_window => { self.event(EmulationEvent::Joypad((player, button, state))); } // Handled by `gui` module DeckAction::ZapperAim(_) | DeckAction::ZapperAimOffscreen | DeckAction::ZapperTrigger => (), DeckAction::SetSaveSlot(slot) if released => { if feature!(Storage) { if self.cfg.emulation.save_slot != slot { self.cfg.emulation.save_slot = slot; self.renderer.add_message( MessageType::Info, format!("Changed Save Slot to {slot}"), ); } } else { self.renderer.add_message( MessageType::Warn, "Save states are not supported yet on this platform.", ); } } DeckAction::SaveState if released && is_root_window => { if feature!(Storage) { self.event(EmulationEvent::SaveState(self.cfg.emulation.save_slot)); } else { self.renderer.add_message( MessageType::Warn, "Save states are not supported yet on this platform.", ); } } DeckAction::LoadState if released && is_root_window => { if feature!(Storage) { self.event(EmulationEvent::LoadState(self.cfg.emulation.save_slot)); } else { self.renderer.add_message( MessageType::Warn, "Save states are not supported yet on this platform.", ); } } DeckAction::ToggleApuChannel(channel) if released => { self.cfg.deck.channels_enabled[channel as usize] = !self.cfg.deck.channels_enabled[channel as usize]; self.event(ConfigEvent::ApuChannelEnabled(( channel, self.cfg.deck.channels_enabled[channel as usize], ))); } DeckAction::MapperRevision(rev) if released => { self.cfg.deck.mapper_revisions.set(rev); self.event(ConfigEvent::MapperRevisions(self.cfg.deck.mapper_revisions)); self.renderer.add_message( MessageType::Info, format!("Changed Mapper Revision to {rev}"), ); } DeckAction::SetNesRegion(region) if released => { self.cfg.deck.region = region; self.event(ConfigEvent::Region(self.cfg.deck.region)); self.renderer.add_message( MessageType::Info, format!("Changed NES Region to {region:?}"), ); } DeckAction::SetVideoFilter(filter) if released => { let filter = if self.cfg.deck.filter == filter { VideoFilter::Pixellate } else { filter }; self.cfg.deck.filter = filter; self.event(ConfigEvent::VideoFilter(filter)); } _ => (), }, Action::Debug(action) => match action { Debug::Toggle(kind) if released => { if matches!(kind, DebugKind::Ppu) { self.event(RendererEvent::Menu(Menu::PpuViewer)); } else { self.renderer.add_message( MessageType::Warn, format!("{kind:?} is not implemented yet"), ); } } Debug::Step(step) if (released | repeat) && is_root_window => { self.event(EmulationEvent::DebugStep(step)); } _ => (), }, _ => (), } } } } ================================================ FILE: tetanes/src/nes/input.rs ================================================ use crate::nes::{ action::{Action, Debug, DebugKind, DebugStep, Feature, Setting, Ui}, config::{Config, InputConfig}, renderer::{gui::Menu, shader::Shader}, }; use egui::ahash::HashMap; use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeMap, VecDeque}, iter::Peekable, ops::{Deref, DerefMut}, }; use tetanes_core::{ action::Action as DeckAction, apu::Channel, common::ResetKind, input::{JoypadBtn, Player}, video::VideoFilter, }; use tracing::warn; use uuid::Uuid; use winit::{ event::{ElementState, MouseButton}, keyboard::{KeyCode, ModifiersState}, }; macro_rules! action_binding { ($action:expr => $bindings:expr) => {{ let action = $action.into(); (action, ActionBindings::new(action, $bindings)) }}; ($action:expr => $modifiers:expr, $key:expr) => { action_binding!($action => [Some(Input::Key($key, $modifiers)), None, None]) }; ($action:expr => $modifiers1:expr, $key1:expr; $modifiers2:expr, $key2:expr) => { action_binding!( $action => [Some(Input::Key($key1, $modifiers1)), Some(Input::Key($key2, $modifiers2)), None] ) }; } #[allow(unused_macro_rules)] macro_rules! shortcut_map { (@ $action:expr => $key:expr) => { action_binding!($action => ModifiersState::empty(), $key) }; (@ $action:expr => $key1:expr; $key2:expr) => { action_binding!($action => ModifiersState::empty(), $key1; ModifiersState::empty(), $key2) }; (@ $action:expr => :$modifiers:expr, $key:expr) => { action_binding!($action => $modifiers, $key) }; (@ $action:expr => :$modifiers1:expr, $key1:expr; $key2:expr) => { action_binding!($action => $modifiers1, $key1; ModifiersState::empty(), $key2) }; (@ $action:expr => :$modifiers1:expr, $key1:expr; :$modifiers2:expr, $key2:expr) => { action_binding!($action => $modifiers1, $key1; $modifiers2, $key2) }; ($({ $action:expr => $(:$modifiers1:expr,) ?$key1:expr$(; $(:$modifiers2:expr,)? $key2:expr)? }),+$(,)?) => { vec![$(shortcut_map!(@ $action => $(:$modifiers1,)? $key1$(; $(:$modifiers2,)? $key2)?),)+] }; } macro_rules! gamepad_map { (@ $action:expr => $player:expr; $button:expr) => { action_binding!($action => [Some(Input::Button($player, $button)), None, None]) }; (@ $action:expr => $player:expr; $button1:expr; ($button2:expr, $state:expr)) => { action_binding!($action => [Some(Input::Button($player, $button1)), Some(Input::Axis($player, $button2, $state)), None]) }; ($({ $action:expr => $player:expr; $button1:expr$(; ($button2:expr, $state:expr))? }),+$(,)?) => { vec![$(gamepad_map!(@ $action => $player; $button1$(; ($button2, $state))?),)+] }; } macro_rules! mouse_map { (@ $action:expr => $button:expr) => { action_binding!($action => [Some(Input::Mouse($button)), None, None]) }; ($({ $action:expr => $button:expr }),+$(,)?) => { vec![$(mouse_map!(@ $action => $button),)+] }; } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[must_use] pub enum Input { Key(KeyCode, ModifiersState), Mouse(MouseButton), Button(Player, gilrs::Button), Axis(Player, gilrs::Axis, AxisDirection), } impl Input { pub fn fmt(input: Input) -> String { use winit::{ event::MouseButton, keyboard::{KeyCode, ModifiersState}, }; match input { Input::Key(keycode, modifiers) => { let mut s = String::with_capacity(32); if modifiers.contains(ModifiersState::CONTROL) { s += "Ctrl"; } if modifiers.contains(ModifiersState::SHIFT) { if !s.is_empty() { s += "+"; } s += "Shift"; } if modifiers.contains(ModifiersState::ALT) { if !s.is_empty() { s += "+"; } s += "Alt"; } if modifiers.contains(ModifiersState::SUPER) { if !s.is_empty() { s += "+"; } s += "Super"; } let ch = match keycode { KeyCode::Backquote => "`", KeyCode::Backslash | KeyCode::IntlBackslash => "\\", KeyCode::BracketLeft => "[", KeyCode::BracketRight => "]", KeyCode::Comma | KeyCode::NumpadComma => ",", KeyCode::Digit0 => "0", KeyCode::Digit1 => "1", KeyCode::Digit2 => "2", KeyCode::Digit3 => "3", KeyCode::Digit4 => "4", KeyCode::Digit5 => "5", KeyCode::Digit6 => "6", KeyCode::Digit7 => "7", KeyCode::Digit8 => "8", KeyCode::Digit9 => "9", KeyCode::Equal => "=", KeyCode::KeyA => "A", KeyCode::KeyB => "B", KeyCode::KeyC => "C", KeyCode::KeyD => "D", KeyCode::KeyE => "E", KeyCode::KeyF => "F", KeyCode::KeyG => "G", KeyCode::KeyH => "H", KeyCode::KeyI => "I", KeyCode::KeyJ => "J", KeyCode::KeyK => "K", KeyCode::KeyL => "L", KeyCode::KeyM => "M", KeyCode::KeyN => "N", KeyCode::KeyO => "O", KeyCode::KeyP => "P", KeyCode::KeyQ => "Q", KeyCode::KeyR => "R", KeyCode::KeyS => "S", KeyCode::KeyT => "T", KeyCode::KeyU => "U", KeyCode::KeyV => "V", KeyCode::KeyW => "W", KeyCode::KeyX => "X", KeyCode::KeyY => "Y", KeyCode::KeyZ => "Z", KeyCode::Minus | KeyCode::NumpadSubtract => "-", KeyCode::Period | KeyCode::NumpadDecimal => ".", KeyCode::Quote => "'", KeyCode::Semicolon => ";", KeyCode::Slash | KeyCode::NumpadDivide => "/", KeyCode::Backspace | KeyCode::NumpadBackspace => "Backspace", KeyCode::Enter | KeyCode::NumpadEnter => "Enter", KeyCode::Space => "Space", KeyCode::Tab => "Tab", KeyCode::Delete => "Delete", KeyCode::End => "End", KeyCode::Help => "Help", KeyCode::Home => "Home", KeyCode::Insert => "Ins", KeyCode::PageDown => "PageDown", KeyCode::PageUp => "PageUp", KeyCode::ArrowDown => "Down", KeyCode::ArrowLeft => "Left", KeyCode::ArrowRight => "Right", KeyCode::ArrowUp => "Up", KeyCode::Numpad0 => "Num0", KeyCode::Numpad1 => "Num1", KeyCode::Numpad2 => "Num2", KeyCode::Numpad3 => "Num3", KeyCode::Numpad4 => "Num4", KeyCode::Numpad5 => "Num5", KeyCode::Numpad6 => "Num6", KeyCode::Numpad7 => "Num7", KeyCode::Numpad8 => "Num8", KeyCode::Numpad9 => "Num9", KeyCode::NumpadAdd => "+", KeyCode::NumpadEqual => "=", KeyCode::NumpadHash => "#", KeyCode::NumpadMultiply => "*", KeyCode::NumpadParenLeft => "(", KeyCode::NumpadParenRight => ")", KeyCode::NumpadStar => "*", KeyCode::Escape => "Escape", KeyCode::Fn => "Fn", KeyCode::F1 => "F1", KeyCode::F2 => "F2", KeyCode::F3 => "F3", KeyCode::F4 => "F4", KeyCode::F5 => "F5", KeyCode::F6 => "F6", KeyCode::F7 => "F7", KeyCode::F8 => "F8", KeyCode::F9 => "F9", KeyCode::F10 => "F10", KeyCode::F11 => "F11", KeyCode::F12 => "F12", KeyCode::F13 => "F13", KeyCode::F14 => "F14", KeyCode::F15 => "F15", KeyCode::F16 => "F16", KeyCode::F17 => "F17", KeyCode::F18 => "F18", KeyCode::F19 => "F19", KeyCode::F20 => "F20", KeyCode::F21 => "F21", KeyCode::F22 => "F22", KeyCode::F23 => "F23", KeyCode::F24 => "F24", KeyCode::F25 => "F25", KeyCode::F26 => "F26", KeyCode::F27 => "F27", KeyCode::F28 => "F28", KeyCode::F29 => "F29", KeyCode::F30 => "F30", KeyCode::F31 => "F31", KeyCode::F32 => "F32", KeyCode::F33 => "F33", KeyCode::F34 => "F34", KeyCode::F35 => "F35", _ => "", }; if !ch.is_empty() { if !s.is_empty() { s += "+"; } s += ch; } s.shrink_to_fit(); s } Input::Button(_, button) => format!("{button:#?}"), Input::Axis(_, axis, direction) => format!("{axis:#?} {direction:#?}"), Input::Mouse(button) => match button { MouseButton::Left => String::from("Left Click"), MouseButton::Right => String::from("Right Click"), MouseButton::Middle => String::from("Middle Click"), MouseButton::Back => String::from("Back Click"), MouseButton::Forward => String::from("Forward Click"), MouseButton::Other(id) => format!("Button {id} Click"), }, } } } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum AxisDirection { Negative, // Left or Up Positive, // Right or Down } pub type Bindings = [Option; 3]; #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] #[must_use] pub struct ActionBindings { pub action: Action, pub bindings: Bindings, } impl ActionBindings { pub const fn new(action: Action, bindings: Bindings) -> Self { Self { action, bindings } } pub fn empty(action: Action) -> Self { Self { action, bindings: Default::default(), } } } impl ActionBindings { pub fn default_shortcuts() -> BTreeMap { use KeyCode::*; const SHIFT: ModifiersState = ModifiersState::SHIFT; const CONTROL: ModifiersState = ModifiersState::CONTROL; let mut bindings = Action::BINDABLE .into_iter() .filter(|action| !action.is_joypad()) .map(|action| (action, ActionBindings::empty(action))) .collect::>(); bindings.extend(shortcut_map!( { Debug::Step(DebugStep::Frame) => :SHIFT, KeyF }, { Debug::Step(DebugStep::Into) => KeyC }, { Debug::Step(DebugStep::Out) => :SHIFT, KeyO }, { Debug::Step(DebugStep::Over) => KeyO }, { Debug::Step(DebugStep::Scanline) => :SHIFT, KeyL }, { Debug::Toggle(DebugKind::Apu) => :SHIFT, KeyA }, { Debug::Toggle(DebugKind::Cpu) => :SHIFT, KeyD }, { Debug::Toggle(DebugKind::Ppu) => :SHIFT, KeyP }, { DeckAction::LoadState => :CONTROL, KeyL }, { DeckAction::Reset(ResetKind::Hard) => :CONTROL, KeyH }, { DeckAction::Reset(ResetKind::Soft) => :CONTROL, KeyR }, { DeckAction::SaveState => :CONTROL, KeyS }, { DeckAction::SetSaveSlot(1) => :CONTROL, Digit1 }, { DeckAction::SetSaveSlot(2) => :CONTROL, Digit2 }, { DeckAction::SetSaveSlot(3) => :CONTROL, Digit3 }, { DeckAction::SetSaveSlot(4) => :CONTROL, Digit4 }, { DeckAction::SetSaveSlot(5) => :CONTROL, Digit5 }, { DeckAction::SetSaveSlot(6) => :CONTROL, Digit6 }, { DeckAction::SetSaveSlot(7) => :CONTROL, Digit7 }, { DeckAction::SetSaveSlot(8) => :CONTROL, Digit8 }, { DeckAction::SetVideoFilter(VideoFilter::Ntsc) => :CONTROL, KeyN }, { DeckAction::ToggleApuChannel(Channel::Dmc) => :SHIFT, Digit5 }, { DeckAction::ToggleApuChannel(Channel::Mapper) => :SHIFT, Digit6 }, { DeckAction::ToggleApuChannel(Channel::Noise) => :SHIFT, Digit4 }, { DeckAction::ToggleApuChannel(Channel::Pulse1) => :SHIFT, Digit1 }, { DeckAction::ToggleApuChannel(Channel::Pulse2) => :SHIFT, Digit2 }, { DeckAction::ToggleApuChannel(Channel::Triangle) => :SHIFT, Digit3 }, { Feature::InstantRewind => KeyR }, { Feature::TakeScreenshot => F10 }, { Feature::ToggleAudioRecording => :SHIFT, KeyR }, { Feature::ToggleReplayRecording => :SHIFT, KeyV }, { Feature::VisualRewind => KeyR }, { Menu::About => F1 }, { Menu::Keybinds => :CONTROL, KeyK; F3 }, { Menu::Preferences => :CONTROL, KeyP; F2 }, { Menu::PerfStats => :CONTROL, KeyF }, { Setting::DecrementScale => :SHIFT, Minus }, { Setting::DecrementSpeed => Minus }, { Setting::FastForward => Space }, { Setting::IncrementScale => :SHIFT, Equal }, { Setting::IncrementSpeed => Equal }, { Setting::ToggleAudio => :CONTROL, KeyM }, { Setting::ToggleFullscreen => :CONTROL, Enter }, { Setting::ToggleMenubar => :CONTROL, KeyE }, { Setting::SetShader(Shader::CrtEasymode) => :CONTROL, KeyT }, { Ui::LoadRom => :CONTROL, KeyO; F3 }, { Ui::Quit => :CONTROL, KeyQ }, { Ui::TogglePause => Escape }, )); bindings.extend(mouse_map!( { DeckAction::ZapperTrigger => MouseButton::Left }, { DeckAction::ZapperAimOffscreen => MouseButton::Right } )); bindings } pub fn default_player_bindings(player: Player) -> BTreeMap { use KeyCode::*; use gilrs::{Axis, Button}; let mut bindings = Action::BINDABLE .into_iter() .filter(|action| action.joypad_player(player)) .map(|action| (action, ActionBindings::empty(action))) .collect::>(); bindings.extend(gamepad_map!( { (player, JoypadBtn::A) => player; Button::East }, { (player, JoypadBtn::TurboA) => player; Button::North }, { (player, JoypadBtn::B) => player; Button::South }, { (player, JoypadBtn::TurboB) => player; Button::West }, { (player, JoypadBtn::Up) => player; Button::DPadUp; (Axis::LeftStickY, AxisDirection::Negative) }, { (player, JoypadBtn::Down) => player; Button::DPadDown; (Axis::LeftStickY, AxisDirection::Positive) }, { (player, JoypadBtn::Left) => player; Button::DPadLeft; (Axis::LeftStickX, AxisDirection::Negative) }, { (player, JoypadBtn::Right) => player; Button::DPadRight; (Axis::LeftStickX, AxisDirection::Positive) }, { (player, JoypadBtn::Select) => player; Button::Select }, { (player, JoypadBtn::Start) => player; Button::Start }, )); let additional_bindings = match player { Player::One => shortcut_map!( { (Player::One, JoypadBtn::A) => KeyZ }, { (Player::One, JoypadBtn::TurboA) => KeyA }, { (Player::One, JoypadBtn::B) => KeyX }, { (Player::One, JoypadBtn::TurboB) => KeyS }, // FIXME: These overwrite Axis bindings above because there are only two binding // slots available at present { (Player::One, JoypadBtn::Up) => ArrowUp }, { (Player::One, JoypadBtn::Down) => ArrowDown }, { (Player::One, JoypadBtn::Left) => ArrowLeft }, { (Player::One, JoypadBtn::Right) => ArrowRight }, { (Player::One, JoypadBtn::Select) => KeyQ }, { (Player::One, JoypadBtn::Start) => KeyW }, ), _ => Vec::new(), }; for (action, addtl_binding) in additional_bindings { if let Some((_, existing_bindings)) = bindings .iter_mut() .find(|(existing_action, _)| **existing_action == action) { for binding in &mut existing_bindings.bindings { if binding.is_none() { *binding = addtl_binding.bindings[0]; } } } else { bindings.insert(action, addtl_binding); } } bindings } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct InputBindings(HashMap); impl InputBindings { pub fn from_input_config(cfg: &InputConfig) -> Self { Self( cfg.action_bindings .iter() .flat_map(|bind| { bind.bindings .iter() .flatten() .map(|input| (*input, bind.action)) }) .collect(), ) } } impl Deref for InputBindings { type Target = HashMap; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for InputBindings { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } /// Represents gamepad input state. #[derive(Default, Debug)] pub struct Gamepads { connected: HashMap, inner: Option, events: VecDeque, ui_consumes: bool, } impl Gamepads { pub fn new() -> Self { let mut connected = HashMap::default(); let mut gilrs = gilrs::Gilrs::new(); let mut events = VecDeque::new(); match &mut gilrs { Ok(inputs) => { for (id, gamepad) in inputs.gamepads() { let uuid = Self::create_uuid(&gamepad); tracing::debug!("gamepad connected: {} ({uuid})", gamepad.name()); connected.insert(id, uuid); } events.reserve(256); } Err(err) => { warn!("failed to initialize inputs: {err:?}"); } } Self { connected, inner: gilrs.ok(), events, ui_consumes: false, } } pub fn update_events(&mut self) { if let Some(inner) = self.inner.as_mut() { while let Some(event) = inner.next_event() { self.events.push_back(event); } } } pub fn axis_state(value: f32) -> (Option, ElementState) { let direction = if value >= 0.6 { Some(AxisDirection::Positive) } else if value <= -0.6 { Some(AxisDirection::Negative) } else { None }; let state = if direction.is_some() { ElementState::Pressed } else { ElementState::Released }; (direction, state) } pub fn has_events(&self) -> bool { !self.events.is_empty() } pub fn input_from_event( &self, event: &gilrs::Event, cfg: &Config, ) -> Option<(Input, ElementState)> { use gilrs::EventType; if let Some(player) = self .connected .get(&event.id) .and_then(|uuid| cfg.input.gamepad_assignment(uuid)) { match event.event { EventType::ButtonPressed(button, _) => { Some((Input::Button(player, button), ElementState::Pressed)) } EventType::ButtonRepeated(button, _) => { Some((Input::Button(player, button), ElementState::Pressed)) } EventType::ButtonReleased(button, _) => { Some((Input::Button(player, button), ElementState::Released)) } EventType::AxisChanged(axis, value, _) => { if let (Some(direction), state) = Gamepads::axis_state(value) { Some((Input::Axis(player, axis, direction), state)) } else { None } } _ => None, } } else { None } } pub fn connected_gamepad(&self, id: gilrs::GamepadId) -> Option> { self.inner .as_ref() .and_then(|inner| inner.connected_gamepad(id)) } pub fn gamepad(&self, id: gilrs::GamepadId) -> Option> { self.inner.as_ref().map(|inner| inner.gamepad(id)) } pub fn gamepad_by_uuid(&self, uuid: &Uuid) -> Option> { self.inner.as_ref().and_then(|inner| { self.connected .iter() .find(|(_, u)| *u == uuid) .and_then(|(id, _)| inner.connected_gamepad(*id)) }) } pub fn gamepad_name_by_uuid(&self, uuid: &Uuid) -> Option { self.gamepad_by_uuid(uuid).map(|g| g.name().to_string()) } pub fn gamepad_uuid(&self, id: gilrs::GamepadId) -> Option { self.connected_gamepad(id).map(|g| Self::create_uuid(&g)) } pub fn is_connected(&self, uuid: &Uuid) -> bool { self.gamepad_by_uuid(uuid).is_some() } pub fn list(&self) -> Option>> { self.inner.as_ref().map(|inner| inner.gamepads().peekable()) } pub fn connected_uuids(&self) -> impl Iterator { self.connected.values() } pub fn events(&self) -> impl Iterator { self.events.iter() } pub fn next_event(&mut self) -> Option { self.events.pop_back() } pub fn clear_events(&mut self) { self.events.clear(); } pub fn set_ui_consumes(&mut self, consumes: bool) { if self.ui_consumes && !consumes { self.events.clear(); } self.ui_consumes = consumes; } pub fn connect(&mut self, gamepad_id: gilrs::GamepadId) { if let Some(gamepad) = self.connected_gamepad(gamepad_id) { let uuid = Self::create_uuid(&gamepad); tracing::debug!("gamepad connected: {} ({uuid})", gamepad.name()); self.connected.insert(gamepad.id(), uuid); } } pub fn disconnect(&mut self, gamepad_id: gilrs::GamepadId) { if let Some(gamepad) = self.gamepad(gamepad_id) { let uuid = Self::create_uuid(&gamepad); tracing::debug!("gamepad disconnected: {} ({uuid})", gamepad.name()); } self.connected.remove(&gamepad_id); } pub fn create_uuid(gamepad: &gilrs::Gamepad<'_>) -> Uuid { let uuid = Uuid::from_bytes(gamepad.uuid()); if uuid != Uuid::nil() { return uuid; } // See: https://gitlab.com/gilrs-project/gilrs/-/issues/107 // SDL always uses USB bus for UUID let bustype = u32::to_be(0x03); // Version is not available. let version = 0; let vendor_id = gamepad.vendor_id().unwrap_or(0); let product_id = gamepad.product_id().unwrap_or(0); if vendor_id == 0 && product_id == 0 { Uuid::new_v4() } else { Uuid::from_fields( bustype, vendor_id, 0, &[ (product_id >> 8) as u8, product_id as u8, 0, 0, (version >> 8) as u8, version as u8, 0, 0, ], ) } } } ================================================ FILE: tetanes/src/nes/renderer/clipboard.rs ================================================ #[must_use] pub struct Clipboard { #[cfg(not(target_arch = "wasm32"))] inner: Option, /// Fallback. text: String, } impl Default for Clipboard { #[allow(clippy::derivable_impls)] fn default() -> Self { Self { #[cfg(not(target_arch = "wasm32"))] inner: arboard::Clipboard::new() .map_err(|err| tracing::warn!("failed to initialize clipboard: {err:?}")) .ok(), text: String::new(), } } } impl std::fmt::Debug for Clipboard { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut res = f.debug_struct("Clipboard"); #[cfg(not(target_arch = "wasm32"))] res.field("inner", &self.inner.as_ref().map(|_| "arboard")); res.field("text", &self.text).finish_non_exhaustive() } } impl Clipboard { pub fn new() -> Self { Self::default() } pub fn get(&mut self) -> Option { #[cfg(not(target_arch = "wasm32"))] if let Some(inner) = self.inner.as_mut() { return inner .get_text() .map_err(|err| tracing::warn!("clipboard paste error: {err:?}")) .ok(); } Some(self.text.clone()) } pub fn set(&mut self, text: impl Into) { let text = text.into(); #[cfg(not(target_arch = "wasm32"))] if let Some(inner) = self.inner.as_mut() { if let Err(err) = inner.set_text(text) { tracing::warn!("clipboard paste error: {err:?}"); } return; } self.text = text } } ================================================ FILE: tetanes/src/nes/renderer/event.rs ================================================ use crate::{ feature, nes::{ config::Config, event::{ConfigEvent, NesEvent, RendererEvent, Response, UiEvent}, input::{Gamepads, Input}, renderer::{ Renderer, State, Viewport, gui::{Gui, lib::pixels_per_point}, }, }, }; use egui::{PointerButton, SystemTheme, ViewportCommand, ViewportId}; use winit::{ dpi::PhysicalPosition, event::{ ElementState, Force, KeyEvent, MouseButton, MouseScrollDelta, Touch, TouchPhase, WindowEvent, }, keyboard::{Key, KeyCode, ModifiersState, NamedKey, PhysicalKey}, window::{Theme, WindowId}, }; impl Renderer { /// Handle event. pub fn on_event(&mut self, event: &mut NesEvent, cfg: &Config) { { let painter = self.painter.borrow(); if let Some(render_state) = painter.render_state() { self.gui.borrow_mut().on_event(&render_state.queue, event); } } match event { NesEvent::Renderer(event) => match event { RendererEvent::ViewportResized(_) => self.resize_window(cfg), RendererEvent::ResizeTexture => self.resize_texture = true, RendererEvent::RomLoaded(_) => { let state = self.state.borrow(); if state.focused != Some(ViewportId::ROOT) { self.ctx .send_viewport_cmd_to(ViewportId::ROOT, ViewportCommand::Focus); } // Loading ROM may need to resize to different region self.resize_texture = true; } _ => (), }, NesEvent::Config(event) => match event { ConfigEvent::DarkTheme(enabled) => { self.ctx.set_visuals(if *enabled { Gui::dark_theme() } else { Gui::light_theme() }); } ConfigEvent::EmbedViewports(embed) => { if feature!(OsViewports) { self.ctx.set_embed_viewports(*embed); } } ConfigEvent::Fullscreen(fullscreen) => { if feature!(OsViewports) { self.ctx .set_embed_viewports(*fullscreen || cfg.renderer.embed_viewports); } if self.fullscreen() != *fullscreen { self.ctx .send_viewport_cmd_to(ViewportId::ROOT, ViewportCommand::Focus); self.ctx.send_viewport_cmd_to( ViewportId::ROOT, ViewportCommand::Fullscreen(*fullscreen), ); } } ConfigEvent::Region(_) | ConfigEvent::HideOverscan(_) | ConfigEvent::Scale(_) => { self.resize_texture = true; } ConfigEvent::Shader(shader) => { self.painter.borrow_mut().set_shader(*shader); } _ => (), }, #[cfg(not(target_arch = "wasm32"))] NesEvent::AccessKit { window_id, event } => { use crate::nes::event::AccessKitWindowEvent; if let Some(viewport_id) = self.viewport_id_for_window(*window_id) { let mut state = self.state.borrow_mut(); if let Some(viewport) = state.viewports.get_mut(&viewport_id) { match event { AccessKitWindowEvent::InitialTreeRequested => { self.ctx.enable_accesskit(); self.ctx.request_repaint_of(viewport_id); } AccessKitWindowEvent::ActionRequested(request) => { viewport .raw_input .events .push(egui::Event::AccessKitActionRequest(request.clone())); self.ctx.request_repaint_of(viewport_id); } AccessKitWindowEvent::AccessibilityDeactivated => { self.ctx.disable_accesskit(); } } }; } } _ => (), } } /// Handle window event. pub fn on_window_event(&mut self, window_id: WindowId, event: &WindowEvent) -> Response { let Some(viewport_id) = self.viewport_id_for_window(window_id) else { return Response::default(); }; let State { viewports, focused, pointer_touch_id, .. } = &mut *self.state.borrow_mut(); let Some(viewport) = viewports.get_mut(&viewport_id) else { return Response::default(); }; #[cfg(not(target_arch = "wasm32"))] if let Some(window) = &viewport.window { tracing::trace!("process accesskit event: {event:?}"); self.accesskit.process_event(window, event); } let pixels_per_point = viewport .window .as_ref() .map_or(1.0, |window| pixels_per_point(&self.ctx, window)); match event { WindowEvent::Focused(new_focused) => { *focused = if *new_focused { Some(viewport_id) } else { None }; } // Note: Does not trigger on all platforms WindowEvent::Occluded(occluded) => viewport.occluded = *occluded, WindowEvent::CloseRequested | WindowEvent::Destroyed => { if viewport_id == ViewportId::ROOT { self.tx.event(UiEvent::Terminate); } else { viewport.info.events.push(egui::ViewportEvent::Close); self.gui.borrow_mut().close_viewport(viewport_id); // We may need to repaint both us and our parent to close the window, // and perhaps twice (once to notice the close-event, once again to enforce it). // `request_repaint_of` does a double-repaint though: self.ctx.request_repaint_of(viewport_id); self.ctx.request_repaint_of(viewport.ids.parent); } } // To support clipboard in wasm, we need to intercept the Paste event so that // we don't try to use the clipboard fallback logic for paste. Associated // behavior in the wasm platform layer handles setting the clipboard text. WindowEvent::KeyboardInput { event: KeyEvent { physical_key: PhysicalKey::Code(key), .. }, .. } => { if let Some(key) = key_from_keycode(*key) { use egui::Key; let modifiers = self.ctx.input(|i| i.modifiers); if feature!(ConsumePaste) && is_paste_command(modifiers, key) { return Response { consumed: true, repaint: true, }; } if matches!(key, Key::Plus | Key::Equals | Key::Minus | Key::Num0) && (modifiers.ctrl || modifiers.command) { self.zoom_changed = true; } } } WindowEvent::Resized(size) => { self.painter .borrow_mut() .on_window_resized(viewport_id, size.width, size.height); } WindowEvent::ThemeChanged(theme) => { self.ctx .send_viewport_cmd(ViewportCommand::SetTheme(if *theme == Theme::Light { SystemTheme::Light } else { SystemTheme::Dark })); } _ => (), }; let res = match event { WindowEvent::ScaleFactorChanged { scale_factor, .. } => { let native_pixels_per_point = *scale_factor as f32; viewport.info.native_pixels_per_point = Some(native_pixels_per_point); Response { repaint: true, consumed: false, } } WindowEvent::MouseInput { state, button, .. } => { Self::on_mouse_button_input(viewport.cursor_pos, viewport, *state, *button); Response { repaint: true, consumed: self.ctx.egui_wants_pointer_input(), } } WindowEvent::MouseWheel { delta, .. } => { Self::on_mouse_wheel(viewport, pixels_per_point, *delta); Response { repaint: true, consumed: self.ctx.egui_wants_pointer_input(), } } WindowEvent::CursorMoved { position, .. } => { Self::on_cursor_moved(viewport, pixels_per_point, *position); Response { repaint: true, consumed: self.ctx.egui_is_using_pointer(), } } WindowEvent::CursorLeft { .. } => { viewport.cursor_pos = None; viewport.raw_input.events.push(egui::Event::PointerGone); Response { repaint: true, consumed: false, } } // WindowEvent::TouchpadPressure {device_id, pressure, stage, .. } => {} // TODO WindowEvent::Touch(touch) => { Self::on_touch(viewport, pointer_touch_id, pixels_per_point, touch); let consumed = match touch.phase { TouchPhase::Started | TouchPhase::Ended | TouchPhase::Cancelled => { self.ctx.egui_wants_pointer_input() } TouchPhase::Moved => self.ctx.egui_is_using_pointer(), }; Response { repaint: true, consumed, } } WindowEvent::KeyboardInput { event, is_synthetic, .. } => { // Winit generates fake "synthetic" KeyboardInput events when the focus // is changed to the window, or away from it. Synthetic key presses // represent no real key presses and should be ignored. // See https://github.com/rust-windowing/winit/issues/3543 if *is_synthetic && event.state == ElementState::Pressed { Response { repaint: true, consumed: false, } } else { Self::on_keyboard_input(viewport, event); // When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes. let consumed = self.ctx.egui_wants_keyboard_input() || event.logical_key == Key::Named(NamedKey::Tab); Response { repaint: true, consumed, } } } WindowEvent::Focused(focused) => { viewport .raw_input .events .push(egui::Event::WindowFocused(*focused)); Response { repaint: true, consumed: false, } } WindowEvent::HoveredFile(path) => { if let Some(viewport) = viewports.get_mut(&viewport_id) { viewport.raw_input.hovered_files.push(egui::HoveredFile { path: Some(path.clone()), ..Default::default() }); } Response { repaint: true, consumed: false, } } WindowEvent::HoveredFileCancelled => { if let Some(viewport) = viewports.get_mut(&viewport_id) { viewport.raw_input.hovered_files.clear(); } Response { repaint: true, consumed: false, } } WindowEvent::DroppedFile(path) => { if let Some(viewport) = viewports.get_mut(&viewport_id) { viewport.raw_input.hovered_files.clear(); viewport.raw_input.dropped_files.push(egui::DroppedFile { path: Some(path.clone()), ..Default::default() }); } Response { repaint: true, consumed: false, } } WindowEvent::ModifiersChanged(state) => { let state = state.state(); let alt = state.alt_key(); let ctrl = state.control_key(); let shift = state.shift_key(); let super_ = state.super_key(); if let Some(viewport) = viewports.get_mut(&viewport_id) { viewport.raw_input.modifiers.alt = alt; viewport.raw_input.modifiers.ctrl = ctrl; viewport.raw_input.modifiers.shift = shift; viewport.raw_input.modifiers.mac_cmd = cfg!(target_os = "macos") && super_; viewport.raw_input.modifiers.command = if cfg!(target_os = "macos") { super_ } else { ctrl }; } Response { repaint: true, consumed: false, } } // Things that may require repaint: WindowEvent::RedrawRequested | WindowEvent::CursorEntered { .. } | WindowEvent::Destroyed | WindowEvent::Occluded(_) | WindowEvent::Resized(_) | WindowEvent::Moved(_) | WindowEvent::ThemeChanged(_) | WindowEvent::TouchpadPressure { .. } | WindowEvent::CloseRequested => Response { repaint: true, consumed: false, }, // Things we completely ignore: WindowEvent::ActivationTokenDone { .. } | WindowEvent::AxisMotion { .. } | WindowEvent::DoubleTapGesture { .. } | WindowEvent::RotationGesture { .. } | WindowEvent::PanGesture { .. } => Response { repaint: false, consumed: false, }, WindowEvent::PinchGesture { delta, .. } => { // Positive delta values indicate magnification (zooming in). // Negative delta values indicate shrinking (zooming out). let zoom_factor = (*delta as f32).exp(); viewport .raw_input .events .push(egui::Event::Zoom(zoom_factor)); Response { repaint: true, consumed: self.ctx.egui_wants_pointer_input(), } } WindowEvent::Ime(_) => Response::default(), }; let gui_res = self.gui.borrow_mut().on_window_event(event); Response { repaint: res.repaint || gui_res.repaint, consumed: res.consumed || gui_res.consumed, } } pub fn on_mouse_motion(&mut self, delta: (f64, f64)) { let State { viewports, focused, .. } = &mut *self.state.borrow_mut(); if let Some(id) = *focused && let Some(viewport) = viewports.get_mut(&id) { viewport .raw_input .events .push(egui::Event::MouseMoved(egui::Vec2 { x: delta.0 as f32, y: delta.1 as f32, })); } } fn on_mouse_button_input( pointer_pos: Option, viewport: &mut Viewport, state: ElementState, button: MouseButton, ) { if let Some(pos) = pointer_pos && let Some(button) = pointer_button_from_mouse(button) { let pressed = state == ElementState::Pressed; viewport.raw_input.events.push(egui::Event::PointerButton { pos, button, pressed, modifiers: viewport.raw_input.modifiers, }); } } fn on_cursor_moved( viewport: &mut Viewport, pixels_per_point: f32, pos_in_pixels: PhysicalPosition, ) { let pos_in_points = egui::pos2( pos_in_pixels.x as f32 / pixels_per_point, pos_in_pixels.y as f32 / pixels_per_point, ); viewport.cursor_pos = Some(pos_in_points); viewport .raw_input .events .push(egui::Event::PointerMoved(pos_in_points)); } fn on_touch( viewport: &mut Viewport, pointer_touch_id: &mut Option, pixels_per_point: f32, touch: &Touch, ) { // Emit touch event viewport.raw_input.events.push(egui::Event::Touch { device_id: egui::TouchDeviceId(egui::epaint::util::hash(touch.device_id)), id: egui::TouchId::from(touch.id), phase: match touch.phase { TouchPhase::Started => egui::TouchPhase::Start, TouchPhase::Moved => egui::TouchPhase::Move, TouchPhase::Ended => egui::TouchPhase::End, TouchPhase::Cancelled => egui::TouchPhase::Cancel, }, pos: egui::pos2( touch.location.x as f32 / pixels_per_point, touch.location.y as f32 / pixels_per_point, ), force: match touch.force { Some(Force::Normalized(force)) => Some(force as f32), Some(Force::Calibrated { force, max_possible_force, .. }) => Some((force / max_possible_force) as f32), None => None, }, }); // If we're not yet translating a touch or we're translating this very // touch … if pointer_touch_id.is_none() || pointer_touch_id.is_some_and(|touch_id| touch_id == touch.id) { // … emit PointerButton resp. PointerMoved events to emulate mouse match touch.phase { TouchPhase::Started => { *pointer_touch_id = Some(touch.id); // First move the pointer to the right location Self::on_cursor_moved(viewport, pixels_per_point, touch.location); Self::on_mouse_button_input( viewport.cursor_pos, viewport, ElementState::Pressed, MouseButton::Left, ); } TouchPhase::Moved => { Self::on_cursor_moved(viewport, pixels_per_point, touch.location); } TouchPhase::Ended => { *pointer_touch_id = None; Self::on_mouse_button_input( viewport.cursor_pos, viewport, ElementState::Released, MouseButton::Left, ); // The pointer should vanish completely to not get any // hover effects viewport.cursor_pos = None; viewport.raw_input.events.push(egui::Event::PointerGone); } TouchPhase::Cancelled => { *pointer_touch_id = None; viewport.cursor_pos = None; viewport.raw_input.events.push(egui::Event::PointerGone); } } } } fn on_mouse_wheel(viewport: &mut Viewport, pixels_per_point: f32, delta: MouseScrollDelta) { let modifiers = viewport.raw_input.modifiers; let (unit, delta) = match delta { MouseScrollDelta::LineDelta(x, y) => (egui::MouseWheelUnit::Line, egui::vec2(x, y)), MouseScrollDelta::PixelDelta(PhysicalPosition { x, y }) => ( egui::MouseWheelUnit::Point, egui::vec2(x as f32, y as f32) / pixels_per_point, ), }; viewport.raw_input.events.push(egui::Event::MouseWheel { unit, delta, phase: egui::TouchPhase::Move, modifiers, }); } fn on_keyboard_input(viewport: &mut Viewport, event: &KeyEvent) { let KeyEvent { // Represents the position of a key independent of the currently active layout. // // It also uniquely identifies the physical key (i.e. it's mostly synonymous with a scancode). // The most prevalent use case for this is games. For example the default keys for the player // to move around might be the W, A, S, and D keys on a US layout. The position of these keys // is more important than their label, so they should map to Z, Q, S, and D on an "AZERTY" // layout. (This value is `KeyCode::KeyW` for the Z key on an AZERTY layout.) physical_key, // Represents the results of a keymap, i.e. what character a certain key press represents. // When telling users "Press Ctrl-F to find", this is where we should // look for the "F" key, because they may have a dvorak layout on // a qwerty keyboard, and so the logical "F" character may not be located on the physical `KeyCode::KeyF` position. logical_key, text, state, .. } = event; let pressed = *state == ElementState::Pressed; let physical_key = if let PhysicalKey::Code(keycode) = *physical_key { key_from_keycode(keycode) } else { None }; let logical_key = key_from_winit_key(logical_key); // Helpful logging to enable when adding new key support tracing::trace!( "logical {:?} -> {:?}, physical {:?} -> {:?}", event.logical_key, logical_key, event.physical_key, physical_key ); let modifiers = viewport.raw_input.modifiers; if let Some(logical_key) = logical_key { if pressed { if is_cut_command(modifiers, logical_key) { viewport.raw_input.events.push(egui::Event::Cut); return; } else if is_copy_command(modifiers, logical_key) { viewport.raw_input.events.push(egui::Event::Copy); return; } else if is_paste_command(modifiers, logical_key) { if let Some(contents) = viewport.clipboard.get() { let contents = contents.replace("\r\n", "\n"); if !contents.is_empty() { viewport.raw_input.events.push(egui::Event::Paste(contents)); } } return; } } viewport.raw_input.events.push(egui::Event::Key { key: logical_key, physical_key, pressed, repeat: false, // egui will fill this in for us! modifiers, }); } if let Some(text) = &text { // Make sure there is text, and that it is not control characters // (e.g. delete is sent as "\u{f728}" on macOS). if !text.is_empty() && text.chars().all(is_printable_char) { // On some platforms we get here when the user presses Cmd-C (copy), ctrl-W, etc. // We need to ignore these characters that are side-effects of commands. // Also make sure the key is pressed (not released). On Linux, text might // contain some data even when the key is released. let is_cmd = modifiers.ctrl || modifiers.command || modifiers.mac_cmd; if pressed && !is_cmd { viewport .raw_input .events .push(egui::Event::Text(text.to_string())); } } } } /// Handle gamepad event updates. pub fn on_gamepad_update(&self, gamepads: &Gamepads) -> Response { if self.gui.borrow().keybinds.wants_input() && gamepads.has_events() { Response { consumed: true, repaint: true, } } else { Response::default() } } } impl TryFrom<(egui::Key, egui::Modifiers)> for Input { type Error = (); fn try_from((key, modifiers): (egui::Key, egui::Modifiers)) -> Result { let keycode = keycode_from_key(key).ok_or(())?; let modifiers = modifiers_state_from_modifiers(modifiers); Ok(Input::Key(keycode, modifiers)) } } impl From for Input { fn from(button: PointerButton) -> Self { Input::Mouse(mouse_button_from_pointer(button)) } } pub fn is_cut_command(modifiers: egui::Modifiers, keycode: egui::Key) -> bool { keycode == egui::Key::Cut || (modifiers.command && keycode == egui::Key::X) || (cfg!(target_os = "windows") && modifiers.shift && keycode == egui::Key::Delete) } pub fn is_copy_command(modifiers: egui::Modifiers, keycode: egui::Key) -> bool { keycode == egui::Key::Copy || (modifiers.command && keycode == egui::Key::C) || (cfg!(target_os = "windows") && modifiers.ctrl && keycode == egui::Key::Insert) } pub fn is_paste_command(modifiers: egui::Modifiers, keycode: egui::Key) -> bool { keycode == egui::Key::Paste || (modifiers.command && keycode == egui::Key::V) || (cfg!(target_os = "windows") && modifiers.shift && keycode == egui::Key::Insert) } /// Winit sends special keys (backspace, delete, F1, …) as characters. /// Ignore those. /// We also ignore '\r', '\n', '\t'. /// Newlines are handled by the `Key::Enter` event. pub const fn is_printable_char(chr: char) -> bool { let is_in_private_use_area = '\u{e000}' <= chr && chr <= '\u{f8ff}' || '\u{f0000}' <= chr && chr <= '\u{ffffd}' || '\u{100000}' <= chr && chr <= '\u{10fffd}'; !is_in_private_use_area && !chr.is_ascii_control() } pub fn key_from_winit_key(key: &winit::keyboard::Key) -> Option { match key { winit::keyboard::Key::Named(named_key) => key_from_named_key(*named_key), winit::keyboard::Key::Character(str) => egui::Key::from_name(str.as_str()), winit::keyboard::Key::Unidentified(_) | winit::keyboard::Key::Dead(_) => None, } } pub fn key_from_named_key(named_key: winit::keyboard::NamedKey) -> Option { use egui::Key; use winit::keyboard::NamedKey; Some(match named_key { NamedKey::Enter => Key::Enter, NamedKey::Tab => Key::Tab, NamedKey::ArrowDown => Key::ArrowDown, NamedKey::ArrowLeft => Key::ArrowLeft, NamedKey::ArrowRight => Key::ArrowRight, NamedKey::ArrowUp => Key::ArrowUp, NamedKey::End => Key::End, NamedKey::Home => Key::Home, NamedKey::PageDown => Key::PageDown, NamedKey::PageUp => Key::PageUp, NamedKey::Backspace => Key::Backspace, NamedKey::Delete => Key::Delete, NamedKey::Insert => Key::Insert, NamedKey::Escape => Key::Escape, NamedKey::Cut => Key::Cut, NamedKey::Copy => Key::Copy, NamedKey::Paste => Key::Paste, NamedKey::Space => Key::Space, NamedKey::F1 => Key::F1, NamedKey::F2 => Key::F2, NamedKey::F3 => Key::F3, NamedKey::F4 => Key::F4, NamedKey::F5 => Key::F5, NamedKey::F6 => Key::F6, NamedKey::F7 => Key::F7, NamedKey::F8 => Key::F8, NamedKey::F9 => Key::F9, NamedKey::F10 => Key::F10, NamedKey::F11 => Key::F11, NamedKey::F12 => Key::F12, NamedKey::F13 => Key::F13, NamedKey::F14 => Key::F14, NamedKey::F15 => Key::F15, NamedKey::F16 => Key::F16, NamedKey::F17 => Key::F17, NamedKey::F18 => Key::F18, NamedKey::F19 => Key::F19, NamedKey::F20 => Key::F20, NamedKey::F21 => Key::F21, NamedKey::F22 => Key::F22, NamedKey::F23 => Key::F23, NamedKey::F24 => Key::F24, NamedKey::F25 => Key::F25, NamedKey::F26 => Key::F26, NamedKey::F27 => Key::F27, NamedKey::F28 => Key::F28, NamedKey::F29 => Key::F29, NamedKey::F30 => Key::F30, NamedKey::F31 => Key::F31, NamedKey::F32 => Key::F32, NamedKey::F33 => Key::F33, NamedKey::F34 => Key::F34, NamedKey::F35 => Key::F35, _ => { tracing::trace!("Unknown key: {named_key:?}"); return None; } }) } pub const fn key_from_keycode(keycode: KeyCode) -> Option { Some(match keycode { KeyCode::ArrowDown => egui::Key::ArrowDown, KeyCode::ArrowLeft => egui::Key::ArrowLeft, KeyCode::ArrowRight => egui::Key::ArrowRight, KeyCode::ArrowUp => egui::Key::ArrowUp, KeyCode::Escape => egui::Key::Escape, KeyCode::Tab => egui::Key::Tab, KeyCode::Backspace => egui::Key::Backspace, KeyCode::Enter | KeyCode::NumpadEnter => egui::Key::Enter, KeyCode::Insert => egui::Key::Insert, KeyCode::Delete => egui::Key::Delete, KeyCode::Home => egui::Key::Home, KeyCode::End => egui::Key::End, KeyCode::PageUp => egui::Key::PageUp, KeyCode::PageDown => egui::Key::PageDown, // Punctuation KeyCode::Space => egui::Key::Space, KeyCode::Comma => egui::Key::Comma, KeyCode::Period => egui::Key::Period, KeyCode::Semicolon => egui::Key::Semicolon, KeyCode::Backslash => egui::Key::Backslash, KeyCode::Slash | KeyCode::NumpadDivide => egui::Key::Slash, KeyCode::BracketLeft => egui::Key::OpenBracket, KeyCode::BracketRight => egui::Key::CloseBracket, KeyCode::Backquote => egui::Key::Backtick, KeyCode::Cut => egui::Key::Cut, KeyCode::Copy => egui::Key::Copy, KeyCode::Paste => egui::Key::Paste, KeyCode::Minus | KeyCode::NumpadSubtract => egui::Key::Minus, KeyCode::NumpadAdd => egui::Key::Plus, KeyCode::Equal => egui::Key::Equals, KeyCode::Digit0 | KeyCode::Numpad0 => egui::Key::Num0, KeyCode::Digit1 | KeyCode::Numpad1 => egui::Key::Num1, KeyCode::Digit2 | KeyCode::Numpad2 => egui::Key::Num2, KeyCode::Digit3 | KeyCode::Numpad3 => egui::Key::Num3, KeyCode::Digit4 | KeyCode::Numpad4 => egui::Key::Num4, KeyCode::Digit5 | KeyCode::Numpad5 => egui::Key::Num5, KeyCode::Digit6 | KeyCode::Numpad6 => egui::Key::Num6, KeyCode::Digit7 | KeyCode::Numpad7 => egui::Key::Num7, KeyCode::Digit8 | KeyCode::Numpad8 => egui::Key::Num8, KeyCode::Digit9 | KeyCode::Numpad9 => egui::Key::Num9, KeyCode::KeyA => egui::Key::A, KeyCode::KeyB => egui::Key::B, KeyCode::KeyC => egui::Key::C, KeyCode::KeyD => egui::Key::D, KeyCode::KeyE => egui::Key::E, KeyCode::KeyF => egui::Key::F, KeyCode::KeyG => egui::Key::G, KeyCode::KeyH => egui::Key::H, KeyCode::KeyI => egui::Key::I, KeyCode::KeyJ => egui::Key::J, KeyCode::KeyK => egui::Key::K, KeyCode::KeyL => egui::Key::L, KeyCode::KeyM => egui::Key::M, KeyCode::KeyN => egui::Key::N, KeyCode::KeyO => egui::Key::O, KeyCode::KeyP => egui::Key::P, KeyCode::KeyQ => egui::Key::Q, KeyCode::KeyR => egui::Key::R, KeyCode::KeyS => egui::Key::S, KeyCode::KeyT => egui::Key::T, KeyCode::KeyU => egui::Key::U, KeyCode::KeyV => egui::Key::V, KeyCode::KeyW => egui::Key::W, KeyCode::KeyX => egui::Key::X, KeyCode::KeyY => egui::Key::Y, KeyCode::KeyZ => egui::Key::Z, KeyCode::F1 => egui::Key::F1, KeyCode::F2 => egui::Key::F2, KeyCode::F3 => egui::Key::F3, KeyCode::F4 => egui::Key::F4, KeyCode::F5 => egui::Key::F5, KeyCode::F6 => egui::Key::F6, KeyCode::F7 => egui::Key::F7, KeyCode::F8 => egui::Key::F8, KeyCode::F9 => egui::Key::F9, KeyCode::F10 => egui::Key::F10, KeyCode::F11 => egui::Key::F11, KeyCode::F12 => egui::Key::F12, KeyCode::F13 => egui::Key::F13, KeyCode::F14 => egui::Key::F14, KeyCode::F15 => egui::Key::F15, KeyCode::F16 => egui::Key::F16, KeyCode::F17 => egui::Key::F17, KeyCode::F18 => egui::Key::F18, KeyCode::F19 => egui::Key::F19, KeyCode::F20 => egui::Key::F20, KeyCode::F21 => egui::Key::F21, KeyCode::F22 => egui::Key::F22, KeyCode::F23 => egui::Key::F23, KeyCode::F24 => egui::Key::F24, KeyCode::F25 => egui::Key::F25, KeyCode::F26 => egui::Key::F26, KeyCode::F27 => egui::Key::F27, KeyCode::F28 => egui::Key::F28, KeyCode::F29 => egui::Key::F29, KeyCode::F30 => egui::Key::F30, KeyCode::F31 => egui::Key::F31, KeyCode::F32 => egui::Key::F32, KeyCode::F33 => egui::Key::F33, KeyCode::F34 => egui::Key::F34, KeyCode::F35 => egui::Key::F35, _ => { return None; } }) } pub const fn keycode_from_key(key: egui::Key) -> Option { Some(match key { egui::Key::ArrowDown => KeyCode::ArrowDown, egui::Key::ArrowLeft => KeyCode::ArrowLeft, egui::Key::ArrowRight => KeyCode::ArrowRight, egui::Key::ArrowUp => KeyCode::ArrowUp, egui::Key::Escape => KeyCode::Escape, egui::Key::Tab => KeyCode::Tab, egui::Key::Backspace => KeyCode::Backspace, egui::Key::Enter => KeyCode::Enter, egui::Key::Insert => KeyCode::Insert, egui::Key::Delete => KeyCode::Delete, egui::Key::Home => KeyCode::Home, egui::Key::End => KeyCode::End, egui::Key::PageUp => KeyCode::PageUp, egui::Key::PageDown => KeyCode::PageDown, // Punctuation egui::Key::Space => KeyCode::Space, egui::Key::Comma => KeyCode::Comma, egui::Key::Period => KeyCode::Period, egui::Key::Semicolon => KeyCode::Semicolon, egui::Key::Backslash => KeyCode::Backslash, egui::Key::Slash => KeyCode::Slash, egui::Key::OpenBracket => KeyCode::BracketLeft, egui::Key::CloseBracket => KeyCode::BracketRight, egui::Key::Cut => KeyCode::Cut, egui::Key::Copy => KeyCode::Copy, egui::Key::Paste => KeyCode::Paste, egui::Key::Minus => KeyCode::Minus, egui::Key::Plus => KeyCode::NumpadAdd, egui::Key::Equals => KeyCode::Equal, egui::Key::Num0 => KeyCode::Digit0, egui::Key::Num1 => KeyCode::Digit1, egui::Key::Num2 => KeyCode::Digit2, egui::Key::Num3 => KeyCode::Digit3, egui::Key::Num4 => KeyCode::Digit4, egui::Key::Num5 => KeyCode::Digit5, egui::Key::Num6 => KeyCode::Digit6, egui::Key::Num7 => KeyCode::Digit7, egui::Key::Num8 => KeyCode::Digit8, egui::Key::Num9 => KeyCode::Digit9, egui::Key::A => KeyCode::KeyA, egui::Key::B => KeyCode::KeyB, egui::Key::C => KeyCode::KeyC, egui::Key::D => KeyCode::KeyD, egui::Key::E => KeyCode::KeyE, egui::Key::F => KeyCode::KeyF, egui::Key::G => KeyCode::KeyG, egui::Key::H => KeyCode::KeyH, egui::Key::I => KeyCode::KeyI, egui::Key::J => KeyCode::KeyJ, egui::Key::K => KeyCode::KeyK, egui::Key::L => KeyCode::KeyL, egui::Key::M => KeyCode::KeyM, egui::Key::N => KeyCode::KeyN, egui::Key::O => KeyCode::KeyO, egui::Key::P => KeyCode::KeyP, egui::Key::Q => KeyCode::KeyQ, egui::Key::R => KeyCode::KeyR, egui::Key::S => KeyCode::KeyS, egui::Key::T => KeyCode::KeyT, egui::Key::U => KeyCode::KeyU, egui::Key::V => KeyCode::KeyV, egui::Key::W => KeyCode::KeyW, egui::Key::X => KeyCode::KeyX, egui::Key::Y => KeyCode::KeyY, egui::Key::Z => KeyCode::KeyZ, egui::Key::F1 => KeyCode::F1, egui::Key::F2 => KeyCode::F2, egui::Key::F3 => KeyCode::F3, egui::Key::F4 => KeyCode::F4, egui::Key::F5 => KeyCode::F5, egui::Key::F6 => KeyCode::F6, egui::Key::F7 => KeyCode::F7, egui::Key::F8 => KeyCode::F8, egui::Key::F9 => KeyCode::F9, egui::Key::F10 => KeyCode::F10, egui::Key::F11 => KeyCode::F11, egui::Key::F12 => KeyCode::F12, egui::Key::F13 => KeyCode::F13, egui::Key::F14 => KeyCode::F14, egui::Key::F15 => KeyCode::F15, egui::Key::F16 => KeyCode::F16, egui::Key::F17 => KeyCode::F17, egui::Key::F18 => KeyCode::F18, egui::Key::F19 => KeyCode::F19, egui::Key::F20 => KeyCode::F20, egui::Key::F21 => KeyCode::F21, egui::Key::F22 => KeyCode::F22, egui::Key::F23 => KeyCode::F23, egui::Key::F24 => KeyCode::F24, egui::Key::F25 => KeyCode::F25, egui::Key::F26 => KeyCode::F26, egui::Key::F27 => KeyCode::F27, egui::Key::F28 => KeyCode::F28, egui::Key::F29 => KeyCode::F29, egui::Key::F30 => KeyCode::F30, egui::Key::F31 => KeyCode::F31, egui::Key::F32 => KeyCode::F32, egui::Key::F33 => KeyCode::F33, egui::Key::F34 => KeyCode::F34, egui::Key::F35 => KeyCode::F35, _ => return None, }) } pub fn modifiers_from_modifiers_state(modifier_state: ModifiersState) -> egui::Modifiers { egui::Modifiers { alt: modifier_state.alt_key(), ctrl: modifier_state.control_key(), shift: modifier_state.shift_key(), #[cfg(target_os = "macos")] mac_cmd: modifier_state.super_key(), #[cfg(not(target_os = "macos"))] mac_cmd: false, #[cfg(target_os = "macos")] command: modifier_state.super_key(), #[cfg(not(target_os = "macos"))] command: modifier_state.control_key(), } } pub fn modifiers_state_from_modifiers(modifiers: egui::Modifiers) -> ModifiersState { let mut modifiers_state = ModifiersState::empty(); if modifiers.shift { modifiers_state |= ModifiersState::SHIFT; } if modifiers.ctrl { modifiers_state |= ModifiersState::CONTROL; } if modifiers.alt { modifiers_state |= ModifiersState::ALT; } #[cfg(target_os = "macos")] if modifiers.mac_cmd { modifiers_state |= ModifiersState::SUPER; } // TODO: egui doesn't seem to support SUPER on Windows/Linux modifiers_state } pub const fn pointer_button_from_mouse(button: MouseButton) -> Option { Some(match button { MouseButton::Left => PointerButton::Primary, MouseButton::Right => PointerButton::Secondary, MouseButton::Middle => PointerButton::Middle, MouseButton::Back => PointerButton::Extra1, MouseButton::Forward => PointerButton::Extra2, MouseButton::Other(_) => return None, }) } pub const fn mouse_button_from_pointer(button: PointerButton) -> MouseButton { match button { PointerButton::Primary => MouseButton::Left, PointerButton::Secondary => MouseButton::Right, PointerButton::Middle => MouseButton::Middle, PointerButton::Extra1 => MouseButton::Back, PointerButton::Extra2 => MouseButton::Forward, } } pub const fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option { use egui::CursorIcon; match cursor_icon { CursorIcon::None => None, CursorIcon::Alias => Some(winit::window::CursorIcon::Alias), CursorIcon::AllScroll => Some(winit::window::CursorIcon::AllScroll), CursorIcon::Cell => Some(winit::window::CursorIcon::Cell), CursorIcon::ContextMenu => Some(winit::window::CursorIcon::ContextMenu), CursorIcon::Copy => Some(winit::window::CursorIcon::Copy), CursorIcon::Crosshair => Some(winit::window::CursorIcon::Crosshair), CursorIcon::Default => Some(winit::window::CursorIcon::Default), CursorIcon::Grab => Some(winit::window::CursorIcon::Grab), CursorIcon::Grabbing => Some(winit::window::CursorIcon::Grabbing), CursorIcon::Help => Some(winit::window::CursorIcon::Help), CursorIcon::Move => Some(winit::window::CursorIcon::Move), CursorIcon::NoDrop => Some(winit::window::CursorIcon::NoDrop), CursorIcon::NotAllowed => Some(winit::window::CursorIcon::NotAllowed), CursorIcon::PointingHand => Some(winit::window::CursorIcon::Pointer), CursorIcon::Progress => Some(winit::window::CursorIcon::Progress), CursorIcon::ResizeHorizontal => Some(winit::window::CursorIcon::EwResize), CursorIcon::ResizeNeSw => Some(winit::window::CursorIcon::NeswResize), CursorIcon::ResizeNwSe => Some(winit::window::CursorIcon::NwseResize), CursorIcon::ResizeVertical => Some(winit::window::CursorIcon::NsResize), CursorIcon::ResizeEast => Some(winit::window::CursorIcon::EResize), CursorIcon::ResizeSouthEast => Some(winit::window::CursorIcon::SeResize), CursorIcon::ResizeSouth => Some(winit::window::CursorIcon::SResize), CursorIcon::ResizeSouthWest => Some(winit::window::CursorIcon::SwResize), CursorIcon::ResizeWest => Some(winit::window::CursorIcon::WResize), CursorIcon::ResizeNorthWest => Some(winit::window::CursorIcon::NwResize), CursorIcon::ResizeNorth => Some(winit::window::CursorIcon::NResize), CursorIcon::ResizeNorthEast => Some(winit::window::CursorIcon::NeResize), CursorIcon::ResizeColumn => Some(winit::window::CursorIcon::ColResize), CursorIcon::ResizeRow => Some(winit::window::CursorIcon::RowResize), CursorIcon::Text => Some(winit::window::CursorIcon::Text), CursorIcon::VerticalText => Some(winit::window::CursorIcon::VerticalText), CursorIcon::Wait => Some(winit::window::CursorIcon::Wait), CursorIcon::ZoomIn => Some(winit::window::CursorIcon::ZoomIn), CursorIcon::ZoomOut => Some(winit::window::CursorIcon::ZoomOut), } } ================================================ FILE: tetanes/src/nes/renderer/gui/keybinds.rs ================================================ use crate::nes::{ action::Action, config::Config, event::{ConfigEvent, NesEventProxy, RendererEvent}, input::{Gamepads, Input}, renderer::gui::lib::ViewportOptions, }; use egui::{ Align2, Button, CentralPanel, Context, Grid, ScrollArea, Ui, Vec2, ViewportClass, ViewportId, }; use parking_lot::Mutex; use std::sync::{ Arc, atomic::{AtomicBool, Ordering}, }; use tetanes_core::{input::Player, time::Instant}; use uuid::Uuid; use winit::event::ElementState; #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] pub enum Tab { #[default] Shortcuts, Joypad(Player), } #[derive(Debug)] #[must_use] pub struct State { tx: NesEventProxy, tab: Tab, pending_input: Option, gamepad_unassign_confirm: Option<(Player, Player, Uuid)>, } #[derive(Debug)] #[must_use] pub struct Keybinds { pub id: ViewportId, open: Arc, state: Arc>, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct PendingInput { action: Action, input: Option, binding: usize, conflict: Option, } #[derive(Debug)] #[must_use] pub struct GamepadState { input_events: Vec<(Input, ElementState)>, connected: Option>, } #[derive(Debug, PartialEq, Eq)] #[must_use] pub struct ConnectedGamepad { uuid: Uuid, name: String, assignment: Option, } impl Keybinds { const TITLE: &'static str = "TetaNES - Keybinds"; pub fn new(tx: NesEventProxy) -> Self { Self { id: ViewportId::from_hash_of(Self::TITLE), open: Arc::new(AtomicBool::new(false)), state: Arc::new(Mutex::new(State { tx, tab: Tab::default(), pending_input: None, gamepad_unassign_confirm: None, })), } } pub fn wants_input(&self) -> bool { self.state.try_lock().is_some_and(|state| { state.pending_input.is_some() || state.gamepad_unassign_confirm.is_some() }) } pub fn open(&self) -> bool { self.open.load(Ordering::Acquire) } pub fn set_open(&self, open: bool, ctx: &Context) { self.open.store(open, Ordering::Release); if open { ctx.send_viewport_cmd_to(self.id, egui::ViewportCommand::Close); } } pub fn toggle_open(&self, ctx: &Context) { let Ok(open) = self .open .fetch_update(Ordering::Release, Ordering::Acquire, |open| Some(!open)) else { return; }; if open { ctx.send_viewport_cmd_to(self.id, egui::ViewportCommand::Close); } } pub fn show(&mut self, ui: &mut Ui, opts: ViewportOptions, cfg: Config, gamepads: &Gamepads) { if !self.open() { return; } let open = Arc::clone(&self.open); let state = Arc::clone(&self.state); let mut viewport_builder = egui::ViewportBuilder::default().with_title(Self::TITLE); if opts.always_on_top { viewport_builder = viewport_builder.with_always_on_top(); } let gamepad_state = GamepadState { input_events: gamepads .events() .filter_map(|event| gamepads.input_from_event(event, &cfg)) .collect::>(), connected: gamepads.list().map(|gamepad_list| { gamepad_list .map(|(_, gamepad)| { let uuid = Gamepads::create_uuid(&gamepad); ConnectedGamepad { uuid, name: gamepad.name().to_string(), assignment: cfg.input.gamepad_assignment(&uuid), } }) .collect::>() }), }; ui.show_viewport_deferred(self.id, viewport_builder, move |ui, class| { if class == ViewportClass::EmbeddedWindow { let mut window_open = open.load(Ordering::Acquire); egui::Window::new(Keybinds::TITLE) .open(&mut window_open) .default_rect(ui.content_rect().shrink(16.0)) .show(ui, |ui| { state.lock().ui(ui, opts.enabled, &cfg, &gamepad_state); }); open.store(window_open, Ordering::Release); } else { CentralPanel::default().show_inside(ui, |ui| { state.lock().ui(ui, opts.enabled, &cfg, &gamepad_state); }); if ui.input(|i| i.viewport().close_requested()) { open.store(false, Ordering::Release); } } if !open.load(Ordering::Acquire) { let mut state = state.lock(); state.pending_input = None; state.gamepad_unassign_confirm = None; } }); } } impl State { fn ui(&mut self, ui: &mut Ui, enabled: bool, cfg: &Config, gamepad_state: &GamepadState) { self.show_set_keybind_window(ui.ctx(), cfg, &gamepad_state.input_events); self.show_gamepad_unassign_window(ui.ctx()); ui.add_enabled_ui(enabled, |ui| { ui.horizontal(|ui| { ui.selectable_value(&mut self.tab, Tab::Shortcuts, "Shortcuts"); ui.selectable_value(&mut self.tab, Tab::Joypad(Player::One), "Player1"); ui.selectable_value(&mut self.tab, Tab::Joypad(Player::Two), "Player2"); ui.selectable_value(&mut self.tab, Tab::Joypad(Player::Three), "Player3"); ui.selectable_value(&mut self.tab, Tab::Joypad(Player::Four), "Player4"); }); ui.separator(); match self.tab { Tab::Shortcuts => self.list(ui, None, cfg, gamepad_state.connected.as_deref()), Tab::Joypad(player) => { self.list(ui, Some(player), cfg, gamepad_state.connected.as_deref()) } } }); } fn list( &mut self, ui: &mut Ui, player: Option, cfg: &Config, connected_gamepads: Option<&[ConnectedGamepad]>, ) { ui.set_min_height(ui.available_height()); if let Some(player) = player { self.player_gamepad_combo(ui, player, connected_gamepads); ui.separator(); } ScrollArea::both().auto_shrink(false).show(ui, |ui| { let grid = Grid::new("keybind_list") .num_columns(4) .spacing([10.0, 6.0]); grid.show(ui, |ui| { ui.heading("Action"); ui.heading("Binding #1"); ui.heading("Binding #2"); ui.heading("Binding #3"); ui.end_row(); let keybinds = match player { None => &cfg.input.shortcuts, Some(player) => &cfg.input.joypads[player as usize], }; let mut clear_bind = None; for (action, bind) in keybinds { ui.strong(action.to_string()); for (slot, input) in bind.bindings.iter().enumerate() { let button = Button::new(input.map(Input::fmt).unwrap_or_default()) // Make enough room for larger inputs like controller joysticks .min_size(Vec2::new(135.0, 0.0)); let res = ui .add(button) .on_hover_text("Click to set. Right-click to unset."); if res.clicked() { self.pending_input = Some(PendingInput { action: *action, input: None, binding: slot, conflict: None, }); } else if res.secondary_clicked() && let Some(input) = input { clear_bind = Some(input) } } ui.end_row(); } if let Some(input) = clear_bind.take() { self.tx.event(ConfigEvent::ActionBindingClear(*input)); } }); }); } fn player_gamepad_combo( &mut self, ui: &mut Ui, player: Player, connected_gamepads: Option<&[ConnectedGamepad]>, ) { ui.horizontal(|ui| { let gamepad_label = "🎮 Assigned Gamepad:"; let unassigned = "Unassigned".to_string(); match connected_gamepads { Some(gamepads) => { if gamepads.is_empty() { ui.add_enabled_ui(false, |ui| { let combo = egui::ComboBox::from_label(gamepad_label) .selected_text("No Gamepads Connected"); combo.show_ui(ui, |_| {}); }); } else { let mut assigned = gamepads .iter() .find(|gamepad| gamepad.assignment == Some(player)); let previous_assigned = assigned; let combo = egui::ComboBox::from_label(gamepad_label).selected_text( assigned .as_ref() .map_or(&unassigned, |assignment| &assignment.name), ); combo.show_ui(ui, |ui| { ui.selectable_value(&mut assigned, None, unassigned); for assignment in gamepads { ui.selectable_value( &mut assigned, Some(assignment), &assignment.name, ); } }); if previous_assigned != assigned { match &assigned { Some(gamepad) => { match assigned.as_ref().and_then(|gamepad| gamepad.assignment) { Some(player) => { self.gamepad_unassign_confirm = Some((player, player, gamepad.uuid)); } None => { self.tx.event(ConfigEvent::GamepadAssign(( player, gamepad.uuid, ))); } } } None => self.tx.event(ConfigEvent::GamepadUnassign(player)), } } } } None => { ui.add_enabled_ui(false, |ui| { let combo = egui::ComboBox::from_label(gamepad_label) .selected_text("Gamepads not supported"); combo.show_ui(ui, |_| {}); }); } } }); } pub fn show_set_keybind_window( &mut self, ctx: &Context, cfg: &Config, gamepad_events: &[(Input, ElementState)], ) { let mut set_keybind_open = self.pending_input.is_some(); let res = egui::Window::new("🖮 Set Keybind") .anchor(Align2::CENTER_CENTER, Vec2::ZERO) .collapsible(false) .resizable(false) .open(&mut set_keybind_open) .show(ctx, |ui| self.set_keybind(ui, cfg, gamepad_events)); if let Some(ref res) = res { // Force on-top focus when embedded if set_keybind_open { ctx.move_to_top(res.response.layer_id); res.response.request_focus(); } else { ctx.memory_mut(|m| m.surrender_focus(res.response.id)); } } if !set_keybind_open { self.pending_input = None; } } pub fn set_keybind( &mut self, ui: &mut Ui, cfg: &Config, gamepad_events: &[(Input, ElementState)], ) { let Some(PendingInput { action, binding, mut input, mut conflict, .. }) = self.pending_input else { return; }; if let Some(action) = conflict { ui.label(format!("Conflict with {action}.")); ui.horizontal(|ui| { if ui.button("Overwrite").clicked() { conflict = None; } if ui.button("Cancel").clicked() { self.pending_input = None; input = None; } }); } else { ui.label(format!( "Press any key on your keyboard or controller to set a new binding for {action}.", )); } match input { Some(input) => { if conflict.is_none() { self.pending_input = None; self.tx .event(ConfigEvent::ActionBindingSet((action, input, binding))); } } None => { let captured = if let Some(keybind) = &mut self.pending_input { let input = ui.input(|i| { use egui::Event; // Find first released key/button event for event in &i.events { match *event { Event::Key { physical_key: Some(key), pressed: false, modifiers, .. } => { // TODO: Ignore unsupported key mappings for now as egui supports less // overall than winit return Input::try_from((key, modifiers)).ok(); } Event::PointerButton { button, pressed: false, .. } => { return Some(Input::from(button)); } _ => (), } } for (input, state) in gamepad_events { if *state == ElementState::Released { return Some(*input); } } None }); if let Some(input) = input { keybind.input = Some(input); let binds = cfg .input .shortcuts .iter() .chain(cfg.input.joypads.iter().flatten()); for (action, bind) in binds { if bind .bindings .iter() .any(|b| b == &Some(input) && *action != keybind.action) { keybind.conflict = Some(*action); } } Some(input) } else { None } } else { None }; if let Some(input) = captured { if let Some(pending) = self.pending_input.take_if(|p| p.conflict.is_none()) { self.tx.event(ConfigEvent::ActionBindingSet(( pending.action, input, pending.binding, ))); } let dialog_viewport = ui.ctx().viewport_id(); let when = Instant::now(); self.tx.event(RendererEvent::RequestRedraw { viewport_id: dialog_viewport, when, }); if dialog_viewport != ViewportId::ROOT { self.tx.event(RendererEvent::RequestRedraw { viewport_id: ViewportId::ROOT, when, }); } } } } } fn show_gamepad_unassign_window(&mut self, ctx: &Context) { if self.gamepad_unassign_confirm.is_none() { return; } let mut gamepad_unassign_open = self.gamepad_unassign_confirm.is_some(); let res = egui::Window::new("🎮 Unassign Gamepad") .anchor(Align2::CENTER_CENTER, Vec2::ZERO) .collapsible(false) .resizable(false) .open(&mut gamepad_unassign_open) .show(ctx, |ui| self.gamepad_unassign_confirm(ui)); if let Some(ref res) = res { // Force on-top focus when embedded if gamepad_unassign_open { ctx.move_to_top(res.response.layer_id); res.response.request_focus(); } else { ctx.memory_mut(|m| m.surrender_focus(res.response.id)); } } if !gamepad_unassign_open { self.gamepad_unassign_confirm = None; } } fn gamepad_unassign_confirm(&mut self, ui: &mut Ui) { if let Some((existing_player, new_player, uuid)) = self.gamepad_unassign_confirm { ui.label(format!("Unassign gamepad from Player {existing_player}?")); ui.horizontal(|ui| { if ui.button("Yes").clicked() { self.tx.event(ConfigEvent::GamepadUnassign(existing_player)); self.tx .event(ConfigEvent::GamepadAssign((new_player, uuid))); self.gamepad_unassign_confirm = None; } if ui.button("Cancel").clicked() { self.gamepad_unassign_confirm = None; } }); } } } ================================================ FILE: tetanes/src/nes/renderer/gui/lib.rs ================================================ use crate::nes::{ config::Config, input::{Gamepads, Input}, renderer::event::{ key_from_keycode, modifiers_from_modifiers_state, pointer_button_from_mouse, }, }; use egui::{ Checkbox, Context, KeyboardShortcut, Pos2, Rect, Response, Sense, TextStyle, TextWrapMode, Ui, Widget, WidgetText, }; use std::ops::{Deref, DerefMut}; use tetanes_core::ppu; use winit::{event::ElementState, window::Window}; #[derive(Debug, Copy, Clone)] #[must_use] pub struct ViewportOptions { pub enabled: bool, pub always_on_top: bool, } #[derive(Debug, Copy, Clone)] pub enum ShowShortcut { Yes, No, } impl ShowShortcut { pub fn then(&self, f: impl FnOnce() -> T) -> Option { match self { Self::Yes => Some(f()), Self::No => None, } } } pub trait ShortcutText<'a> where Self: Sized + 'a, { fn shortcut_text(self, shortcut_text: impl Into) -> ShortcutWidget<'a, Self> { ShortcutWidget { inner: self, shortcut_text: shortcut_text.into(), phantom: std::marker::PhantomData, } } } pub fn cursor_to_zapper(x: f32, y: f32, rect: Rect) -> Option { let width = ppu::size::WIDTH as f32; let height = ppu::size::HEIGHT as f32; // Normalize x/y to 0..=1 and scale to PPU dimensions let x = ((x - rect.min.x) / rect.width()) * width; let y = ((y - rect.min.y) / rect.height()) * height; ((0.0..width).contains(&x) && (0.0..height).contains(&y)).then_some(Pos2::new(x, y)) } pub fn input_down(ui: &mut Ui, gamepads: &Gamepads, cfg: &Config, input: Input) -> bool { ui.input_mut(|i| match input { Input::Key(keycode, modifier_state) => key_from_keycode(keycode).is_some_and(|key| { let modifiers = modifiers_from_modifiers_state(modifier_state); i.key_down(key) && i.modifiers == modifiers }), Input::Button(player, button) => cfg .input .gamepad_assigned_to(player) .and_then(|uuid| gamepads.gamepad_by_uuid(&uuid)) .is_some_and(|g| g.is_pressed(button)), Input::Mouse(mouse_button) => pointer_button_from_mouse(mouse_button) .is_some_and(|pointer| i.pointer.button_down(pointer)), Input::Axis(player, axis, direction) => cfg .input .gamepad_assigned_to(player) .and_then(|uuid| gamepads.gamepad_by_uuid(&uuid)) .and_then(|g| g.axis_data(axis).map(|data| data.value())) .is_some_and(|value| { let (dir, state) = Gamepads::axis_state(value); dir == Some(direction) && state == ElementState::Pressed }), }) } #[must_use] pub struct ShortcutWidget<'a, T> { inner: T, shortcut_text: WidgetText, phantom: std::marker::PhantomData<&'a ()>, } impl Deref for ShortcutWidget<'_, T> { type Target = T; fn deref(&self) -> &Self::Target { &self.inner } } impl DerefMut for ShortcutWidget<'_, T> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } } impl Widget for ShortcutWidget<'_, T> where T: Widget, { fn ui(self, ui: &mut Ui) -> Response { ui.horizontal(|ui| { let res = self.inner.ui(ui); if !self.shortcut_text.is_empty() { let shortcut_galley = self.shortcut_text.into_galley( ui, Some(TextWrapMode::Extend), f32::INFINITY, TextStyle::Button, ); let available_rect = ui.available_rect_before_wrap(); let gap_before_shortcut_text = ui.spacing().item_spacing.x; let mut desired_size = shortcut_galley.size(); desired_size.x += gap_before_shortcut_text; // Ensure sense is set to hover so that screen readers don't try to read it, // consistent with `shortcut_text` on `Button` let (rect, _) = ui.allocate_at_least(desired_size, Sense::hover()); if ui.is_rect_visible(rect) { let text_pos = Pos2::new( available_rect.max.x - shortcut_galley.size().x, rect.center().y - 0.5 * shortcut_galley.size().y, ); ui.painter() .galley(text_pos, shortcut_galley, ui.visuals().weak_text_color()); } } res }) .inner } } #[must_use] pub struct ToggleValue<'a> { selected: &'a mut bool, text: WidgetText, } impl<'a> ToggleValue<'a> { pub fn new(selected: &'a mut bool, text: impl Into) -> Self { Self { selected, text: text.into(), } } } impl Widget for ToggleValue<'_> { fn ui(self, ui: &mut Ui) -> Response { let mut res = ui.selectable_label(*self.selected, self.text); if res.clicked() { *self.selected = !*self.selected; res.mark_changed(); } res } } #[must_use] pub struct RadioValue<'a, T> { current_value: &'a mut T, alternative: T, text: WidgetText, } impl<'a, T: PartialEq> RadioValue<'a, T> { pub fn new(current_value: &'a mut T, alternative: T, text: impl Into) -> Self { Self { current_value, alternative, text: text.into(), } } } impl Widget for RadioValue<'_, T> { fn ui(self, ui: &mut Ui) -> Response { let mut res = ui.radio(*self.current_value == self.alternative, self.text); if res.clicked() && *self.current_value != self.alternative { *self.current_value = self.alternative; res.mark_changed(); } res } } impl<'a> ShortcutText<'a> for Checkbox<'a> {} impl<'a> ShortcutText<'a> for ToggleValue<'a> {} impl<'a, T> ShortcutText<'a> for RadioValue<'a, T> {} impl TryFrom for KeyboardShortcut { type Error = (); fn try_from(val: Input) -> Result { if let Input::Key(keycode, modifier_state) = val { Ok(KeyboardShortcut { logical_key: key_from_keycode(keycode).ok_or(())?, modifiers: modifiers_from_modifiers_state(modifier_state), }) } else { Err(()) } } } pub fn screen_center(ctx: &Context) -> Option { ctx.input(|i| { let outer_rect = i.viewport().outer_rect?; let size = outer_rect.size(); let monitor_size = i.viewport().monitor_size?; if 1.0 < monitor_size.x && 1.0 < monitor_size.y { let x = (monitor_size.x - size.x) / 2.0; let y = (monitor_size.y - size.y) / 2.0; Some(Pos2::new(x, y)) } else { None } }) } pub fn screen_size_in_pixels(window: &Window) -> egui::Vec2 { let size = window.inner_size(); egui::vec2(size.width as f32, size.height as f32) } pub fn pixels_per_point(egui_ctx: &egui::Context, window: &Window) -> f32 { let native_pixels_per_point = window.scale_factor() as f32; let egui_zoom_factor = egui_ctx.zoom_factor(); egui_zoom_factor * native_pixels_per_point } pub fn inner_rect_in_points(window: &Window, pixels_per_point: f32) -> Option { let inner_pos_px = window.inner_position().ok()?; let inner_pos_px = egui::pos2(inner_pos_px.x as f32, inner_pos_px.y as f32); let inner_size_px = window.inner_size(); let inner_size_px = egui::vec2(inner_size_px.width as f32, inner_size_px.height as f32); let inner_rect_px = egui::Rect::from_min_size(inner_pos_px, inner_size_px); Some(inner_rect_px / pixels_per_point) } pub fn outer_rect_in_points(window: &Window, pixels_per_point: f32) -> Option { let outer_pos_px = window.outer_position().ok()?; let outer_pos_px = egui::pos2(outer_pos_px.x as f32, outer_pos_px.y as f32); let outer_size_px = window.outer_size(); let outer_size_px = egui::vec2(outer_size_px.width as f32, outer_size_px.height as f32); let outer_rect_px = egui::Rect::from_min_size(outer_pos_px, outer_size_px); Some(outer_rect_px / pixels_per_point) } pub fn to_winit_icon(icon: &egui::IconData) -> Option { if icon.is_empty() { None } else { match winit::window::Icon::from_rgba(icon.rgba.clone(), icon.width, icon.height) { Ok(winit_icon) => Some(winit_icon), Err(err) => { tracing::warn!("Invalid IconData: {err}"); None } } } } /// An animated dashed rectangle. pub fn animated_dashed_rect( ui: &mut Ui, rect: Rect, stroke: impl Into, dash_length: f32, gap_length: f32, ) { if ui.is_rect_visible(rect) { ui.ctx().request_repaint(); // because it is animated let rect = [ rect.left_top(), rect.right_top(), rect.right_bottom(), rect.left_bottom(), rect.left_top(), ]; let time = ui.input(|i| i.time as f32); let total_length = dash_length + gap_length; let dash_offset = (time * 10.0) % total_length; ui.painter().add(egui::Shape::dashed_line_with_offset( &rect, stroke, &[dash_length], &[gap_length], dash_offset, )); } } ================================================ FILE: tetanes/src/nes/renderer/gui/ppu_viewer.rs ================================================ use crate::nes::{ event::{DebugEvent, EmulationEvent, NesEventProxy}, renderer::{ gui::lib::{ViewportOptions, animated_dashed_rect}, painter::RenderState, texture::Texture, }, }; use egui::{ CentralPanel, Color32, Context, CursorIcon, DragValue, Grid, Image, Label, Panel, Pos2, Rect, ScrollArea, Sense, Slider, StrokeKind, Ui, Vec2, ViewportClass, ViewportId, }; use parking_lot::Mutex; use std::sync::{ Arc, atomic::{AtomicBool, Ordering}, }; use tetanes_core::{ debug::PpuDebugger, mapper::Map, ppu::{self, Ppu, addr, cycle, scanline, scroll::Scroll, sprite::Sprite}, }; #[derive(Debug)] #[must_use] struct State { tx: NesEventProxy, tab: Tab, // TODO: persist in config refresh_cycle: u16, refresh_scanline: u16, show_refresh_lines: bool, show_dividers: bool, show_tile_grid: bool, show_scroll_overlay: bool, show_attr_grid_16x: bool, show_attr_grid_32x: bool, nametables: NametablesState, pattern_tables: PatternTablesState, oam: OamState, palette: PalettesState, ppu: Ppu, } #[derive(Debug)] #[must_use] struct NametablesState { pixels: Vec, texture: Texture, zoom: f32, selected: Option, } #[derive(Debug)] #[must_use] struct PatternTablesState { pixels: Vec, texture: Texture, zoom: f32, selected: Option, } #[derive(Debug)] #[must_use] struct OamState { oam_pixels: Vec, sprite_pixels: Vec, sprites: Vec, oam_texture: Texture, sprites_texture: Texture, zoom: f32, oam_selected: Option, } #[derive(Debug)] #[must_use] struct PalettesState { size: Vec2, pixels: Vec, colors: Vec, zoom: f32, selected: Option, } #[derive(Debug, Copy, Clone)] #[must_use] struct NametableTile { index: u16, uv: Rect, col: u16, row: u16, x: u16, // 0..=248 y: u16, // 0..=232 nametable_addr: u16, tile_addr: u16, palette_index: u8, palette_addr: u16, attr_addr: u16, attr_val: u8, } impl Default for NametableTile { fn default() -> Self { Self { index: 0, uv: Rect::NOTHING, col: 0, row: 0, x: 0, y: 0, nametable_addr: 0, tile_addr: 0, palette_index: 0, palette_addr: 0, attr_addr: 0, attr_val: 0, } } } #[derive(Debug, Copy, Clone)] #[must_use] struct ChrTile { index: u16, uv: Rect, tile_addr: u16, } impl Default for ChrTile { fn default() -> Self { Self { index: 0, uv: Rect::NOTHING, tile_addr: 0, } } } #[derive(Debug, Copy, Clone)] #[must_use] struct PaletteColor { index: u8, value: u8, addr: u16, color: Color32, } impl Default for PaletteColor { fn default() -> Self { Self { index: 0, value: 0, addr: 0, color: Color32::BLACK, } } } #[derive(Debug)] #[must_use] pub struct PpuViewer { pub id: ViewportId, open: Arc, state: Arc>, } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] pub enum Tab { #[default] Nametables, PatternTables, Oam, Palette, } impl PpuViewer { const TITLE: &'static str = "TetaNES - PPU Viewer"; pub fn new(tx: NesEventProxy, render_state: &mut RenderState) -> Self { Self { id: ViewportId::from_hash_of(Self::TITLE), open: Arc::new(AtomicBool::new(false)), state: Arc::new(Mutex::new(State { tx, tab: Tab::default(), refresh_cycle: 0, refresh_scanline: scanline::VBLANK_NTSC, show_refresh_lines: false, show_dividers: true, show_tile_grid: false, show_scroll_overlay: false, show_attr_grid_16x: false, show_attr_grid_32x: false, nametables: NametablesState { // 4 nametables with 4 color channels (RGBA) pixels: vec![0x00; 4 * 4 * ppu::size::FRAME], texture: Texture::new( render_state, 2.0 * Vec2::new(ppu::size::WIDTH as f32, ppu::size::HEIGHT as f32), 1.0, Some("nes nametables"), ), zoom: 1.5, selected: None, }, pattern_tables: PatternTablesState { // 2 pattern tables with 4 color channels (RGBA) pixels: vec![0x00; 2 * 4 * ppu::size::FRAME], texture: Texture::new( render_state, Vec2::new(ppu::size::WIDTH as f32, ppu::size::WIDTH as f32 / 2.0), 1.0, Some("nes pattern tables"), ), zoom: 3.0, selected: None, }, oam: OamState { // 64 8x8 sprites with 4 color channels (RGBA) oam_pixels: vec![0x00; 64 * 8 * 8 * 4], // 1 nametable with 4 color channels (RGBA) sprite_pixels: vec![0x00; 4 * ppu::size::FRAME], // 64 sprites sprites: vec![Sprite::new(); 64], oam_texture: Texture::new( render_state, Vec2::splat(64.0), 1.0, Some("nes oam"), ), sprites_texture: Texture::new( render_state, Vec2::new(ppu::size::WIDTH as f32, ppu::size::HEIGHT as f32), 1.0, Some("nes sprites"), ), zoom: 3.0, oam_selected: None, }, palette: PalettesState { // 2 palette tables size: Vec2::new(64.0, 32.0), // 32 palette colors with 4 color channels (RGBA) pixels: vec![0x00; 4 * 32], // 32 colors colors: vec![0x00; 32], zoom: 3.0, selected: None, }, ppu: Ppu::default(), })), } } pub const fn id(&self) -> ViewportId { self.id } pub fn open(&self) -> bool { self.open.load(Ordering::Acquire) } pub fn set_open(&self, open: bool, ctx: &Context) { self.open.store(open, Ordering::Release); self.state.lock().update_debugger(open); if open { ctx.send_viewport_cmd_to(self.id, egui::ViewportCommand::Close); } } pub fn toggle_open(&self, ctx: &Context) { let Ok(open) = self .open .fetch_update(Ordering::Release, Ordering::Acquire, |open| Some(!open)) else { return; }; self.state.lock().update_debugger(!open); if open { ctx.send_viewport_cmd_to(self.id, egui::ViewportCommand::Close); } } pub fn update_ppu(&mut self, queue: &wgpu::Queue, ppu: Ppu) { let mut state = self.state.lock(); match state.tab { Tab::Nametables => { ppu.load_nametables(&mut state.nametables.pixels); let mut pixels = std::mem::take(&mut state.palette.pixels); let mut colors = std::mem::take(&mut state.palette.colors); ppu.load_palettes(&mut pixels, &mut colors); state.palette.pixels = pixels; state.palette.colors = colors; state .nametables .texture .update(queue, &state.nametables.pixels); } Tab::PatternTables => { ppu.load_pattern_tables(&mut state.pattern_tables.pixels); state .pattern_tables .texture .update(queue, &state.pattern_tables.pixels); } Tab::Oam => { let mut oam_pixels = std::mem::take(&mut state.oam.oam_pixels); let mut sprite_pixels = std::mem::take(&mut state.oam.sprite_pixels); let mut sprites = std::mem::take(&mut state.oam.sprites); // Clear to black each frame sprite_pixels.chunks_mut(4).for_each(|chunk| { chunk[0] = 0; chunk[1] = 0; chunk[2] = 0; chunk[3] = 255; }); ppu.load_oam(&mut oam_pixels, &mut sprite_pixels, &mut sprites); state.oam.oam_pixels = oam_pixels; state.oam.sprite_pixels = sprite_pixels; state.oam.sprites = sprites; state.oam.oam_texture.update(queue, &state.oam.oam_pixels); state .oam .sprites_texture .update(queue, &state.oam.sprite_pixels); } Tab::Palette => { let mut pixels = std::mem::take(&mut state.palette.pixels); let mut colors = std::mem::take(&mut state.palette.colors); ppu.load_palettes(&mut pixels, &mut colors); state.palette.pixels = pixels; state.palette.colors = colors; } } state.ppu = ppu; } pub fn show(&mut self, ui: &mut Ui, opts: ViewportOptions) { if !self.open.load(Ordering::Relaxed) { return; } let open = Arc::clone(&self.open); let state = Arc::clone(&self.state); let mut viewport_builder = egui::ViewportBuilder::default() .with_title(Self::TITLE) .with_inner_size(Vec2::new(1024.0, 768.0)); if opts.always_on_top { viewport_builder = viewport_builder.with_always_on_top(); } ui.show_viewport_deferred(self.id, viewport_builder, move |ui, class| { if class == ViewportClass::EmbeddedWindow { let mut window_open = open.load(Ordering::Acquire); egui::Window::new(PpuViewer::TITLE) .open(&mut window_open) .show(ui, |ui| state.lock().ui(ui, opts.enabled)); open.store(window_open, Ordering::Release); } else { CentralPanel::default().show_inside(ui, |ui| state.lock().ui(ui, opts.enabled)); if ui.input(|i| i.viewport().close_requested()) { open.store(false, Ordering::Release); } } }); } } impl State { fn update_debugger(&self, open: bool) { let tx = self.tx.clone(); let debugger = PpuDebugger { cycle: self.refresh_cycle, scanline: self.refresh_scanline, callback: Arc::new(move |ppu| tx.event(DebugEvent::Ppu(Box::new(ppu)))), }; self.tx.event(if open { EmulationEvent::AddDebugger(debugger.into()) } else { EmulationEvent::RemoveDebugger(debugger.into()) }); } fn ui(&mut self, ui: &mut Ui, enabled: bool) { ui.add_enabled_ui(enabled, |ui| { Panel::top("ppu_viewer_menubar").show_inside(ui, |ui| { ui.horizontal(|ui| { ui.selectable_value(&mut self.tab, Tab::Nametables, "Nametables"); ui.selectable_value(&mut self.tab, Tab::PatternTables, "Pattern Tables"); ui.selectable_value(&mut self.tab, Tab::Oam, "OAM"); ui.selectable_value(&mut self.tab, Tab::Palette, "Palette"); }); }); match self.tab { Tab::Nametables => self.nametables_tab(ui), Tab::PatternTables => self.pattern_tables_tab(ui), Tab::Oam => self.oam_tab(ui), Tab::Palette => self.palette_tab(ui), } }); } fn grid_settings(&mut self, ui: &mut Ui) { let res = ui .checkbox(&mut self.show_dividers, "Table Dividers") .on_hover_text("Show divider lines between tables."); if res.changed() { // TODO: update config } let res = ui .checkbox(&mut self.show_tile_grid, "Tile Grid") .on_hover_text("Show grid lines between tiles."); if res.changed() { // TODO: update config } } fn general_settings(&mut self, ui: &mut Ui) { ui.strong("Refresh on:") .on_hover_cursor(CursorIcon::Help) .on_hover_text("Change which PPU cycle/scanline viewer state refreshes on."); ui.indent("refresh_settings", |ui| { ui.horizontal(|ui| { let drag = DragValue::new(&mut self.refresh_cycle) .range(0..=cycle::END) .suffix(" cycle"); let res = ui.add(drag); if res.changed() { self.update_debugger(true); } }); ui.horizontal(|ui| { let drag = DragValue::new(&mut self.refresh_scanline) .range(0..=self.ppu.prerender_scanline) .suffix(" scanline"); let res = ui.add(drag); if res.changed() { self.update_debugger(true); } }); }); } fn nametables_tab(&mut self, ui: &mut Ui) { Panel::right("nametable_panel").show_inside(ui, |ui| { ScrollArea::vertical().show(ui, |ui| { ui.add_space(12.0); ui.heading("Nametable Info"); ui.separator(); let grid = Grid::new("nametables_info") .num_columns(2) .spacing([40.0, 6.0]); grid.show(ui, |ui| { ui.strong("Mirroring:"); ui.label(format!("{:?}", self.ppu.mirroring())); ui.end_row(); }); ui.add_space(16.0); ui.heading("Selected Tile"); ui.separator(); self.nametable_tile(ui, "nametable_tile_selected", self.nametables.selected); ui.add_space(16.0); ui.separator(); ui.collapsing("Settings", |ui| { self.general_settings(ui); let res = ui .checkbox(&mut self.show_refresh_lines, "Refresh Markers") .on_hover_text( "Show lines indicating the current refresh cycle and scanline.", ); if res.changed() { // TODO: update config } self.grid_settings(ui); let res = ui .checkbox(&mut self.show_scroll_overlay, "Scroll Overlay") .on_hover_text("Show scroll position overlay."); if res.changed() { // TODO: update config } let res = ui .checkbox(&mut self.show_attr_grid_16x, "Attribute Grid (16x16)") .on_hover_text("Show grid lines within each attribute block."); if res.changed() { // TODO: update config } let res = ui .checkbox(&mut self.show_attr_grid_32x, "Attribute Grid (32x32)") .on_hover_text("Show grid lines between attribute blocks."); if res.changed() { // TODO: update config } zoom_slider(ui, &mut self.nametables.zoom); }); }); }); let texture_size = self.nametables.texture.size; CentralPanel::default().show_inside(ui, |ui| { let scroll = ScrollArea::both() .min_scrolled_width(texture_size.x) .min_scrolled_height(texture_size.y); scroll.show(ui, |ui| { let image = Image::from_texture(self.nametables.texture.sized()) .fit_to_exact_size(self.nametables.zoom * texture_size) .sense(Sense::click()); let res = ui.add(image).on_hover_cursor(CursorIcon::Cell); let image_rect = res.rect; if let Some(pos) = res.hover_pos() && image_rect.contains(pos) { self.nametable_hover(ui, &res, pos); } if self.show_dividers { // Split the 4x4 nametables in half vertically and horizontally ui.painter().vline( image_rect.center().x, image_rect.y_range(), (1.0, Color32::WHITE), ); ui.painter().hline( image_rect.x_range(), image_rect.center().y, (1.0, Color32::WHITE), ); } if self.show_refresh_lines { let cycle_offset = self.refresh_cycle as f32 * image_rect.size().x / 2.0 / cycle::END as f32; let scanline_offset = self.refresh_scanline as f32 * image_rect.size().y / 2.0 / self.ppu.prerender_scanline as f32; ui.painter().vline( image_rect.left() + cycle_offset, image_rect.y_range(), (1.0, Color32::RED), ); ui.painter().vline( image_rect.center().x + cycle_offset, image_rect.y_range(), (1.0, Color32::RED), ); ui.painter().hline( image_rect.x_range(), image_rect.top() + scanline_offset, (1.0, Color32::GREEN), ); ui.painter().hline( image_rect.x_range(), image_rect.center().y + scanline_offset, (1.0, Color32::GREEN), ); } if self.show_tile_grid { paint_grid(ui, image_rect, 60.0, 64.0, Color32::LIGHT_BLUE); } if self.show_attr_grid_16x { paint_grid(ui, image_rect, 30.0, 32.0, Color32::LIGHT_RED); } if self.show_attr_grid_32x { // Because 32x doesn't divide evenly into 240, split this up into two passes with a // dividing line, forcing the leftover attribute space to be at the bottom. Also // halve the number of rows let top_rect = Rect::from_min_max(image_rect.min, image_rect.right_center()); let bot_rect = Rect::from_min_max(image_rect.left_center(), image_rect.right_bottom()); paint_grid(ui, top_rect, 7.5, 16.0, Color32::LIGHT_GREEN); ui.painter().hline( top_rect.x_range(), top_rect.bottom(), (1.0, Color32::LIGHT_GREEN), ); paint_grid(ui, bot_rect, 7.5, 16.0, Color32::LIGHT_GREEN); } if self.show_scroll_overlay { self.nametable_scroll_overlay(ui, image_rect); } if let Some(offset) = self.nametables.selected { let selection = tile_selection(image_rect, self.nametables.texture.size, offset); animated_dashed_rect(ui, selection, (1.0, Color32::WHITE), 3.0, 3.0); } }); }); } fn nametable_hover(&mut self, ui: &mut Ui, res: &egui::Response, pos: Pos2) { let image_rect = res.rect; let texture_size = self.nametables.texture.size; let offset = translate_screen_pos_to_tile(pos, image_rect, texture_size); let selection = tile_selection(image_rect, texture_size, offset); animated_dashed_rect( ui, selection, (1.0, Color32::from_white_alpha(220)), 3.0, 3.0, ); res.clone().on_hover_ui_at_pointer(|ui| { self.nametable_tile(ui, "nametable_tile_hover", Some(offset)); }); if res.clicked() { self.nametables.selected = Some(offset); } } fn nametable_tile_from_offset(&self, offset: Vec2, texture_size: Vec2) -> NametableTile { let Vec2 { x, y } = offset; // Get row/column 8x8 tile and the nametable it's in let mut col = x as u16 / 8; let mut row = y as u16 / 8; let nametable = if col >= 32 { 1 } else { 0 } | if row >= 30 { 2 } else { 0 }; // Wrap row/column to a single nametable col &= 31; if row >= 30 { // Not a power of two, so can't bitwise & row -= 30; } let nametable_index = (row << 5) + col; let base_nametable_addr = addr::NAMETABLE_START | (nametable * ppu::size::NAMETABLE); let base_attr_addr = base_nametable_addr + addr::ATTR_OFFSET; let nametable_addr = base_nametable_addr + nametable_index; let tile_index = u16::from(self.ppu.mapper.chr_peek(nametable_addr, &self.ppu.ciram)); let tile_addr = self.ppu.ctrl.bg_select + (tile_index << 4); let supertile = ((row & 0xFC) << 1) + (col >> 2); let attr_addr = base_attr_addr + supertile; let attr_val = self.ppu.mapper.chr_peek(attr_addr, &self.ppu.ciram); let attr_shift = (col & 0x02) | ((row & 0x02) << 1); // TODO: handle mmc5 extended attributes let palette_addr = ((attr_val >> attr_shift) & 0x03) << 2; let palette_index = palette_addr >> 2; let palette_addr = addr::PALETTE_START + u16::from(palette_addr); let tile_uv = Rect::from_min_size( (Vec2::new(x, y) / texture_size).to_pos2(), Vec2::splat(8.0) / texture_size, ); let x = (x as u16) % ppu::size::WIDTH; let y = (y as u16) % ppu::size::HEIGHT; NametableTile { index: tile_index, uv: tile_uv, col, row, x, y, nametable_addr, tile_addr, palette_index, palette_addr, attr_addr, attr_val, } } fn nametable_tile(&mut self, ui: &mut Ui, label: &str, offset: Option) { let tile = offset .map(|offset| self.nametable_tile_from_offset(offset, self.nametables.texture.size)); let NametableTile { uv, index, col, row, x, y, nametable_addr, tile_addr, palette_index, palette_addr, attr_addr, attr_val, .. } = tile.unwrap_or_default(); let grid = Grid::new(label).num_columns(2).spacing([40.0, 6.0]); grid.show(ui, |ui| { ui.strong("Tile:"); let tile_image = Image::from_texture(self.nametables.texture.sized()) .uv(uv) .maintain_aspect_ratio(false) // Ignore original aspect ratio .fit_to_exact_size(Vec2::splat(64.0)) .sense(Sense::click()); ui.add(tile_image); ui.end_row(); ui.strong("Palette:"); if tile.is_some() { self.palette_row( ui, palette_index.into(), ui.cursor().min, Vec2::splat(16.0), true, ); } ui.end_row(); ui.strong("Column, Row:"); if tile.is_some() { ui.label(format!("{col}, {row}")); } ui.end_row(); ui.strong("X, Y:"); if tile.is_some() { ui.label(format!("{x}, {y}")); } ui.end_row(); ui.strong("Nametable Address:"); if tile.is_some() { ui.label(format!("${nametable_addr:04X}")); } ui.end_row(); ui.strong("Tile Index:"); if tile.is_some() { ui.label(format!("${index:02X}")); } ui.end_row(); ui.strong("Tile Address:"); if tile.is_some() { ui.label(format!("${tile_addr:04X}")); } ui.end_row(); ui.strong("Palette Index:"); if tile.is_some() { ui.label(format!("{palette_index}")); } ui.end_row(); ui.strong("Palette Address:"); if tile.is_some() { ui.label(format!("${palette_addr:04X}")); } ui.end_row(); ui.strong("Attribute Address:"); if tile.is_some() { ui.label(format!("${attr_addr:04X}")); } ui.end_row(); ui.strong("Attribute Value:"); if tile.is_some() { ui.label(format!("${attr_val:02X}")); } ui.end_row(); }); } fn nametable_scroll_overlay(&self, ui: &mut Ui, image_rect: Rect) { let Ppu { cycle, scanline, vblank_scanline, prerender_scanline, scroll, .. } = self.ppu; let use_scroll_t = scanline >= vblank_scanline || (scanline == scanline::VISIBLE_END && cycle >= cycle::SPR_EVAL_END) || (scanline == prerender_scanline && cycle < cycle::BG_PREFETCH_START + 7); let scroll_v = if use_scroll_t { scroll.t } else { scroll.v }; let mut scroll_x = ((scroll_v & Scroll::COARSE_X_MASK) << 3) | (((scroll_v & Scroll::NT_X_MASK) >> 10) * ppu::size::WIDTH); let scroll_y = ((scroll_v & Scroll::COARSE_Y_MASK) >> 2) | (((scroll_v & Scroll::NT_Y_MASK) >> 11) * ppu::size::HEIGHT) | ((scroll_v & Scroll::FINE_Y_MASK) >> 12); if use_scroll_t { scroll_x |= scroll.fine_x; } else { // During rendering, subtract according to current cycle/scanline if cycle <= scanline::VISIBLE_END { if cycle >= 8 { scroll_x = scroll_x.saturating_sub(cycle & !0x07); } // Adjust for 2x increments at end of last scanline scroll_x = scroll_x.saturating_sub(16); } else if cycle >= cycle::BG_PREFETCH_START + 7 { scroll_x = scroll_x.saturating_sub(8); if cycle >= cycle::BG_PREFETCH_END { scroll_x = scroll_x.saturating_sub(8); } } scroll_x += scroll.fine_x; } // Scroll overlay let nametable_size = image_rect.size() / 2.0; // Translate scroll_x/scroll_y to image space let scroll = Vec2::new(scroll_x as f32, scroll_y as f32) * image_rect.size() / self.nametables.texture.size; let scroll_min = image_rect.min + scroll; let scroll_max = scroll_min + nametable_size; let overlay = Rect::from_min_max(scroll_min, scroll_max.min(image_rect.max)); ui.painter().rect( overlay, 0.0, Color32::from_black_alpha(75), (1.0, Color32::WHITE), egui::StrokeKind::Inside, ); // Wrap overlay around the right/bottom edge let Vec2 { x, y } = scroll_max - image_rect.max; let wrapped_size = Vec2::new( if x > 0.0 { x } else { nametable_size.x }, if y > 0.0 { y } else { nametable_size.y }, ); if wrapped_size.max_elem() > 0.0 { ui.painter().rect( Rect::from_min_size(image_rect.min, wrapped_size), 0.0, Color32::from_black_alpha(75), (1.0, Color32::WHITE), egui::StrokeKind::Inside, ); } } fn pattern_tables_tab(&mut self, ui: &mut Ui) { Panel::right("pattern_tables_panel").show_inside(ui, |ui| { ScrollArea::vertical().show(ui, |ui| { ui.add_space(12.0); ui.heading("Selected Tile"); ui.separator(); self.pattern_tables_tile( ui, "pattern_tables_tile_selected", self.pattern_tables.selected, ); ui.add_space(16.0); ui.separator(); ui.collapsing("Settings", |ui| { self.general_settings(ui); self.grid_settings(ui); // TODO: Selectable palette/last known palette zoom_slider(ui, &mut self.pattern_tables.zoom); }); }); }); let texture_size = self.pattern_tables.texture.size; CentralPanel::default().show_inside(ui, |ui| { let scroll = ScrollArea::both() .min_scrolled_width(texture_size.x) .min_scrolled_height(texture_size.y); scroll.show(ui, |ui| { let image = Image::from_texture(self.pattern_tables.texture.sized()) .fit_to_exact_size(self.pattern_tables.zoom * texture_size) .sense(Sense::click()); let res = ui.add(image).on_hover_cursor(CursorIcon::Cell); let image_rect = res.rect; if let Some(pos) = res.hover_pos() && image_rect.contains(pos) { self.pattern_tables_hover(ui, &res, pos); } if self.show_dividers { ui.painter().vline( image_rect.center().x, image_rect.y_range(), (1.0, Color32::WHITE), ); } if self.show_tile_grid { paint_grid(ui, image_rect, 16.0, 32.0, Color32::LIGHT_BLUE); } if let Some(offset) = self.pattern_tables.selected { let selection = tile_selection(image_rect, self.pattern_tables.texture.size, offset); animated_dashed_rect(ui, selection, (1.0, Color32::WHITE), 3.0, 3.0); } }); }); } fn pattern_tables_hover(&mut self, ui: &mut Ui, res: &egui::Response, pos: Pos2) { let image_rect = res.rect; let texture_size = self.pattern_tables.texture.size; let offset = translate_screen_pos_to_tile(pos, image_rect, texture_size); let selection = tile_selection(image_rect, texture_size, offset); animated_dashed_rect( ui, selection, (1.0, Color32::from_white_alpha(220)), 3.0, 3.0, ); res.clone().on_hover_ui_at_pointer(|ui| { self.pattern_tables_tile(ui, "pattern_tables_tile_hover", Some(offset)); }); if res.clicked() { self.pattern_tables.selected = Some(offset); } } fn pattern_chr_tile_from_offset(&self, offset: Vec2, texture_size: Vec2) -> ChrTile { let Vec2 { x, y } = offset; // Get row/column 8x8 tile and the pattern table it's in let mut col = x as u16 / 8; let row = y as u16 / 8; let pattern_table = if col >= 16 { 1 } else { 0 }; // Wrap column to a single pattern table col &= 15; let tile_uv = Rect::from_min_size( (Vec2::new(x, y) / texture_size).to_pos2(), Vec2::splat(8.0) / texture_size, ); let tile_addr = (pattern_table << 12) | ((col + (row << 4)) << 4); ChrTile { index: (tile_addr >> 4) & 0xFF, uv: tile_uv, tile_addr, } } fn pattern_tables_tile(&mut self, ui: &mut Ui, label: &str, offset: Option) { let tile = offset.map(|offset| { self.pattern_chr_tile_from_offset(offset, self.pattern_tables.texture.size) }); let ChrTile { uv, index, tile_addr, .. } = tile.unwrap_or_default(); let grid = Grid::new(label).num_columns(2).spacing([40.0, 6.0]); grid.show(ui, |ui| { ui.strong("Tile:"); let tile_image = Image::from_texture(self.pattern_tables.texture.sized()) .uv(uv) .maintain_aspect_ratio(false) // Ignore original aspect ratio .fit_to_exact_size(Vec2::splat(64.0)) .sense(Sense::click()); ui.add(tile_image); ui.end_row(); ui.strong("Tile Index:"); if tile.is_some() { ui.label(format!("${index:02X}")); } ui.end_row(); ui.strong("Tile Address:"); if tile.is_some() { ui.label(format!("${tile_addr:04X}")); } ui.end_row(); }); } fn oam_tab(&mut self, ui: &mut Ui) { Panel::right("oam_panel").show_inside(ui, |ui| { ScrollArea::vertical().show(ui, |ui| { ui.add_space(12.0); ui.heading("Selected Tile"); ui.separator(); self.oam_tile(ui, "oam_selected", self.oam.oam_selected); ui.add_space(16.0); ui.separator(); ui.collapsing("Settings", |ui| { self.general_settings(ui); let res = ui .checkbox(&mut self.show_tile_grid, "Tile Grid") .on_hover_text("Show grid lines between tiles."); if res.changed() { // TODO: update config } zoom_slider(ui, &mut self.oam.zoom); }); }); }); CentralPanel::default().show_inside(ui, |ui| { let scroll = ScrollArea::both() .min_scrolled_width(self.oam.oam_texture.size.x) .min_scrolled_height(self.oam.oam_texture.size.y); scroll.show(ui, |ui| { ui.horizontal(|ui| { // Draw OAM tiles let image = Image::from_texture(self.oam.oam_texture.sized()) .fit_to_exact_size(2.0 * self.oam.zoom * self.oam.oam_texture.size) .sense(Sense::click()); let res = ui.add(image).on_hover_cursor(CursorIcon::Cell); let oam_image_rect = res.rect; if let Some(pos) = res.hover_pos() && oam_image_rect.contains(pos) { self.oam_hover(ui, &res, pos); } if self.show_tile_grid { paint_grid(ui, oam_image_rect, 8.0, 8.0, Color32::LIGHT_BLUE); } let image = Image::from_texture(self.oam.sprites_texture.sized()) // match OAM size .shrink_to_fit() .sense(Sense::click()); let res = ui.add(image).on_hover_cursor(CursorIcon::Cell); let spr_image_rect = res.rect; if let Some(pos) = res.hover_pos() && spr_image_rect.contains(pos) { self.sprites_hover(ui, &res, pos); } if self.show_tile_grid { paint_grid(ui, spr_image_rect, 30.0, 32.0, Color32::LIGHT_BLUE); } if let Some(offset) = self.oam.oam_selected { let selection = tile_selection(oam_image_rect, self.oam.oam_texture.size, offset); animated_dashed_rect(ui, selection, (1.0, Color32::WHITE), 3.0, 3.0); let sprite_index = (offset.x / 8.0) as usize + (offset.y / 8.0) as usize * 8; let sprite = self.oam.sprites.get(sprite_index); if let Some(sprite) = sprite { let offset = Vec2::new( ((sprite.x as f32) / 8.0).floor() * 8.0, ((sprite.y as f32) / 8.0).floor() * 8.0, ); if offset.x < ppu::size::WIDTH as f32 && offset.y < ppu::size::HEIGHT as f32 { let selection = tile_selection( spr_image_rect, self.oam.sprites_texture.size, offset, ); animated_dashed_rect( ui, selection, (1.0, Color32::WHITE), 3.0, 3.0, ); } } } }); }); }); } fn oam_hover(&mut self, ui: &mut Ui, res: &egui::Response, pos: Pos2) { let image_rect = res.rect; let texture_size = self.oam.oam_texture.size; let offset = translate_screen_pos_to_tile(pos, image_rect, texture_size); let selection = tile_selection(image_rect, texture_size, offset); animated_dashed_rect( ui, selection, (1.0, Color32::from_white_alpha(220)), 3.0, 3.0, ); let sprite_index = (offset.x / 8.0) as usize + (offset.y / 8.0) as usize * 8; let sprite = self.oam.sprites.get(sprite_index); if sprite.is_some() { res.clone().on_hover_ui_at_pointer(|ui| { self.oam_tile(ui, "oam_hover", Some(offset)); }); if res.clicked() { self.oam.oam_selected = Some(offset); } } } fn sprites_hover(&mut self, ui: &mut Ui, res: &egui::Response, pos: Pos2) { let image_rect = res.rect; let texture_size = self.oam.sprites_texture.size; let offset = translate_screen_pos_to_tile(pos, image_rect, texture_size); let selection = tile_selection(image_rect, texture_size, offset); animated_dashed_rect( ui, selection, (1.0, Color32::from_white_alpha(220)), 3.0, 3.0, ); let sprite_index = self.oam.sprites.iter().position(|sprite| { let grid_x = sprite.x as f32 / 8.0; let grid_y = sprite.y as f32 / 8.0; let x_min = grid_x.floor() * 8.0; let x_max = grid_x.ceil() * 8.0; let y_min = grid_y.floor() * 8.0; let y_max = grid_y.ceil() * 8.0; (x_min..=x_max).contains(&offset.x) && (y_min..=y_max).contains(&offset.y) }); if let Some(index) = sprite_index { let offset = Vec2::new((index % 8) as f32, (index / 8) as f32) * 8.0; res.clone().on_hover_ui_at_pointer(|ui| { self.oam_tile(ui, "oam_hover", Some(offset)); }); if res.clicked() { self.oam.oam_selected = Some(offset); } } } fn oam_tile(&mut self, ui: &mut Ui, label: &str, offsets: Option) { let tile = offsets.map(|offset| self.oam_tile_from_offset(offset, self.oam.oam_texture.size)); let ChrTile { uv, index, tile_addr, .. } = tile.unwrap_or_default(); let grid = Grid::new(label).num_columns(2).spacing([40.0, 6.0]); grid.show(ui, |ui| { ui.strong("Tile:"); let tile_image = Image::from_texture(self.oam.oam_texture.sized()) .uv(uv) .maintain_aspect_ratio(false) // Ignore original aspect ratio .fit_to_exact_size(Vec2::splat(64.0)) .sense(Sense::click()); ui.add(tile_image); ui.end_row(); ui.strong("Tile Index:"); if tile.is_some() { ui.label(format!("${index:02X}")); } ui.end_row(); ui.strong("Tile Address:"); if tile.is_some() { ui.label(format!("${tile_addr:04X}")); } ui.end_row(); // TODO: sprite index, palete address, position, horizontal/vertical flip/backgroud // priority, palette row }); } fn oam_tile_from_offset(&self, offset: Vec2, texture_size: Vec2) -> ChrTile { let Vec2 { x, y } = offset; // Get row/column 8x8 tile let col = x as u16 / 8; let row = y as u16 / 8; let tile_uv = Rect::from_min_size( (Vec2::new(x, y) / texture_size).to_pos2(), Vec2::splat(8.0) / texture_size, ); let index = col + (row * 8); ChrTile { index, uv: tile_uv, tile_addr: self.oam.sprites[index as usize].tile_addr, } } fn palette_tab(&mut self, ui: &mut Ui) { Panel::right("palette_panel").show_inside(ui, |ui| { ScrollArea::vertical().show(ui, |ui| { ui.add_space(12.0); ui.heading("Selected Color"); ui.separator(); self.palette(ui, "palette_info_selected", self.palette.selected); }); }); CentralPanel::default().show_inside(ui, |ui| { ScrollArea::both().show(ui, |ui| { ui.horizontal(|ui| { let res = self .palette_grid(ui, 4.0 * self.palette.zoom * self.palette.size) .on_hover_cursor(CursorIcon::Cell); let palette_rect = res.rect; if let Some(pos) = res.hover_pos() && palette_rect.contains(pos) { self.palette_hover(ui, &res, pos); } if let Some(offset) = self.palette.selected { let selection = tile_selection(palette_rect, self.palette.size, offset); animated_dashed_rect(ui, selection, (1.0, Color32::WHITE), 3.0, 3.0); } }); }); }); } fn palette_hover(&mut self, ui: &mut Ui, res: &egui::Response, pos: Pos2) { let image_rect = res.rect; let offset = translate_screen_pos_to_tile(pos, image_rect, self.palette.size); let selection = tile_selection(image_rect, self.palette.size, offset); animated_dashed_rect( ui, selection, (1.0, Color32::from_white_alpha(220)), 3.0, 3.0, ); res.clone().on_hover_ui_at_pointer(|ui| { self.palette(ui, "palette_hover", Some(offset)); }); if res.clicked() { self.palette.selected = Some(offset); } } fn palette_color_from_offset(&self, offset: Vec2) -> PaletteColor { let Vec2 { x, y } = offset; // Get row/column 32x32 palette and the palette table it's in let mut col = x as u16 / 8; let row = y as u16 / 8; let palette = if col >= 4 { 1 } else { 0 }; // Wrap column to a single palette table col &= 3; let index = col + row * 4; let color_index = palette * 0x10 + index; let pixel_idx = color_index as usize * 4; PaletteColor { index: index as u8, addr: addr::PALETTE_START + color_index, value: self.palette.colors[color_index as usize], color: if let [red, green, blue] = self.palette.pixels[pixel_idx..pixel_idx + 3] { Color32::from_rgb(red, green, blue) } else { Color32::default() }, } } fn palette(&mut self, ui: &mut Ui, label: &str, offset: Option) { let palette = offset.map(|offset| self.palette_color_from_offset(offset)); let PaletteColor { index, value, color, addr, .. } = palette.unwrap_or_default(); let grid = Grid::new(label).num_columns(2).spacing([40.0, 6.0]); grid.show(ui, |ui| { ui.strong("Color:"); let (rect, _) = ui.allocate_exact_size(Vec2::splat(32.0), Sense::hover()); ui.painter().rect_filled(rect, 1.0, color); ui.end_row(); ui.strong("Index:"); if palette.is_some() { ui.label(format!("${index:02X}")); } ui.end_row(); ui.strong("Value:"); if palette.is_some() { ui.label(format!("${value:02X}")); } ui.end_row(); ui.strong("Palette Address:"); if palette.is_some() { ui.label(format!("${addr:02X}")); } ui.end_row(); ui.strong("Hex:"); if palette.is_some() { ui.label(&color.to_hex()[0..7]); // Truncate the alpha channel } ui.end_row(); ui.strong("RGB:"); if palette.is_some() { let (r, g, b, _) = &color.to_tuple(); ui.label(format!("({r:03}, {g:03}, {b:03})")); } ui.end_row(); }); } fn palette_row(&self, ui: &mut Ui, index: usize, pos: Pos2, size: Vec2, show_backdrop: bool) { for x in 0..4 { let mut idx = (index * 4 + x) * 4; if show_backdrop && x == 0 { idx = 0; } if let [red, green, blue] = self.palette.pixels[idx..idx + 3] { let pos = pos + Vec2::new(x as f32 * size.x, 0.0); let rect = Rect::from_min_max(pos, pos + size); ui.painter() .rect_filled(rect, 0.0, Color32::from_rgb(red, green, blue)); } } } fn palette_grid(&self, ui: &mut Ui, size: Vec2) -> egui::Response { ui.vertical(|ui| { ui.horizontal(|ui| { let res = ui.add(Label::new("Background")); ui.add_space(size.x / 2.0 - res.rect.width()); ui.add(Label::new("Sprites")); }); let (rect, res) = ui.allocate_exact_size(size, Sense::click()); ui.painter() .rect_stroke(rect, 0.0, (1.0, Color32::BLACK), StrokeKind::Inside); let size = Vec2::new(size.x / 8.0, size.y / 4.0); for offset in [0, 4] { for (y, index) in (offset..offset + 4).enumerate() { let pos = rect.min + Vec2::new(offset as f32 * size.x, y as f32 * size.y).floor(); self.palette_row(ui, index, pos, size, false); } } res }) .inner } } /// A Zoom slider fn zoom_slider(ui: &mut Ui, zoom: &mut f32) { ui.horizontal(|ui| { let drag = Slider::new(zoom, 0.1..=5.0).step_by(0.05).suffix("x"); let res = ui.add(drag); if res.changed() { // TODO: update config } ui.label("Zoom") .on_hover_cursor(CursorIcon::Help) .on_hover_text("Zoom preview in or out."); }); } /// A grid overlay. fn paint_grid(ui: &mut Ui, rect: Rect, y_spacing: f32, x_spacing: f32, color: Color32) { let min = rect.min; let max = rect.max; let size = rect.size(); let x_increment = size.x / x_spacing; let mut x = min.x + x_increment; while x < max.x { ui.painter().vline(x, rect.y_range(), (1.0, color)); x += x_increment; } let y_increment = size.y / y_spacing; let mut y = min.y + y_increment; while y < max.y { ui.painter().hline(rect.x_range(), y, (1.0, color)); y += y_increment; } } /// Translate position in screen space to texture space and find containing 8x8 tile offset fn translate_screen_pos_to_tile(pos: Pos2, image_rect: Rect, texture_size: Vec2) -> Vec2 { let normalized_pos = (pos - image_rect.min) / image_rect.size(); let texture_pos = normalized_pos * texture_size; (texture_pos / 8.0).floor() * 8.0 } /// Return tile selection rectangle given an offset. fn tile_selection(image_rect: Rect, texture_size: Vec2, tile_offset: Vec2) -> Rect { let scale = image_rect.size() / texture_size; Rect::from_min_size( image_rect.min + scale * tile_offset, scale * Vec2::splat(8.0), ) } ================================================ FILE: tetanes/src/nes/renderer/gui/preferences.rs ================================================ use crate::{ feature, nes::{ action::Setting, config::{AudioConfig, Config, EmulationConfig, RendererConfig}, event::{ConfigEvent, NesEventProxy, UiEvent}, renderer::{ gui::{ MessageType, lib::{RadioValue, ShortcutText, ShowShortcut, ViewportOptions}, }, shader::Shader, }, }, }; use egui::{ Align, CentralPanel, Checkbox, Context, CursorIcon, DragValue, Grid, Key, Layout, ScrollArea, Slider, TextEdit, Ui, Vec2, ViewportClass, ViewportId, }; use parking_lot::Mutex; use std::sync::{ Arc, atomic::{AtomicBool, Ordering}, }; use tetanes_core::{ action::Action as DeckAction, apu::Channel, common::NesRegion, control_deck::Config as DeckConfig, fs, genie::GenieCode, input::FourPlayer, mem::RamState, time::Duration, video::VideoFilter, }; #[derive(Debug)] #[must_use] pub struct State { tx: NesEventProxy, tab: Tab, genie_entry: GenieEntry, } #[derive(Debug)] #[must_use] pub struct Preferences { pub id: ViewportId, open: Arc, state: Arc>, } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] pub enum Tab { #[default] Emulation, Audio, Video, Input, } #[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct GenieEntry { code: String, error: Option, } impl Preferences { const TITLE: &'static str = "TetaNES - Preferences"; pub fn new(tx: NesEventProxy) -> Self { Self { id: egui::ViewportId::from_hash_of(Self::TITLE), open: Arc::new(AtomicBool::new(false)), state: Arc::new(Mutex::new(State { tx, tab: Tab::default(), genie_entry: GenieEntry::default(), })), } } pub fn open(&self) -> bool { self.open.load(Ordering::Acquire) } pub fn set_open(&self, open: bool, ctx: &Context) { self.open.store(open, Ordering::Release); if open { ctx.send_viewport_cmd_to(self.id, egui::ViewportCommand::Close); } } pub fn toggle_open(&self, ctx: &Context) { let Ok(open) = self .open .fetch_update(Ordering::Release, Ordering::Acquire, |open| Some(!open)) else { return; }; if open { ctx.send_viewport_cmd_to(self.id, egui::ViewportCommand::Close); } } pub fn show(&mut self, ui: &mut Ui, opts: ViewportOptions, cfg: Config) { if !self.open() { return; } let open = Arc::clone(&self.open); let state = Arc::clone(&self.state); let mut viewport_builder = egui::ViewportBuilder::default().with_title(Self::TITLE); if opts.always_on_top { viewport_builder = viewport_builder.with_always_on_top(); } ui.show_viewport_deferred(self.id, viewport_builder, move |ui, class| { if class == ViewportClass::EmbeddedWindow { let mut window_open = open.load(Ordering::Acquire); egui::Window::new(Preferences::TITLE) .open(&mut window_open) .default_rect(ui.content_rect().shrink(16.0)) .show(ui, |ui| state.lock().ui(ui, opts.enabled, &cfg)); open.store(window_open, Ordering::Release); } else { CentralPanel::default() .show_inside(ui, |ui| state.lock().ui(ui, opts.enabled, &cfg)); if ui.input(|i| i.viewport().close_requested()) { open.store(false, Ordering::Release); } } }); } pub fn show_genie_codes_entry(&mut self, ui: &mut Ui, cfg: &Config) { self.state.lock().genie_codes_entry(ui, cfg); } pub fn genie_codes_list(tx: &NesEventProxy, ui: &mut Ui, cfg: &Config, scroll: bool) { if !cfg.deck.genie_codes.is_empty() { ui.vertical(|ui| { ui.horizontal(|ui| { ui.strong("Current Genie Codes:"); if ui.button("Clear All").clicked() { tx.event(ConfigEvent::GenieCodeClear); } }); let render_codes = |ui: &mut Ui, cfg: &Config| { ui.indent("current_genie_codes", |ui| { let grid = Grid::new("genie_codes").num_columns(2).spacing([40.0, 6.0]); grid.show(ui, |ui| { for genie in &cfg.deck.genie_codes { ui.label(genie.code()); // icon: waste basket if ui.button("🗑").clicked() { tx.event(ConfigEvent::GenieCodeRemoved( genie.code().to_string(), )); } ui.end_row(); } }); }) }; if scroll { ScrollArea::vertical().show(ui, |ui| { render_codes(ui, cfg); }); } else { render_codes(ui, cfg); } }); } } pub fn save_slot_radio( tx: &NesEventProxy, ui: &mut Ui, mut save_slot: u8, cfg: &Config, show_shortcut: ShowShortcut, ) { ui.vertical(|ui| { for slot in 1..=4 { let radio = RadioValue::new(&mut save_slot, slot, slot.to_string()).shortcut_text( show_shortcut .then(|| cfg.shortcut(DeckAction::SetSaveSlot(slot))) .unwrap_or_default(), ); if ui.add(radio).changed() { tx.event(ConfigEvent::SaveSlot(save_slot)); } } }); ui.vertical(|ui| { for slot in 5..=8 { let radio = RadioValue::new(&mut save_slot, slot, slot.to_string()).shortcut_text( show_shortcut .then(|| cfg.shortcut(DeckAction::SetSaveSlot(slot))) .unwrap_or_default(), ); if ui.add(radio).changed() { tx.event(ConfigEvent::SaveSlot(save_slot)); } } }); } pub fn speed_slider(tx: &NesEventProxy, ui: &mut Ui, mut speed: f32) { let slider = Slider::new(&mut speed, 0.25..=2.0) .step_by(0.25) .suffix("x"); let res = ui .add(slider) .on_hover_text("Adjust the speed of the NES emulation."); if res.changed() { tx.event(ConfigEvent::Speed(speed)); } } pub fn run_ahead_slider(tx: &NesEventProxy, ui: &mut Ui, mut run_ahead: usize) { let slider = Slider::new(&mut run_ahead, 0..=4); let res = ui .add(slider) .on_hover_text("Simulate a number of frames in the future to reduce input lag."); if res.changed() { tx.event(ConfigEvent::RunAhead(run_ahead)); } } pub fn rewind_checkbox( tx: &NesEventProxy, ui: &mut Ui, mut rewind: bool, shortcut: impl Into>, ) { let shortcut = shortcut.into(); let icon = shortcut.as_ref().map(|_| "🔄 ").unwrap_or_default(); let checkbox = Checkbox::new(&mut rewind, format!("{icon}Enable Rewinding")) .shortcut_text(shortcut.unwrap_or_default()); let res = ui .add(checkbox) .on_hover_text("Enable instant and visual rewinding. Increases memory usage."); if res.clicked() { tx.event(ConfigEvent::RewindEnabled(rewind)); } } pub fn zapper_checkbox( tx: &NesEventProxy, ui: &mut Ui, mut zapper: bool, shortcut: impl Into>, ) { let shortcut = shortcut.into(); let icon = shortcut.as_ref().map(|_| "🔫 ").unwrap_or_default(); let checkbox = Checkbox::new(&mut zapper, format!("{icon}Enable Zapper Gun")) .shortcut_text(shortcut.unwrap_or_default()); let res = ui .add(checkbox) .on_hover_text("Enable the Zapper Light Gun for games that support it."); if res.clicked() { tx.event(ConfigEvent::ZapperConnected(zapper)); } } pub fn overscan_checkbox( tx: &NesEventProxy, ui: &mut Ui, mut hide_overscan: bool, shortcut: impl Into>, ) { let shortcut = shortcut.into(); let icon = shortcut.as_ref().map(|_| "📺 ").unwrap_or_default(); let checkbox = Checkbox::new(&mut hide_overscan, format!("{icon}Hide Overscan")) .shortcut_text(shortcut.unwrap_or_default()); let res = ui.add(checkbox) .on_hover_text("Traditional CRT displays would crop the top and bottom edges of the image. Disable this to show the overscan."); if res.clicked() { tx.event(ConfigEvent::HideOverscan(hide_overscan)); } } pub fn video_filter_radio( tx: &NesEventProxy, ui: &mut Ui, mut filter: VideoFilter, cfg: &Config, show_shortcut: ShowShortcut, ) { let previous_filter = filter; let shortcut = show_shortcut.then(|| cfg.shortcut(DeckAction::SetVideoFilter(VideoFilter::Pixellate))); let icon = shortcut.as_ref().map(|_| "🌁 ").unwrap_or_default(); let radio = RadioValue::new( &mut filter, VideoFilter::Pixellate, format!("{icon}Pixellate"), ) .shortcut_text(shortcut.unwrap_or_default()); ui.add(radio).on_hover_text("Basic pixel-perfect rendering"); let shortcut = show_shortcut.then(|| cfg.shortcut(DeckAction::SetVideoFilter(VideoFilter::Ntsc))); let icon = shortcut.as_ref().map(|_| "📼 ").unwrap_or_default(); let radio = RadioValue::new(&mut filter, VideoFilter::Ntsc, format!("{icon}Ntsc")) .shortcut_text(shortcut.unwrap_or_default()); ui.add(radio).on_hover_text( "Emulate traditional NTSC rendering where chroma spills over into luma.", ); if filter != previous_filter { tx.event(ConfigEvent::VideoFilter(filter)); } } pub fn shader_radio( tx: &NesEventProxy, ui: &mut Ui, mut shader: Shader, cfg: &Config, show_shortcut: ShowShortcut, ) { let previous_shader = shader; let shortcut = show_shortcut.then(|| cfg.shortcut(Setting::SetShader(Shader::Default))); let icon = shortcut.as_ref().map(|_| "🗋 ").unwrap_or_default(); let radio = RadioValue::new(&mut shader, Shader::Default, format!("{icon}Default")) .shortcut_text(shortcut.unwrap_or_default()); ui.add(radio).on_hover_text("Default shader."); let shortcut = show_shortcut.then(|| cfg.shortcut(Setting::SetShader(Shader::CrtEasymode))); let icon = shortcut.as_ref().map(|_| "📺 ").unwrap_or_default(); let radio = RadioValue::new( &mut shader, Shader::CrtEasymode, format!("{icon}CRT Easymode"), ) .shortcut_text(shortcut.unwrap_or_default()); ui.add(radio) .on_hover_text("Emulate traditional CRT aperture grill masking."); if shader != previous_shader { tx.event(ConfigEvent::Shader(shader)); } } pub fn four_player_radio(tx: &NesEventProxy, ui: &mut Ui, mut four_player: FourPlayer) { let previous_four_player = four_player; ui.radio_value(&mut four_player, FourPlayer::Disabled, "Disabled"); ui.radio_value(&mut four_player, FourPlayer::FourScore, "Four Score") .on_hover_text("Enable NES Four Score for games that support 4 players."); ui.radio_value(&mut four_player, FourPlayer::Satellite, "Satellite") .on_hover_text("Enable NES Satellite for games that support 4 players."); if four_player != previous_four_player { tx.event(ConfigEvent::FourPlayer(four_player)); } } pub fn nes_region_radio(tx: &NesEventProxy, ui: &mut Ui, mut region: NesRegion) { let previous_region = region; ui.radio_value(&mut region, NesRegion::Auto, "Auto") .on_hover_text("Auto-detect region based on loaded ROM."); ui.radio_value(&mut region, NesRegion::Ntsc, "NTSC") .on_hover_text("Emulate NTSC timing and aspect-ratio."); ui.radio_value(&mut region, NesRegion::Pal, "PAL") .on_hover_text("Emulate PAL timing and aspect-ratio."); ui.radio_value(&mut region, NesRegion::Dendy, "Dendy") .on_hover_text("Emulate Dendy timing and aspect-ratio."); if region != previous_region { tx.event(ConfigEvent::Region(region)); } } pub fn ram_state_radio(tx: &NesEventProxy, ui: &mut Ui, mut ram_state: RamState) { let previous_ram_state = ram_state; ui.radio_value(&mut ram_state, RamState::AllZeros, "All 0x00") .on_hover_text("Clear startup RAM to all zeroes for predictable emulation."); ui.radio_value(&mut ram_state, RamState::AllOnes, "All 0xFF") .on_hover_text("Clear startup RAM to all ones for predictable emulation."); ui.radio_value(&mut ram_state, RamState::Random, "Random") .on_hover_text("Randomize startup RAM, which some games use as a basic RNG seed."); if ram_state != previous_ram_state { tx.event(ConfigEvent::RamState(ram_state)); } } pub fn menubar_checkbox( tx: &NesEventProxy, ui: &mut Ui, mut show_menubar: bool, shortcut: impl Into>, ) { let shortcut = shortcut.into(); let icon = shortcut.as_ref().map(|_| "☰ ").unwrap_or_default(); let checkbox = Checkbox::new(&mut show_menubar, format!("{icon}Show Menu Bar")) .shortcut_text(shortcut.unwrap_or_default()); let res = ui.add(checkbox).on_hover_text("Show the menu bar."); if res.clicked() { tx.event(ConfigEvent::ShowMenubar(show_menubar)); } } pub fn messages_checkbox( tx: &NesEventProxy, ui: &mut Ui, mut show_messages: bool, shortcut: impl Into>, ) { let shortcut = shortcut.into(); // icon: document with text let icon = shortcut.as_ref().map(|_| "🖹 ").unwrap_or_default(); let checkbox = Checkbox::new(&mut show_messages, format!("{icon}Show Messages")) .shortcut_text(shortcut.unwrap_or_default()); let res = ui .add(checkbox) .on_hover_text("Show shortcut and emulator messages."); if res.clicked() { tx.event(ConfigEvent::ShowMessages(show_messages)); } } pub fn screen_reader_checkbox(ui: &mut Ui, shortcut: impl Into>) { let shortcut = shortcut.into(); // icon: document with text let icon = shortcut.as_ref().map(|_| "🔈 ").unwrap_or_default(); let mut screen_reader = ui.ctx().options(|o| o.screen_reader); let checkbox = Checkbox::new(&mut screen_reader, format!("{icon}Enable Screen Reader")) .shortcut_text(shortcut.unwrap_or_default()); let res = ui .add(checkbox) .on_hover_text("Enable screen reader to read buttons and labels out loud."); if res.clicked() { ui.ctx().options_mut(|o| o.screen_reader = screen_reader); } } pub fn window_scale_radio(tx: &NesEventProxy, ui: &mut Ui, mut scale: f32) { let previous_scale = scale; ui.vertical(|ui| { ui.radio_value(&mut scale, 1.0, "1x"); ui.radio_value(&mut scale, 2.0, "2x"); ui.radio_value(&mut scale, 3.0, "3x"); }); ui.vertical(|ui| { ui.radio_value(&mut scale, 4.0, "4x"); ui.radio_value(&mut scale, 5.0, "5x"); }); if scale != previous_scale { tx.event(ConfigEvent::Scale(scale)); } } pub fn fullscreen_checkbox( tx: &NesEventProxy, ui: &mut Ui, mut fullscreen: bool, shortcut: impl Into>, ) { let shortcut = shortcut.into(); // icon: screen let icon = shortcut.as_ref().map(|_| "🖵 ").unwrap_or_default(); let checkbox = Checkbox::new(&mut fullscreen, format!("{icon}Fullscreen")) .shortcut_text(shortcut.unwrap_or_default()); if ui.add(checkbox).clicked() { tx.event(ConfigEvent::Fullscreen(fullscreen)); } } pub fn embed_viewports_checkbox( tx: &NesEventProxy, ui: &mut Ui, cfg: &Config, shortcut: impl Into>, ) { if feature!(OsViewports) { ui.add_enabled_ui(!cfg.renderer.fullscreen, |ui| { let shortcut = shortcut.into(); // icon: maximize let icon = shortcut.as_ref().map(|_| "🗖 ").unwrap_or_default(); let mut embed_viewports = ui.ctx().embed_viewports(); let checkbox = Checkbox::new(&mut embed_viewports, format!("{icon}Embed Viewports")) .shortcut_text(shortcut.unwrap_or_default()); let res = ui.add(checkbox).on_disabled_hover_text( "Non-embedded viewports are not supported while in fullscreen.", ); if res.clicked() { ui.ctx().set_embed_viewports(embed_viewports); tx.event(ConfigEvent::EmbedViewports(embed_viewports)); } }); } } pub fn always_on_top_checkbox( tx: &NesEventProxy, ui: &mut Ui, mut always_on_top: bool, shortcut: impl Into>, ) { if feature!(OsViewports) { let shortcut = shortcut.into(); let icon = shortcut.as_ref().map(|_| "🔝 ").unwrap_or_default(); let checkbox = Checkbox::new(&mut always_on_top, format!("{icon}Always on Top")) .shortcut_text(shortcut.unwrap_or_default()); // FIXME: Currently when not using embeded viewports, toggling always on top from // the preferences window will focus the primary window, potentially obscuring the // preferences window if ui.add(checkbox).clicked() { tx.event(ConfigEvent::AlwaysOnTop(always_on_top)); } } } } impl State { fn ui(&mut self, ui: &mut Ui, enabled: bool, cfg: &Config) { ui.add_enabled_ui(enabled, |ui| { ui.set_min_height(ui.available_height()); ui.horizontal(|ui| { ui.selectable_value(&mut self.tab, Tab::Emulation, "Emulation"); ui.selectable_value(&mut self.tab, Tab::Audio, "Audio"); ui.selectable_value(&mut self.tab, Tab::Video, "Video"); ui.selectable_value(&mut self.tab, Tab::Input, "Input"); }); ui.separator(); ScrollArea::both().show(ui, |ui| { match self.tab { Tab::Emulation => self.emulation_tab(ui, cfg), Tab::Audio => Self::audio_tab(&self.tx, ui, cfg), Tab::Video => Self::video_tab(&self.tx, ui, cfg), Tab::Input => Self::input_tab(&self.tx, ui, cfg), } ui.separator(); ui.horizontal(|ui| { if ui.button("Restore Defaults").clicked() { Self::restore_defaults(&self.tx, ui.ctx()); } if feature!(Storage) && ui.button("Clear Save States").clicked() { Self::clear_save_states(&self.tx); } if feature!(Filesystem) && ui.button("Clear Recent ROMs").clicked() { self.tx.event(ConfigEvent::RecentRomsClear); } #[cfg(target_arch = "wasm32")] if ui.button("Download Save States").clicked() && let Err(err) = crate::platform::download_save_states() { self.tx .event(UiEvent::Message((MessageType::Error, err.to_string()))); } }); }); }); } fn emulation_tab(&mut self, ui: &mut Ui, cfg: &Config) { let EmulationConfig { mut auto_save, auto_save_interval, mut auto_load, rewind, mut rewind_interval, mut rewind_seconds, run_ahead, save_slot, speed, .. } = cfg.emulation; let DeckConfig { mut emulate_ppu_warmup, four_player, ram_state, region, .. } = cfg.deck; let grid = Grid::new("emulation_checkboxes") .num_columns(2) .spacing([80.0, 6.0]); grid.show(ui, |ui| { let tx = &self.tx; let res = ui.checkbox(&mut auto_load, "Auto-Load") .on_hover_text("Automatically load game state from the current save slot on load."); if res.changed() { tx.event(ConfigEvent::AutoLoad( auto_load, )); } ui.end_row(); ui.vertical(|ui| { Preferences::rewind_checkbox(tx, ui, rewind, None); ui.add_enabled_ui(rewind, |ui| { ui.indent("rewind_settings", |ui| { ui.horizontal(|ui| { let suffix = if rewind_seconds == 1 { " second" } else { " seconds" }; let drag = DragValue::new(&mut rewind_seconds) .range(1..=360) .suffix(suffix); let res = ui.add(drag) .on_hover_text("The maximum number of seconds to rewind."); if res.changed() { tx.event(ConfigEvent::RewindSeconds(rewind_seconds)); } }); ui.horizontal(|ui| { let suffix = if rewind_interval == 1 { " frame" } else { " frames" }; let drag = DragValue::new(&mut rewind_interval) .range(1..=60) .prefix("every ") .suffix(suffix); let res = ui.add(drag) .on_hover_text("The frame interval to save rewind states."); if res.changed() { tx.event(ConfigEvent::RewindInterval(rewind_interval)); } }); }); }); }); ui.vertical(|ui| { let res = ui.checkbox(&mut auto_save, "Auto-Save") .on_hover_text(concat!( "Automatically save game state to the current save slot ", "on exit or unloading and an optional interval. ", "Setting to 0 will disable saving on an interval.", )); if res.changed() { tx.event(ConfigEvent::AutoSave( auto_save, )); } ui.add_enabled_ui(auto_save, |ui| { ui.indent("auto_save_settings", |ui| { ui.horizontal(|ui| { let mut auto_save_interval = auto_save_interval.as_secs(); let suffix = if auto_save_interval == 1 { " second" } else { " seconds" }; let drag = DragValue::new(&mut auto_save_interval) .range(0..=60) .prefix("every ") .suffix(suffix); let res = ui.add(drag) .on_hover_text(concat!( "Set the interval to auto-save game state. ", "A value of `0` will still save on exit or unload while Auto-Save is enabled." )); if res.changed() { tx.event(ConfigEvent::AutoSaveInterval(Duration::from_secs(auto_save_interval))); } }); }); }); }); ui.end_row(); let res = ui.checkbox(&mut emulate_ppu_warmup, "Emulate PPU Warmup") .on_hover_text(concat!( "Set whether to emulate PPU warmup where writes to certain registers are ignored. ", "Can result in some games not working correctly" )); if res.clicked() { tx.event(ConfigEvent::EmulatePpuWarmup(emulate_ppu_warmup)); } ui.end_row(); }); ui.separator(); let grid = Grid::new("emulation_sliders") .num_columns(2) .spacing([40.0, 6.0]); grid.show(ui, |ui| { let tx = &self.tx; ui.horizontal(|ui| { Preferences::speed_slider(tx, ui, speed); ui.label("Emulation Speed") .on_hover_cursor(CursorIcon::Help) .on_hover_text("Change the speed of the emulation."); }); ui.end_row(); ui.horizontal(|ui| { Preferences::run_ahead_slider(tx, ui, run_ahead); ui.label("Run Ahead") .on_hover_cursor(CursorIcon::Help) .on_hover_text( "Simulate a number of frames in the future to reduce input lag.", ); }); ui.end_row(); }); ui.separator(); let grid = Grid::new("emulation_radios") .num_columns(4) .spacing([20.0, 6.0]); grid.show(ui, |ui| { let tx = &self.tx; ui.with_layout(Layout::left_to_right(Align::Min), |ui| { ui.strong("Save Slot:") .on_hover_cursor(CursorIcon::Help) .on_hover_text("Select which slot to use when saving or loading game state."); }); Grid::new("save_slots") .num_columns(2) .spacing([20.0, 6.0]) .show(ui, |ui| { Preferences::save_slot_radio(tx, ui, save_slot, cfg, ShowShortcut::No) }); ui.with_layout(Layout::left_to_right(Align::Min), |ui| { ui.strong("Four Player:") .on_hover_cursor(CursorIcon::Help) .on_hover_text( "Some game titles support up to 4 players (requires connected controllers).", ); }); ui.vertical(|ui| Preferences::four_player_radio(tx, ui, four_player)); ui.end_row(); ui.with_layout(Layout::left_to_right(Align::Min), |ui| { ui.strong("NES Region:") .on_hover_cursor(CursorIcon::Help) .on_hover_text("Which regional NES hardware to emulate."); }); ui.vertical(|ui| Preferences::nes_region_radio(tx, ui, region)); ui.with_layout(Layout::left_to_right(Align::Min), |ui| { ui.strong("RAM State:") .on_hover_cursor(CursorIcon::Help) .on_hover_text("What values are read from NES RAM on load."); }); ui.vertical(|ui| Preferences::ram_state_radio(tx, ui, ram_state)); ui.end_row(); }); let grid = Grid::new("genie_codes").num_columns(2).spacing([40.0, 6.0]); grid.show(ui, |ui| { self.genie_codes_entry(ui, cfg); Preferences::genie_codes_list(&self.tx, ui, cfg, false); }); } fn audio_tab(tx: &NesEventProxy, ui: &mut Ui, cfg: &Config) { let AudioConfig { latency, mut buffer_size, mut enabled, } = cfg.audio; let DeckConfig { channels_enabled, .. } = cfg.deck; let res = ui.checkbox(&mut enabled, "Enable Audio"); if res.clicked() { tx.event(ConfigEvent::AudioEnabled(enabled)); } ui.add_enabled_ui(cfg.audio.enabled, |ui| { ui.indent("apu_channels", |ui| { Grid::new("apu_channels") .spacing([60.0, 6.0]) .num_columns(2) .show(ui, |ui| { let mut pulse1_enabled = channels_enabled[0]; if ui.checkbox(&mut pulse1_enabled, "Enable Pulse1").clicked() { tx.event(ConfigEvent::ApuChannelEnabled((Channel::Pulse1, pulse1_enabled))); } let mut noise_enabled = channels_enabled[3]; if ui.checkbox(&mut noise_enabled, "Enable Noise").clicked() { tx.event(ConfigEvent::ApuChannelEnabled((Channel::Noise, noise_enabled))); } ui.end_row(); let mut pulse1_enabled = channels_enabled[1]; if ui.checkbox(&mut pulse1_enabled, "Enable Pulse2").clicked() { tx.event(ConfigEvent::ApuChannelEnabled((Channel::Pulse2, pulse1_enabled))); } let mut dmc_enabled = channels_enabled[4]; if ui.checkbox(&mut dmc_enabled, "Enable DMC").clicked() { tx.event(ConfigEvent::ApuChannelEnabled((Channel::Dmc, dmc_enabled))); } ui.end_row(); let mut triangle_enabled = channels_enabled[2]; if ui.checkbox(&mut triangle_enabled, "Enable Triangle").clicked() { tx.event(ConfigEvent::ApuChannelEnabled((Channel::Triangle, triangle_enabled))); } let mut mapper_enabled = channels_enabled[5]; if ui.checkbox(&mut mapper_enabled, "Enable Mapper").clicked() { tx.event(ConfigEvent::ApuChannelEnabled((Channel::Mapper, mapper_enabled))); } ui.end_row(); }); ui.separator(); Grid::new("audio_settings") .spacing([40.0, 6.0]) .num_columns(2) .show(ui, |ui| { ui.horizontal(|ui| { let drag = DragValue::new(&mut buffer_size) .speed(10) .range(128..=8192) .prefix("buffer ") .suffix(" samples"); let res = ui.add(drag) .on_hover_text( "The audio sample buffer size allocated to the sound driver. Increased audio buffer size can help reduce audio underruns.", ); if res.changed() { tx.event(ConfigEvent::AudioBuffer(buffer_size)); } }); ui.end_row(); ui.horizontal(|ui| { let mut latency = latency.as_millis() as u64; let drag = DragValue::new(&mut latency) .range(1..=1000) .suffix(" ms latency"); let res = ui.add(drag) .on_hover_text( "The amount of queued audio before sending to the sound driver. Increased audio latency can help reduce audio underruns.", ); if res.changed() { tx.event(ConfigEvent::AudioLatency(Duration::from_millis(latency))); } }); ui.end_row(); }); }); }); } fn video_tab(tx: &NesEventProxy, ui: &mut Ui, cfg: &Config) { let RendererConfig { always_on_top, fullscreen, hide_overscan, scale, shader, show_menubar, show_messages, .. } = cfg.renderer; let DeckConfig { filter, .. } = cfg.deck; Grid::new("video_checkboxes") .spacing([80.0, 6.0]) .num_columns(2) .show(ui, |ui| { Preferences::menubar_checkbox(tx, ui, show_menubar, None); Preferences::fullscreen_checkbox(tx, ui, fullscreen, None); ui.end_row(); Preferences::messages_checkbox(tx, ui, show_messages, None); Preferences::embed_viewports_checkbox(tx, ui, cfg, None); ui.end_row(); Preferences::overscan_checkbox(tx, ui, hide_overscan, None); Preferences::always_on_top_checkbox(tx, ui, always_on_top, None); ui.end_row(); }); ui.separator(); Grid::new("video_preferences") .num_columns(2) .spacing([40.0, 6.0]) .show(ui, |ui| { ui.with_layout(Layout::left_to_right(Align::Min), |ui| { ui.strong("Window Scale:"); }); Grid::new("save_slots") .num_columns(2) .spacing([20.0, 6.0]) .show(ui, |ui| { Preferences::window_scale_radio(tx, ui, scale); }); ui.end_row(); ui.with_layout(Layout::left_to_right(Align::Min), |ui| { ui.strong("Video Filter:"); }); ui.vertical(|ui| { Preferences::video_filter_radio(tx, ui, filter, cfg, ShowShortcut::No); }); ui.end_row(); ui.with_layout(Layout::left_to_right(Align::Min), |ui| { ui.strong("Shader:"); }); ui.vertical(|ui| Preferences::shader_radio(tx, ui, shader, cfg, ShowShortcut::No)); }); } fn input_tab(tx: &NesEventProxy, ui: &mut Ui, cfg: &Config) { let DeckConfig { mut concurrent_dpad, zapper, .. } = cfg.deck; Grid::new("input_checkboxes") .num_columns(2) .spacing([80.0, 6.0]) .show(ui, |ui| { Preferences::zapper_checkbox(tx, ui, zapper, None); ui.end_row(); let res = ui.checkbox(&mut concurrent_dpad, "Enable Concurrent D-Pad"); if res.clicked() { tx.event(ConfigEvent::ConcurrentDpad(concurrent_dpad)); } }); } pub fn genie_codes_entry(&mut self, ui: &mut Ui, cfg: &Config) { let tx = &self.tx; ui.vertical(|ui| { // desired_width below doesn't have the desired effect ui.allocate_space(Vec2::new(200.0, 0.0)); let genie_label = ui.strong("Add Genie Code(s):") .on_hover_cursor(CursorIcon::Help) .on_hover_text( "A Game Genie Code is a 6 or 8 letter string that temporarily modifies game memory during operation. e.g. `AATOZE` will start Super Mario Bros. with 9 lives.\n\nYou can enter one code per line." ); let text_edit = TextEdit::multiline(&mut self.genie_entry.code) .hint_text("e.g. AATOZE") .desired_width(200.0); let entry_res = ui.add(text_edit) .labelled_by(genie_label.id); if entry_res.changed() { self.genie_entry.error = None; } let has_entry = !self.genie_entry.code.is_empty(); let add_clicked = ui.horizontal(|ui| { ui.add_enabled_ui(has_entry, |ui| { let add_clicked = ui.button("Add").clicked(); if ui.button("Clear").clicked() { self.genie_entry.code.clear(); self.genie_entry.error = None; } add_clicked }).inner }).inner; if (has_entry && entry_res.lost_focus() && ui.input(|i| i.key_pressed(Key::Enter))) || add_clicked { for code in self.genie_entry.code.lines() { let code = code.trim(); if code.is_empty() { continue; } match GenieCode::parse(code) { Ok(hex) => { let code = GenieCode::from_raw(code.to_string(), &hex); if !cfg.deck.genie_codes.contains(&code) { tx.event(ConfigEvent::GenieCodeAdded(code)); } } Err(err) => self.genie_entry.error = Some(err.to_string()), } } if self.genie_entry.error.is_none() { self.genie_entry.code.clear(); } } if let Some(error) = &self.genie_entry.error { ui.colored_label(ui.visuals().error_fg_color, error); } }); } fn restore_defaults(tx: &NesEventProxy, ctx: &Context) { ctx.memory_mut(|mem| *mem = Default::default()); // Inform all cfg updates let Config { deck, emulation, audio, renderer, input, } = Config::default(); let events = [ ConfigEvent::ActionBindings(input.action_bindings), ConfigEvent::AlwaysOnTop(renderer.always_on_top), ConfigEvent::ApuChannelsEnabled(deck.channels_enabled), ConfigEvent::AudioBuffer(audio.buffer_size), ConfigEvent::AudioEnabled(audio.enabled), ConfigEvent::AudioLatency(audio.latency), ConfigEvent::AutoLoad(emulation.auto_load), ConfigEvent::AutoSave(emulation.auto_save), ConfigEvent::AutoSaveInterval(emulation.auto_save_interval), ConfigEvent::ConcurrentDpad(deck.concurrent_dpad), ConfigEvent::DarkTheme(renderer.dark_theme), ConfigEvent::EmbedViewports(renderer.embed_viewports), ConfigEvent::FourPlayer(deck.four_player), ConfigEvent::Fullscreen(renderer.fullscreen), ConfigEvent::GamepadAssignments(input.gamepad_assignments), ConfigEvent::GenieCodeClear, ConfigEvent::HideOverscan(renderer.hide_overscan), ConfigEvent::MapperRevisions(deck.mapper_revisions), ConfigEvent::RamState(deck.ram_state), // Clearing recent roms is handled in a separate button ConfigEvent::Region(deck.region), ConfigEvent::RewindEnabled(emulation.rewind), ConfigEvent::RewindInterval(emulation.rewind_interval), ConfigEvent::RewindSeconds(emulation.rewind_seconds), ConfigEvent::RunAhead(emulation.run_ahead), ConfigEvent::SaveSlot(emulation.save_slot), ConfigEvent::Shader(renderer.shader), ConfigEvent::ShowMenubar(renderer.show_menubar), ConfigEvent::ShowMessages(renderer.show_messages), ConfigEvent::Speed(emulation.speed), ConfigEvent::VideoFilter(deck.filter), ConfigEvent::ZapperConnected(deck.zapper), ]; for event in events { tx.event(event); } } pub(crate) fn clear_save_states(tx: &NesEventProxy) { let data_dir = Config::default_data_dir(); match fs::clear_dir(data_dir) { Ok(_) => tx.event(UiEvent::Message(( MessageType::Info, "Save States cleared.".to_string(), ))), Err(_) => tx.event(UiEvent::Message(( MessageType::Error, "Failed to clear Save States.".to_string(), ))), } } } ================================================ FILE: tetanes/src/nes/renderer/gui.rs ================================================ use crate::{ feature, nes::{ RunState, action::{Debug, DebugKind, DebugStep, Feature, Setting, Ui as UiAction}, config::{Config, RecentRom, RendererConfig}, emulation::FrameStats, event::{ ConfigEvent, DebugEvent, EmulationEvent, NesEvent, NesEventProxy, RendererEvent, Response, UiEvent, }, input::Gamepads, renderer::{ gui::{ keybinds::Keybinds, lib::{ ShortcutText, ShowShortcut, ToggleValue, ViewportOptions, cursor_to_zapper, input_down, }, ppu_viewer::PpuViewer, preferences::Preferences, }, painter::RenderState, texture::Texture, }, rom::{HOMEBREW_ROMS, RomAsset}, version::Version, }, sys::{SystemInfo, info::System}, }; use egui::{ Align, Button, CentralPanel, Color32, Context, CornerRadius, CursorIcon, Direction, FontData, FontDefinitions, FontFamily, Frame, Grid, Image, Layout, Panel, Pos2, Rect, RichText, ScrollArea, Sense, Stroke, Ui, UiBuilder, ViewportClass, ViewportId, Visuals, hex_color, include_image, style::{HandleShape, Selection, TextCursorStyle, WidgetVisuals}, }; use serde::{Deserialize, Serialize}; use std::sync::{ Arc, atomic::{AtomicBool, Ordering}, }; use tetanes_core::{ action::Action as DeckAction, common::{NesRegion, ResetKind}, control_deck::LoadedRom, cpu::instr::InstrRef, ppu, time::{Duration, Instant}, }; use tracing::{error, info, warn}; use winit::event::WindowEvent; mod keybinds; pub mod lib; mod ppu_viewer; mod preferences; const UI_SETTINGS_TITLE: &str = "🔧 UI Settings"; #[cfg(debug_assertions)] const UI_INSPECTION_TITLE: &str = "🔍 UI Inspection"; #[cfg(debug_assertions)] const UI_MEMORY_TITLE: &str = "📝 UI Memory"; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum Menu { About, Keybinds, PerfStats, PpuViewer, Preferences, } #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum MessageType { Info, Warn, Error, } #[derive(Debug)] #[must_use] pub struct Gui { ctx: Context, initialized: bool, title: String, tx: NesEventProxy, pub nes_texture: Texture, corrupted_cpu_instr: Option, pub run_state: RunState, pub menu_height: f32, nes_frame: Rect, about_open: bool, ui_settings_id: ViewportId, ui_settings_open: Arc, #[cfg(debug_assertions)] ui_inspection_id: ViewportId, #[cfg(debug_assertions)] ui_inspection_open: Arc, #[cfg(debug_assertions)] ui_memory_id: ViewportId, #[cfg(debug_assertions)] ui_memory_open: Arc, perf_stats_open: bool, update_window_open: bool, version: Version, pub keybinds: Keybinds, preferences: Preferences, debugger_open: bool, ppu_viewer: PpuViewer, apu_mixer_open: bool, viewport_info_open: bool, replay_recording: bool, audio_recording: bool, frame_stats: FrameStats, messages: Vec<(MessageType, String, Instant)>, pub loaded_rom: Option, about_homebrew_rom_open: Option, start: Instant, sys: System, pub error: Option, enable_auto_update: bool, dont_show_updates: bool, } impl Gui { const MSG_TIMEOUT: Duration = Duration::from_secs(3); const MAX_MESSAGES: usize = 5; const NO_ROM_LOADED: &'static str = "No ROM is loaded."; /// Create a `Gui` instance. pub fn new( ctx: Context, tx: NesEventProxy, render_state: &mut RenderState, cfg: &Config, ) -> Self { let nes_texture = Texture::new( render_state, cfg.texture_size(), cfg.deck.region.aspect_ratio(), Some("nes frame"), ); Self { ctx, initialized: false, title: Config::WINDOW_TITLE.to_string(), tx: tx.clone(), nes_texture, corrupted_cpu_instr: None, run_state: RunState::Running, menu_height: 0.0, nes_frame: Rect::ZERO, about_open: false, ui_settings_id: egui::ViewportId::from_hash_of(UI_SETTINGS_TITLE), ui_settings_open: Arc::new(AtomicBool::new(false)), #[cfg(debug_assertions)] ui_inspection_id: egui::ViewportId::from_hash_of(UI_INSPECTION_TITLE), #[cfg(debug_assertions)] ui_inspection_open: Arc::new(AtomicBool::new(false)), #[cfg(debug_assertions)] ui_memory_id: egui::ViewportId::from_hash_of(UI_MEMORY_TITLE), #[cfg(debug_assertions)] ui_memory_open: Arc::new(AtomicBool::new(false)), perf_stats_open: false, update_window_open: false, version: Version::new(), keybinds: Keybinds::new(tx.clone()), preferences: Preferences::new(tx.clone()), debugger_open: false, ppu_viewer: PpuViewer::new(tx, render_state), apu_mixer_open: false, viewport_info_open: false, replay_recording: false, audio_recording: false, frame_stats: FrameStats::new(), messages: Vec::new(), loaded_rom: None, about_homebrew_rom_open: None, start: Instant::now(), sys: System::default(), error: None, enable_auto_update: false, dont_show_updates: false, } } pub fn on_window_event(&mut self, event: &WindowEvent) -> Response { match event { WindowEvent::KeyboardInput { .. } | WindowEvent::MouseInput { .. } if self.keybinds.wants_input() => { Response { consumed: true, ..Default::default() } } _ => Response::default(), } } pub fn on_event(&mut self, queue: &wgpu::Queue, event: &mut NesEvent) { match event { NesEvent::Ui(UiEvent::UpdateAvailable(version)) => { self.version.set_latest(version.clone()); self.update_window_open = true; self.ctx.request_repaint(); } NesEvent::Emulation(event) => match event { EmulationEvent::ReplayRecord(recording) => { self.replay_recording = *recording; } EmulationEvent::AudioRecord(recording) => { self.audio_recording = *recording; } EmulationEvent::CpuCorrupted { instr } => { self.corrupted_cpu_instr = Some(*instr); self.ctx.request_repaint(); } EmulationEvent::RunState(mode) => { self.run_state = *mode; } _ => (), }, NesEvent::Renderer(event) => match event { RendererEvent::FrameStats(stats) => { self.frame_stats = *stats; } // Toggling true is handled in the menu widget RendererEvent::ShowMenubar(show) if !*show => { self.menu_height = 0.0; } RendererEvent::ReplayLoaded => { self.run_state = RunState::Running; self.tx.event(EmulationEvent::RunState(self.run_state)); } RendererEvent::RomUnloaded => { self.run_state = RunState::Running; self.tx.event(EmulationEvent::RunState(self.run_state)); self.loaded_rom = None; self.title = Config::WINDOW_TITLE.to_string(); } RendererEvent::RomLoaded(rom) => { self.run_state = RunState::Running; self.tx.event(EmulationEvent::RunState(self.run_state)); self.title = format!("{} :: {}", Config::WINDOW_TITLE, rom.name); self.loaded_rom = Some(rom.clone()); } RendererEvent::Menu(menu) => match menu { Menu::About => self.about_open = !self.about_open, Menu::Keybinds => self.keybinds.toggle_open(&self.ctx), Menu::PerfStats => { self.perf_stats_open = !self.perf_stats_open; self.tx .event(EmulationEvent::ShowFrameStats(self.perf_stats_open)); } Menu::PpuViewer => self.ppu_viewer.toggle_open(&self.ctx), Menu::Preferences => self.preferences.toggle_open(&self.ctx), }, _ => (), }, NesEvent::Debug(DebugEvent::Ppu(ppu)) => { self.ppu_viewer.update_ppu(queue, std::mem::take(ppu)); self.ctx.request_repaint_of(self.ppu_viewer.id()); } _ => (), } } pub fn add_message(&mut self, ty: MessageType, text: S) where S: Into, { let text = text.into(); match ty { MessageType::Info => info!("{text}"), MessageType::Warn => warn!("{text}"), MessageType::Error => error!("{text}"), } self.messages .push((ty, text, Instant::now() + Self::MSG_TIMEOUT)); } pub fn loaded_region(&self) -> Option { self.loaded_rom.as_ref().map(|rom| rom.region) } pub fn aspect_ratio(&self, cfg: &Config) -> f32 { let region = cfg .deck .region .is_auto() .then(|| self.loaded_region()) .flatten() .unwrap_or(cfg.deck.region); region.aspect_ratio() } /// Create the UI. pub fn ui(&mut self, ui: &mut Ui, cfg: &Config, gamepads: &Gamepads) { if !self.initialized { self.initialize(ui, cfg); } if cfg.renderer.show_menubar { Panel::top("menubar").show_inside(ui, |ui| self.menubar(ui, cfg)); } let viewport_opts = ViewportOptions { enabled: !self.keybinds.wants_input(), always_on_top: cfg.renderer.always_on_top, }; CentralPanel::default() .frame(Frame::canvas(ui.style())) .show_inside(ui, |ui| { self.nes_frame(ui, viewport_opts.enabled, cfg, gamepads); }); self.preferences.show(ui, viewport_opts, cfg.clone()); self.keybinds.show(ui, viewport_opts, cfg.clone(), gamepads); self.ppu_viewer.show(ui, viewport_opts); self.show_about_window(ui, viewport_opts.enabled); self.show_about_homebrew_window(ui, viewport_opts.enabled); self.show_performance_window(ui, viewport_opts.enabled, cfg); self.show_update_window(ui, viewport_opts.enabled, cfg); Self::show_viewport( UI_SETTINGS_TITLE, ui, viewport_opts, &self.ui_settings_open, |ctx, ui| { ScrollArea::both().show(ui, |ui| ctx.settings_ui(ui)); }, ); #[cfg(debug_assertions)] { Self::show_viewport( UI_INSPECTION_TITLE, ui, viewport_opts, &self.ui_inspection_open, |ctx, ui| { ScrollArea::both().show(ui, |ui| ctx.inspection_ui(ui)); }, ); Self::show_viewport( UI_MEMORY_TITLE, ui, viewport_opts, &self.ui_memory_open, |ctx, ui| { ScrollArea::both().show(ui, |ui| ctx.memory_ui(ui)); }, ); } } fn initialize(&mut self, ui: &mut Ui, cfg: &Config) { let theme = if cfg.renderer.dark_theme { Self::dark_theme() } else { Self::light_theme() }; ui.set_visuals(theme); { let style = ui.style_mut(); style.spacing.scroll.floating = false; style.spacing.scroll.foreground_color = false; style.spacing.scroll.bar_width = 8.0; } const FONT: (&str, &[u8]) = ( "pixeloid-sans", include_bytes!("../../../assets/pixeloid-sans.ttf"), ); const BOLD_FONT: (&str, &[u8]) = ( "pixeloid-sans-bold", include_bytes!("../../../assets/pixeloid-sans-bold.ttf"), ); const MONO_FONT: (&str, &[u8]) = ( "pixeloid-mono", include_bytes!("../../../assets/pixeloid-mono.ttf"), ); egui_extras::install_image_loaders(ui); let mut fonts = FontDefinitions::default(); for (name, data) in [FONT, BOLD_FONT, MONO_FONT] { let font_data = FontData::from_static(data); fonts.font_data.insert(name.to_string(), font_data.into()); } match fonts.families.get_mut(&FontFamily::Proportional) { Some(font) => font.insert(0, FONT.0.to_string()), None => tracing::warn!("failed to set proportional font"), } match fonts.families.get_mut(&FontFamily::Monospace) { Some(font) => font.insert(0, MONO_FONT.0.to_string()), None => tracing::warn!("failed to set monospace font"), } ui.set_fonts(fonts); // Check for update on start if self.version.requires_updates() { let notify_latest = false; self.version.check_for_updates(&self.tx, notify_latest); } self.initialized = true; } fn show_about_window(&mut self, ctx: &Context, enabled: bool) { let mut about_open = self.about_open; egui::Window::new("ℹ About TetaNES") .open(&mut about_open) .show(ctx, |ui| self.about(ui, enabled)); self.about_open = about_open; } fn show_about_homebrew_window(&mut self, ctx: &Context, enabled: bool) { let Some(rom) = self.about_homebrew_rom_open else { return; }; let mut about_homebrew_open = true; egui::Window::new(format!("ℹ About {}", rom.name)) .open(&mut about_homebrew_open) .show(ctx, |ui| { ui.add_enabled_ui(enabled, |ui| { ScrollArea::vertical().show(ui, |ui| { ui.strong("Author(s):"); ui.label(rom.authors); ui.add_space(12.0); ui.strong("Description:"); ui.label(rom.description); ui.add_space(12.0); ui.strong("Source:"); ui.hyperlink(rom.source); }); }); }); if !about_homebrew_open { self.about_homebrew_rom_open = None; } } pub(super) fn show_viewport_info_window( &mut self, ctx: &Context, id: egui::ViewportId, info: &egui::ViewportInfo, ) { egui::Window::new(format!("ℹ Viewport Info ({id:?})")) .open(&mut self.viewport_info_open) .show(ctx, |ui| info.ui(ui)); } fn show_performance_window(&mut self, ctx: &Context, enabled: bool, cfg: &Config) { let mut perf_stats_open = self.perf_stats_open; egui::Window::new("🛠 Performance Stats") .open(&mut perf_stats_open) .show(ctx, |ui| { ui.add_enabled_ui(enabled, |ui| self.performance_stats(ui, cfg)); }); self.perf_stats_open = perf_stats_open; } pub(super) fn close_viewport(&self, viewport_id: ViewportId) { match viewport_id { id if id == self.keybinds.id => self.keybinds.set_open(false, &self.ctx), id if id == self.ppu_viewer.id => self.ppu_viewer.set_open(false, &self.ctx), id if id == self.preferences.id => self.preferences.set_open(false, &self.ctx), id if id == self.ui_settings_id => { self.ui_settings_open.store(false, Ordering::Release); self.ctx .send_viewport_cmd_to(self.ui_settings_id, egui::ViewportCommand::Close); } #[cfg(debug_assertions)] id if id == self.ui_inspection_id => { self.ui_inspection_open.store(false, Ordering::Release); self.ctx .send_viewport_cmd_to(self.ui_inspection_id, egui::ViewportCommand::Close); } #[cfg(debug_assertions)] id if id == self.ui_memory_id => { self.ui_memory_open.store(false, Ordering::Release); self.ctx .send_viewport_cmd_to(self.ui_memory_id, egui::ViewportCommand::Close); } _ => (), } } fn show_viewport( title: impl Into, ui: &mut Ui, opts: ViewportOptions, open: &Arc, add_contents: impl Fn(&Context, &mut Ui) + Send + Sync + 'static, ) { if !open.load(Ordering::Acquire) { return; } let title = title.into(); let viewport_id = egui::ViewportId::from_hash_of(&title); let mut viewport_builder = egui::ViewportBuilder::default().with_title(&title); if opts.always_on_top { viewport_builder = viewport_builder.with_always_on_top(); } let open = Arc::clone(open); let ctx = ui.ctx().clone(); ui.show_viewport_deferred(viewport_id, viewport_builder, move |ui, class| { if class == ViewportClass::EmbeddedWindow { let mut window_open = open.load(Ordering::Acquire); egui::Window::new(&title) .open(&mut window_open) .vscroll(true) .show(ui, |ui| { ui.add_enabled_ui(opts.enabled, |ui| add_contents(&ctx, ui)); }); open.store(window_open, Ordering::Release); } else { CentralPanel::default().show_inside(ui, |ui| { ui.add_enabled_ui(opts.enabled, |ui| add_contents(&ctx, ui)); }); if ui.input(|i| i.viewport().close_requested()) { open.store(false, Ordering::Release); } } }); } fn show_update_window(&mut self, ctx: &Context, enabled: bool, cfg: &Config) { let mut update_window_open = self.update_window_open && cfg.renderer.show_updates; let mut close_window = false; egui::Window::new("🌐 Update Available") .open(&mut update_window_open) .resizable(false) .show(ctx, |ui| { ui.add_enabled_ui(enabled, |ui| { ui.label(format!( "An update is available for TetaNES! (v{})", self.version.latest(), )); ui.hyperlink("https://github.com/lukexor/tetanes/releases"); ui.add_space(15.0); // TODO: Add auto-update for each platform if self.enable_auto_update { ui.label("Would you like to install it and restart?"); ui.add_space(15.0); ui.checkbox(&mut self.dont_show_updates, "Don't show this again"); ui.add_space(15.0); ui.with_layout(Layout::right_to_left(Align::Min), |ui| { let res = ui.button("Skip").on_hover_text(format!( "Keep the current version of TetaNES (v{}).", self.version.current() )); if res.clicked() { close_window = true; } let res = ui.button("Continue").on_hover_text(format!( "Install the latest version (v{}) restart TetaNES.", self.version.current() )); if res.clicked() && let Err(err) = self.version.install_update_and_restart() { self.add_message( MessageType::Error, format!("Failed to install update: {err}"), ); close_window = true; } }); } else { ui.label("Click the above link to download the update for your system."); ui.add_space(15.0); ui.checkbox(&mut self.dont_show_updates, "Don't show this again"); ui.add_space(15.0); ui.with_layout(Layout::right_to_left(Align::Min), |ui| { if ui.button(" OK ").clicked() { close_window = true; } }); } }); }); if close_window || update_window_open != self.update_window_open && cfg.renderer.show_updates { self.update_window_open = false; if self.dont_show_updates == cfg.renderer.show_updates { self.tx .event(ConfigEvent::ShowUpdates(!self.dont_show_updates)); self.dont_show_updates = false; } } } fn menubar(&mut self, ui: &mut Ui, cfg: &Config) { ui.add_enabled_ui(!self.keybinds.wants_input(), |ui| { let inner_res = egui::MenuBar::new().ui(ui, |ui| { ui.horizontal_wrapped(|ui| { Self::toggle_dark_mode_button(&self.tx, ui); ui.separator(); ui.menu_button("📁 File", |ui| self.file_menu(ui, cfg)); ui.menu_button("🔨 Controls", |ui| self.controls_menu(ui, cfg)); ui.menu_button("🔧 Config", |ui| self.config_menu(ui, cfg)); // icon: screen ui.menu_button("🖵 Window", |ui| self.window_menu(ui, cfg)); ui.menu_button("🕷 Debug", |ui| self.debug_menu(ui, cfg)); ui.menu_button("❓ Help", |ui| self.help_menu(ui)); if cfg!(debug_assertions) { ui.separator(); ui.label( RichText::new("⚠ Debug build ⚠") .small() .color(ui.visuals().warn_fg_color), ) .on_hover_text("TetaNES was compiled with debug assertions enabled."); } }); }); let spacing = ui.style().spacing.item_spacing; let border = 1.0; let height = inner_res.response.rect.height() + spacing.y + border; if height != self.menu_height { self.menu_height = height; self.tx.event(RendererEvent::ResizeTexture); } }); } pub fn toggle_dark_mode_button(tx: &NesEventProxy, ui: &mut Ui) { if ui.style().visuals.dark_mode { let button = Button::new("☀").frame(false); let res = ui.add(button).on_hover_text("Switch to light mode"); if res.clicked() { ui.ctx().set_visuals(Self::light_theme()); tx.event(ConfigEvent::DarkTheme(false)); } } else { let button = Button::new("🌙").frame(false); let res = ui.add(button).on_hover_text("Switch to dark mode"); if res.clicked() { ui.ctx().set_visuals(Self::dark_theme()); tx.event(ConfigEvent::DarkTheme(true)); } } } fn file_menu(&mut self, ui: &mut Ui, cfg: &Config) { let button = Button::new("📂 Load ROM...").shortcut_text(cfg.shortcut(UiAction::LoadRom)); if ui.add(button).clicked() { if self.loaded_rom.is_some() { self.run_state = RunState::AutoPaused; self.tx.event(EmulationEvent::RunState(self.run_state)); } // NOTE: Due to some platforms file dialogs blocking the event loop, // loading requires a round-trip in order for the above pause to // get processed. self.tx.event(UiEvent::LoadRomDialog); } ui.menu_button("🍺 Homebrew ROM...", |ui| self.homebrew_rom_menu(ui)); let tx = &self.tx; ui.add_enabled_ui(self.loaded_rom.is_some(), |ui| { let button = Button::new("⏹ Unload ROM...").shortcut_text(cfg.shortcut(UiAction::UnloadRom)); let res = ui.add(button).on_disabled_hover_text(Self::NO_ROM_LOADED); if res.clicked() { tx.event(EmulationEvent::UnloadRom); } let button = Button::new("🎞 Load Replay").shortcut_text(cfg.shortcut(UiAction::LoadReplay)); let res = ui .add(button) .on_hover_text("Load a replay file for the currently loaded ROM.") .on_disabled_hover_text(Self::NO_ROM_LOADED); if res.clicked() { self.run_state = RunState::AutoPaused; tx.event(EmulationEvent::RunState(self.run_state)); // NOTE: Due to some platforms file dialogs blocking the event loop, // loading requires a round-trip in order for the above pause to // get processed. tx.event(UiEvent::LoadReplayDialog); } }); if feature!(Filesystem) { ui.menu_button("🗄 Recently Played...", |ui| { // Sizing pass here since the width of the submenu can change as recent ROMS are // added or cleared. ui.scope_builder(UiBuilder::new().sizing_pass(), |ui| { if cfg.renderer.recent_roms.is_empty() { ui.label("No recent ROMs"); } else { for rom in &cfg.renderer.recent_roms { ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); if ui.button(rom.name()).clicked() { match rom { RecentRom::Homebrew { name } => { match HOMEBREW_ROMS.iter().find(|rom| rom.name == name) { Some(rom) => { tx.event(EmulationEvent::LoadRom(( rom.name.to_string(), rom.data(), ))); } None => { tx.event(UiEvent::Message(( MessageType::Error, "Failed to load rom".into(), ))); } } } RecentRom::Path(path) => { tx.event(EmulationEvent::LoadRomPath(path.to_path_buf())) } } } } } }); }); ui.separator(); } if feature!(Storage) { ui.add_enabled_ui(self.loaded_rom.is_some(), |ui| { let button = Button::new("💾 Save State").shortcut_text(cfg.shortcut(DeckAction::SaveState)); let res = ui .add(button) .on_hover_text("Save the current state to the selected save slot.") .on_disabled_hover_text(Self::NO_ROM_LOADED); if res.clicked() { tx.event(EmulationEvent::SaveState(cfg.emulation.save_slot)); }; let button = Button::new("⎗ Load State").shortcut_text(cfg.shortcut(DeckAction::LoadState)); let res = ui .add(button) .on_hover_text("Load a previous state from the selected save slot.") .on_disabled_hover_text(Self::NO_ROM_LOADED); if res.clicked() { tx.event(EmulationEvent::LoadState(cfg.emulation.save_slot)); } }); // icon: # in a square ui.menu_button("󾠬 Save Slot...", |ui| { Preferences::save_slot_radio( tx, ui, cfg.emulation.save_slot, cfg, ShowShortcut::Yes, ); }); } if feature!(OsViewports) { ui.separator(); let button = Button::new("⎆ Quit").shortcut_text(cfg.shortcut(UiAction::Quit)); if ui.add(button).clicked() { tx.event(UiEvent::Terminate); }; } } fn homebrew_rom_menu(&mut self, ui: &mut Ui) { ScrollArea::vertical().show(ui, |ui| { for rom in HOMEBREW_ROMS { ui.horizontal(|ui| { if ui.button(rom.name).clicked() { self.tx .event(EmulationEvent::LoadRom((rom.name.to_string(), rom.data()))); } let res = ui.button("ℹ").on_hover_ui(|ui| { ui.set_max_width(400.0); Self::about_homebrew(ui, rom); }); if res.clicked() { self.about_homebrew_rom_open = Some(rom); } }); } }); } fn controls_menu(&mut self, ui: &mut Ui, cfg: &Config) { let tx = &self.tx; ui.add_enabled_ui(self.loaded_rom.is_some(), |ui| { let button = Button::new(if self.run_state.paused() { "▶ Resume" } else { "⏸ Pause" }) .shortcut_text(cfg.shortcut(UiAction::TogglePause)); let res = ui.add(button).on_disabled_hover_text(Self::NO_ROM_LOADED); if res.clicked() { self.run_state = match self.run_state { RunState::Running => RunState::ManuallyPaused, RunState::ManuallyPaused | RunState::AutoPaused => RunState::Running, }; tx.event(EmulationEvent::RunState(self.run_state)); }; }); let button = Button::new(if cfg.audio.enabled { "🔇 Mute" } else { "🔊 Unmute" }) .shortcut_text(cfg.shortcut(Setting::ToggleAudio)); if ui.add(button).clicked() { tx.event(ConfigEvent::AudioEnabled(!cfg.audio.enabled)); }; ui.separator(); ui.add_enabled_ui(self.loaded_rom.is_some(), |ui| { ui.add_enabled_ui(cfg.emulation.rewind, |ui| { let button = Button::new("⟲ Instant Rewind") .shortcut_text(cfg.shortcut(Feature::InstantRewind)); let disabled_hover_text = if self.loaded_rom.is_none() { Self::NO_ROM_LOADED } else { "Rewind can be enabled under the `Config` menu." }; let res = ui .add(button) .on_hover_text("Instantly rewind state to a previous point.") .on_disabled_hover_text(disabled_hover_text); if res.clicked() { tx.event(EmulationEvent::InstantRewind); }; }); let button = Button::new("🔃 Reset") .shortcut_text(cfg.shortcut(DeckAction::Reset(ResetKind::Soft))); let res = ui .add(button) .on_hover_text("Emulate a soft reset of the NES.") .on_disabled_hover_text(Self::NO_ROM_LOADED); if res.clicked() { tx.event(EmulationEvent::Reset(ResetKind::Soft)); }; let button = Button::new("🔌 Power Cycle") .shortcut_text(cfg.shortcut(DeckAction::Reset(ResetKind::Hard))); let res = ui .add(button) .on_hover_text("Emulate a power cycle of the NES.") .on_disabled_hover_text(Self::NO_ROM_LOADED); if res.clicked() { tx.event(EmulationEvent::Reset(ResetKind::Hard)); }; }); if feature!(Filesystem) { ui.separator(); ui.add_enabled_ui(self.loaded_rom.is_some(), |ui| { let button = Button::new("🖼 Screenshot") .shortcut_text(cfg.shortcut(Feature::TakeScreenshot)); let res = ui.add(button).on_disabled_hover_text(Self::NO_ROM_LOADED); if res.clicked() { tx.event(EmulationEvent::Screenshot); }; let button_txt = if self.replay_recording { "⏹ Stop Replay Recording" } else { "🎞 Record Replay" }; let button = Button::new(button_txt) .shortcut_text(cfg.shortcut(Feature::ToggleReplayRecording)); let res = ui .add(button) .on_hover_text("Record or stop recording a game replay file.") .on_disabled_hover_text(Self::NO_ROM_LOADED); if res.clicked() { tx.event(EmulationEvent::ReplayRecord(!self.replay_recording)); }; let button_txt = if self.audio_recording { "⏹ Stop Audio Recording" } else { "🎤 Record Audio" }; let button = Button::new(button_txt) .shortcut_text(cfg.shortcut(Feature::ToggleAudioRecording)); let res = ui .add(button) .on_hover_text("Record or stop recording a audio file.") .on_disabled_hover_text(Self::NO_ROM_LOADED); if res.clicked() { tx.event(EmulationEvent::AudioRecord(!self.audio_recording)); }; }); } } fn config_menu(&mut self, ui: &mut Ui, cfg: &Config) { let tx = &self.tx; Preferences::zapper_checkbox( tx, ui, cfg.deck.zapper, cfg.shortcut(DeckAction::ToggleZapperConnected), ); Preferences::rewind_checkbox( tx, ui, cfg.emulation.rewind, cfg.shortcut(Setting::ToggleRewinding), ); Preferences::overscan_checkbox( tx, ui, cfg.renderer.hide_overscan, cfg.shortcut(Setting::ToggleOverscan), ); ui.separator(); ui.menu_button("🕒 Emulation Speed...", |ui| { let speed = cfg.emulation.speed; let button = Button::new("Increment").shortcut_text(cfg.shortcut(Setting::IncrementSpeed)); if ui.add(button).clicked() { let new_speed = cfg.next_increment_speed(); if speed != new_speed { tx.event(ConfigEvent::Speed(new_speed)); } } let button = Button::new("Decrement").shortcut_text(cfg.shortcut(Setting::DecrementSpeed)); if ui.add(button).clicked() { let new_speed = cfg.next_decrement_speed(); if speed != new_speed { tx.event(ConfigEvent::Speed(new_speed)); } } Preferences::speed_slider(tx, ui, cfg.emulation.speed); }); ui.menu_button("🏃 Run Ahead...", |ui| { Preferences::run_ahead_slider(tx, ui, cfg.emulation.run_ahead); }); ui.separator(); ui.menu_button("🌉 Video Filter...", |ui| { Preferences::video_filter_radio(tx, ui, cfg.deck.filter, cfg, ShowShortcut::Yes); }); ui.menu_button("🕶 Shader...", |ui| { Preferences::shader_radio(tx, ui, cfg.renderer.shader, cfg, ShowShortcut::Yes); }); ui.menu_button("🌎 Nes Region...", |ui| { Preferences::nes_region_radio(tx, ui, cfg.deck.region); }); ui.menu_button("🎮 Four Player...", |ui| { Preferences::four_player_radio(tx, ui, cfg.deck.four_player); }); ui.menu_button("📓 Game Genie Codes...", |ui| { self.preferences.show_genie_codes_entry(ui, cfg); ui.separator(); Preferences::genie_codes_list(tx, ui, cfg, true); }); ui.separator(); let mut preferences_open = self.preferences.open(); // icon: gear let toggle = ToggleValue::new(&mut preferences_open, "🔧 Preferences") .shortcut_text(cfg.shortcut(Menu::Preferences)); if ui.add(toggle).clicked() { self.preferences.set_open(preferences_open, &self.ctx); } let mut keybinds_open = self.keybinds.open(); // icon: keyboard let toggle = ToggleValue::new(&mut keybinds_open, "🖮 Keybinds") .shortcut_text(cfg.shortcut(Menu::Keybinds)); if ui.add(toggle).clicked() { self.keybinds.set_open(keybinds_open, &self.ctx); }; } fn window_menu(&mut self, ui: &mut Ui, cfg: &Config) { use Setting::*; let tx = &self.tx; let RendererConfig { scale, fullscreen, always_on_top, show_menubar, show_messages, .. } = cfg.renderer; ui.menu_button("📏 Window Scale...", |ui| { let button = Button::new("Increment").shortcut_text(cfg.shortcut(IncrementScale)); if ui.add(button).clicked() { let new_scale = cfg.next_increment_scale(); if scale != new_scale { tx.event(ConfigEvent::Scale(scale)); } } let button = Button::new("Decrement").shortcut_text(cfg.shortcut(DecrementScale)); if ui.add(button).clicked() { let new_scale = cfg.next_decrement_scale(); if scale != new_scale { tx.event(ConfigEvent::Scale(scale)); } } Preferences::window_scale_radio(tx, ui, cfg.renderer.scale); }); egui::gui_zoom::zoom_menu_buttons(ui); ui.separator(); Preferences::fullscreen_checkbox(tx, ui, fullscreen, cfg.shortcut(ToggleFullscreen)); Preferences::embed_viewports_checkbox(tx, ui, cfg, cfg.shortcut(ToggleEmbedViewports)); Preferences::always_on_top_checkbox(tx, ui, always_on_top, cfg.shortcut(ToggleAlwaysOnTop)); ui.separator(); Preferences::menubar_checkbox(tx, ui, show_menubar, cfg.shortcut(ToggleMenubar)); Preferences::messages_checkbox(tx, ui, show_messages, cfg.shortcut(ToggleMessages)); if feature!(ScreenReader) { Preferences::screen_reader_checkbox(ui, cfg.shortcut(ToggleScreenReader)); } } fn debug_menu(&mut self, ui: &mut Ui, cfg: &Config) { let tx = &self.tx; let mut perf_stats_open = self.perf_stats_open; let toggle = ToggleValue::new(&mut perf_stats_open, "🛠 Performance Stats") .shortcut_text(cfg.shortcut(Menu::PerfStats)); let res = ui .add(toggle) .on_hover_text("Enable a performance statistics overlay"); if res.clicked() { self.perf_stats_open = perf_stats_open; tx.event(EmulationEvent::ShowFrameStats(self.perf_stats_open)); } let mut gui_settings_open = self.ui_settings_open.load(Ordering::Acquire); let toggle = ToggleValue::new(&mut gui_settings_open, UI_SETTINGS_TITLE); let res = ui.add(toggle).on_hover_text("Toggle the UI style window"); if res.clicked() { self.ui_settings_open .store(gui_settings_open, Ordering::Release); } #[cfg(debug_assertions)] { let mut gui_inspection_open = self.ui_inspection_open.load(Ordering::Acquire); let toggle = ToggleValue::new(&mut gui_inspection_open, "🔍 UI Inspection"); let res = ui .add(toggle) .on_hover_text("Toggle the UI inspection window"); if res.clicked() { self.ui_inspection_open .store(gui_inspection_open, Ordering::Release); } let mut gui_memory_open = self.ui_memory_open.load(Ordering::Acquire); let toggle = ToggleValue::new(&mut gui_memory_open, "📝 UI Memory"); let res = ui.add(toggle).on_hover_text("Toggle the UI memory window"); if res.clicked() { self.ui_memory_open .store(gui_memory_open, Ordering::Release); } ui.toggle_value(&mut self.viewport_info_open, "ℹ Viewport Info"); #[cfg(target_arch = "wasm32")] if ui.button("❗Test panic!").clicked() { panic!("panic test"); } } ui.separator(); ui.add_enabled_ui(false, |ui| { let debugger_shortcut = cfg.shortcut(Debug::Toggle(DebugKind::Cpu)); let toggle = ToggleValue::new(&mut self.debugger_open, "🚧 Debugger") .shortcut_text(debugger_shortcut); ui.add(toggle) .on_hover_text("Toggle the Debugger.") .on_disabled_hover_text("Not yet implemented."); }); let ppu_viewer_shortcut = cfg.shortcut(Debug::Toggle(DebugKind::Ppu)); let mut open = self.ppu_viewer.open(); let toggle = ToggleValue::new(&mut open, "🌇 PPU Viewer").shortcut_text(ppu_viewer_shortcut); let res = ui.add(toggle).on_hover_text("Toggle the PPU Viewer."); if res.clicked() { self.ppu_viewer.set_open(open, &self.ctx); } ui.add_enabled_ui(false, |ui| { let apu_mixer_shortcut = cfg.shortcut(Debug::Toggle(DebugKind::Apu)); let toggle = ToggleValue::new(&mut self.apu_mixer_open, "🎼 APU Mixer") .shortcut_text(apu_mixer_shortcut); ui.add(toggle) .on_hover_text("Toggle the APU Mixer.") .on_disabled_hover_text("Not yet implemented."); }); ui.separator(); ui.add_enabled_ui(self.loaded_rom.is_some(), |ui| { let button = Button::new("➡ Step").shortcut_text(cfg.shortcut(Debug::Step(DebugStep::Into))); let res = ui .add(button) .on_hover_text("Step a single CPU instruction.") .on_disabled_hover_text(Self::NO_ROM_LOADED); if res.clicked() { tx.event(EmulationEvent::DebugStep(DebugStep::Into)); } let button = Button::new("⬆ Step Out").shortcut_text(cfg.shortcut(Debug::Step(DebugStep::Out))); let res = ui .add(button) .on_hover_text("Step out of the current CPU function.") .on_disabled_hover_text(Self::NO_ROM_LOADED); if res.clicked() { tx.event(EmulationEvent::DebugStep(DebugStep::Out)); } let button = Button::new("⮫ Step Over") .shortcut_text(cfg.shortcut(Debug::Step(DebugStep::Over))); let res = ui .add(button) .on_hover_text("Step over the next CPU instruction.") .on_disabled_hover_text(Self::NO_ROM_LOADED); if res.clicked() { tx.event(EmulationEvent::DebugStep(DebugStep::Over)); } let button = Button::new("➖ Step Scanline") .shortcut_text(cfg.shortcut(Debug::Step(DebugStep::Scanline))); let res = ui .add(button) .on_hover_text("Step an entire PPU scanline.") .on_disabled_hover_text(Self::NO_ROM_LOADED); if res.clicked() { tx.event(EmulationEvent::DebugStep(DebugStep::Scanline)); } let button = Button::new("🖼 Step Frame") .shortcut_text(cfg.shortcut(Debug::Step(DebugStep::Frame))); let res = ui .add(button) .on_hover_text("Step an entire PPU Frame.") .on_disabled_hover_text(Self::NO_ROM_LOADED); if res.clicked() { tx.event(EmulationEvent::DebugStep(DebugStep::Frame)); } }); } fn nes_frame(&mut self, ui: &mut Ui, enabled: bool, cfg: &Config, gamepads: &Gamepads) { ui.add_enabled_ui(enabled, |ui| { let tx = &self.tx; CentralPanel::default().show_inside(ui, |ui| { if self.loaded_rom.is_some() { let layout = Layout { main_dir: Direction::TopDown, main_align: Align::Center, cross_align: Align::Center, ..Default::default() }; ui.with_layout(layout, |ui| { let image = Image::from_texture(self.nes_texture.sized()) .shrink_to_fit() .sense(Sense::click()); let hover_cursor = if cfg.deck.zapper { CursorIcon::Crosshair } else { CursorIcon::Default }; let res = ui.add(image).on_hover_cursor(hover_cursor); self.nes_frame = res.rect; if cfg.deck.zapper { if res.clicked() { tx.event(EmulationEvent::ZapperTrigger); } if cfg .action_input(DeckAction::ZapperAimOffscreen) .is_some_and(|input| input_down(ui, gamepads, cfg, input)) { let pos = (ppu::size::WIDTH + 10, ppu::size::HEIGHT + 10); tx.event(EmulationEvent::ZapperAim(pos)); } else if let Some(Pos2 { x, y }) = res .hover_pos() .and_then(|Pos2 { x, y }| cursor_to_zapper(x, y, res.rect)) { let pos = (x.round() as u16, y.round() as u16); tx.event(EmulationEvent::ZapperAim(pos)); } } }); } else { ui.vertical_centered(|ui| { ui.horizontal_centered(|ui| { let image = Image::new(include_image!("../../../assets/tetanes.png")) .shrink_to_fit() .tint(Color32::GRAY); ui.add(image); }); }); } }); let mut recording_labels = Vec::new(); if self.replay_recording { recording_labels.push("Replay"); } if self.audio_recording { recording_labels.push("Audio"); } if !recording_labels.is_empty() { Frame::side_top_panel(ui.style()).show(ui, |ui| { ui.with_layout( Layout::top_down_justified(Align::LEFT).with_main_wrap(true), |ui| { ui.label( RichText::new(format!( "Recording {}...", recording_labels.join(" & ") )) .italics(), ) }, ); }); } if cfg.renderer.show_messages { if let Some(instr) = self.corrupted_cpu_instr { Frame::popup(ui.style()).show(ui, |ui| { ui.with_layout( Layout::top_down_justified(Align::LEFT).with_main_wrap(true), |ui| { ui.colored_label( Color32::RED, format!( "Invalid CPU opcode: ${:02X} {:?} #{:?} encountered. Title: {}", instr.opcode, instr.instr, instr.addr_mode, self.loaded_rom.as_ref().map(|rom| rom.name.as_str()).unwrap_or_default() ), ); ui.vertical(|ui| { ui.label("Recovery options:"); ui.horizontal(|ui| { if ui.button("Reset").clicked() { self.tx.event(EmulationEvent::Reset(ResetKind::Soft)); self.corrupted_cpu_instr = None; } if ui.button("Power Cycle").clicked() { self.tx.event(EmulationEvent::Reset(ResetKind::Hard)); self.corrupted_cpu_instr = None; } }); ui.horizontal(|ui| { if ui.button("Clear Save States").clicked() { preferences::State::clear_save_states(&self.tx); } if ui.button("Load ROM").clicked() { self.tx.event(UiEvent::LoadRomDialog); } }); }); }, ); }); } if self.error.is_some() { Frame::popup(ui.style()).show(ui, |ui| { ui.with_layout( Layout::top_down_justified(Align::LEFT).with_main_wrap(true), |ui| { self.error_bar(ui); }, ); }); } if !self.messages.is_empty() { Frame::popup(ui.style()).show(ui, |ui| { ui.with_layout( Layout::top_down_justified(Align::LEFT).with_main_wrap(true), |ui| { self.message_bar(ui); }, ); }); } if self.run_state.paused() { Frame::new().inner_margin(5.0).show(ui, |ui| { ui.heading(RichText::new("⏸").color(Color32::LIGHT_GRAY).size(40.0)); }); } } }); } fn performance_stats(&mut self, ui: &mut Ui, cfg: &Config) { let grid = Grid::new("perf_stats").num_columns(2).spacing([40.0, 6.0]); grid.show(ui, |ui| { ui.ctx().request_repaint_after(Duration::from_secs(1)); self.sys.update(); let good_color = if ui.style().visuals.dark_mode { hex_color!("#b8cc52") } else { hex_color!("#86b300") }; let warn_color = ui.style().visuals.warn_fg_color; let bad_color = ui.style().visuals.error_fg_color; let fps_color = |fps| match fps { fps if fps < 30.0 => bad_color, fps if fps < 60.0 => warn_color, _ => good_color, }; let frame_time_color = |time| match time { time if time <= 1000.0 * 1.0 / 60.0 => good_color, time if time <= 1000.0 * 1.0 / 30.0 => warn_color, _ => bad_color, }; let fps = self.frame_stats.fps; ui.strong("FPS:"); if fps.is_finite() { ui.colored_label(fps_color(fps), format!("{fps:.2}")); } else { ui.label("N/A"); } ui.end_row(); let fps_min = self.frame_stats.fps_min; ui.strong("FPS (min):"); if fps_min.is_finite() { ui.colored_label(fps_color(fps_min), format!("{fps_min:.2}")); } else { ui.label("N/A"); } ui.end_row(); let frame_time = self.frame_stats.frame_time; ui.strong("Frame Time:"); if frame_time.is_finite() { ui.colored_label(frame_time_color(frame_time), format!("{frame_time:.2} ms")); } else { ui.label("N/A"); } ui.end_row(); let frame_time_max = self.frame_stats.frame_time_max; ui.strong("Frame Time (max):"); if frame_time_max.is_finite() { ui.colored_label( frame_time_color(frame_time_max), format!("{frame_time_max:.2} ms"), ); } else { ui.label("N/A"); } ui.end_row(); ui.strong("Frame Count:"); ui.label(format!("{}", self.frame_stats.frame_count)); ui.end_row(); if let Some(stats) = self.sys.stats() { let cpu_color = |cpu| match cpu { cpu if cpu <= 25.0 => good_color, cpu if cpu <= 50.0 => warn_color, _ => bad_color, }; const fn bytes_to_mb(bytes: u64) -> u64 { bytes / 0x100000 } ui.label(""); ui.end_row(); ui.strong("CPU:"); ui.colored_label( cpu_color(stats.cpu_usage), format!("{:.2}%", stats.cpu_usage), ); ui.end_row(); ui.strong("Memory:"); ui.label(format!("{} MB", bytes_to_mb(stats.memory))); ui.end_row(); let du = stats.disk_usage; ui.strong("Disk read new/total:"); ui.label(format!( "{:.2}/{:.2} MB", bytes_to_mb(du.read_bytes), bytes_to_mb(du.total_read_bytes) )); ui.end_row(); ui.strong("Disk written new/total:"); ui.label(format!( "{:.2}/{:.2} MB", bytes_to_mb(du.written_bytes), bytes_to_mb(du.total_written_bytes), )); ui.end_row(); } ui.label(""); ui.end_row(); ui.strong("Run Time:"); let run_time = { let secs = self.start.elapsed().as_secs(); let days = secs / 86_400; let hours = (secs % 86_400) / 3_600; let mins = (secs % 3_600) / 60; let secs = secs % 60; if days > 0 { format!("{days}d {hours}h {mins}m {secs}s") } else if hours > 0 { format!("{hours}h {mins}m {secs}s") } else if mins > 0 { format!("{mins}m {secs}s") } else { format!("{secs}s") } }; ui.label(run_time); ui.end_row(); let (cursor_pos, zapper_pos) = match ui.input(|i| i.pointer.latest_pos()) { Some(Pos2 { x, y }) => { let zapper_pos = match cursor_to_zapper(x, y, self.nes_frame) { Some(Pos2 { x, y }) => format!("({x:.0}, {y:.0})"), None => "(-, -)".to_string(), }; (format!("({x:.0}, {y:.0})"), zapper_pos) } None => ("(-, -)".to_string(), "(-, -)".to_string()), }; ui.strong("Cursor Pos:"); ui.label(cursor_pos); ui.end_row(); if cfg.deck.zapper { ui.strong("Zapper Pos:"); ui.label(zapper_pos); ui.end_row(); } }); } fn help_menu(&mut self, ui: &mut Ui) { if self.version.requires_updates() && ui.button("🌐 Check for Updates...").clicked() { let notify_latest = true; self.version.check_for_updates(&self.tx, notify_latest); } ui.toggle_value(&mut self.about_open, "ℹ About"); } fn about(&mut self, ui: &mut Ui, enabled: bool) { ui.add_enabled_ui(enabled, |ui| { ui.with_layout(Layout::left_to_right(Align::Min), |ui| { let image = Image::new(include_image!("../../../assets/tetanes_icon.png")) .max_height(50.0) .shrink_to_fit(); ui.add(image); ui.vertical(|ui| { let grid = Grid::new("version").num_columns(2).spacing([40.0, 6.0]); grid.show(ui, |ui| { ui.strong("Version:"); ui.label(self.version.current()); ui.end_row(); ui.strong("GitHub:"); ui.hyperlink("https://github.com/lukexor/tetanes"); ui.end_row(); }); if feature!(Filesystem) { ui.separator(); ui.horizontal_wrapped(|ui| { let grid = Grid::new("directories").num_columns(2).spacing([40.0, 6.0]); grid.show(ui, |ui| { let config_dir = Config::default_config_dir(); ui.strong("Preferences:"); ui.label(format!("{}", config_dir.display())); ui.end_row(); let data_dir = Config::default_data_dir(); ui.strong("Save States/RAM, Replays: "); ui.label(format!("{}", data_dir.display())); ui.end_row(); let picture_dir = Config::default_picture_dir(); ui.strong("Screenshots: "); ui.label(format!("{}", picture_dir.display())); ui.end_row(); let audio_dir = Config::default_audio_dir(); ui.strong("Audio Recordings: "); ui.label(format!("{}", audio_dir.display())); ui.end_row(); }); }); } }); }); }); } fn about_homebrew(ui: &mut Ui, rom: RomAsset) { ScrollArea::vertical().show(ui, |ui| { ui.strong("Author(s):"); ui.label(rom.authors); ui.add_space(12.0); ui.strong("Description:"); ui.label(rom.description); ui.add_space(12.0); ui.strong("Source:"); ui.hyperlink(rom.source); }); } fn message_bar(&mut self, ui: &mut Ui) { let now = Instant::now(); self.messages.retain(|(_, _, expires)| now < *expires); self.messages.dedup_by(|a, b| a.1.eq(&b.1)); for (ty, message, _) in self.messages.iter().take(Self::MAX_MESSAGES) { let visuals = &ui.style().visuals; let (icon, color) = match ty { MessageType::Info => ("ℹ", visuals.widgets.noninteractive.fg_stroke.color), MessageType::Warn => ("⚠", visuals.warn_fg_color), MessageType::Error => ("❗", visuals.error_fg_color), }; ui.colored_label(color, format!("{icon} {message}")); } } fn error_bar(&mut self, ui: &mut Ui) { if let Some(error) = self.error.clone() { let available_width = ui.available_width(); ui.set_min_width(available_width); ui.horizontal(|ui| { let res = ui.colored_label(Color32::RED, error); ui.add_space(available_width - res.rect.width() - 30.0); if ui.button("❌").clicked() { self.error = None; } }); } } pub fn dark_theme() -> egui::Visuals { Visuals { dark_mode: true, widgets: egui::style::Widgets { noninteractive: WidgetVisuals { weak_bg_fill: hex_color!("#14191f"), bg_fill: hex_color!("#14191f"), bg_stroke: Stroke::new(1f32, hex_color!("#253340")), // separators, indentation lines fg_stroke: Stroke::new(1f32, hex_color!("#e6b673")), // normal text color corner_radius: CornerRadius::ZERO, expansion: 0.0, }, inactive: WidgetVisuals { weak_bg_fill: hex_color!("#253340"), // button background bg_fill: hex_color!("#253340"), // checkbox background bg_stroke: Stroke::default(), fg_stroke: Stroke::new(1f32, hex_color!("#a9491f")), // button text corner_radius: CornerRadius::ZERO, expansion: 0.0, }, hovered: WidgetVisuals { weak_bg_fill: hex_color!("#212733"), bg_fill: hex_color!("#212733"), bg_stroke: Stroke::new(1f32, hex_color!("#f29718")), // e.g. hover over window edge or button fg_stroke: Stroke::new(1.5f32, hex_color!("#ffb454")), corner_radius: CornerRadius::ZERO, expansion: 1.0, }, active: WidgetVisuals { weak_bg_fill: hex_color!("#253340"), bg_fill: hex_color!("#253340"), bg_stroke: Stroke::new(1f32, hex_color!("#fed7aa")), fg_stroke: Stroke::new(1f32, hex_color!("#fed7aa")), corner_radius: CornerRadius::ZERO, expansion: 1.0, }, open: WidgetVisuals { weak_bg_fill: hex_color!("#151a1e"), bg_fill: hex_color!("#14191f"), bg_stroke: Stroke::new(1f32, hex_color!("#253340")), fg_stroke: Stroke::new(1f32, hex_color!("#ffb454")), corner_radius: CornerRadius::ZERO, expansion: 0.0, }, }, selection: Selection { bg_fill: hex_color!("#253340"), stroke: Stroke::new(1f32, hex_color!("#ffb454")), }, hyperlink_color: hex_color!("#36a3d9"), faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so extreme_bg_color: hex_color!("#091015"), // e.g. TextEdit background code_bg_color: hex_color!("#253340"), warn_fg_color: hex_color!("#e7c547"), error_fg_color: hex_color!("#ff3333"), window_corner_radius: CornerRadius::ZERO, window_fill: hex_color!("#14191f"), window_stroke: Stroke::new(1f32, hex_color!("#253340")), window_highlight_topmost: true, menu_corner_radius: CornerRadius::ZERO, panel_fill: hex_color!("#14191f"), text_cursor: TextCursorStyle { stroke: Stroke::new(1f32, hex_color!("#95e6cb")), ..Default::default() }, striped: true, handle_shape: HandleShape::Rect { aspect_ratio: 1.25 }, ..Default::default() } } pub fn light_theme() -> egui::Visuals { egui::Visuals { dark_mode: false, widgets: egui::style::Widgets { noninteractive: WidgetVisuals { weak_bg_fill: hex_color!("#ffffff"), bg_fill: hex_color!("#ffffff"), bg_stroke: Stroke::new(1f32, hex_color!("#d9d7ce")), // separators, indentation lines fg_stroke: Stroke::new(1f32, hex_color!("#253340")), // normal text color corner_radius: CornerRadius::ZERO, expansion: 0.0, }, inactive: WidgetVisuals { weak_bg_fill: hex_color!("#d9d8d7"), // button background bg_fill: hex_color!("#d9d8d7"), // checkbox background bg_stroke: Stroke::default(), fg_stroke: Stroke::new(1f32, hex_color!("#a2441b")), // button text corner_radius: CornerRadius::ZERO, expansion: 0.0, }, hovered: WidgetVisuals { weak_bg_fill: hex_color!("#ffd9b3"), bg_fill: hex_color!("#ffd9b3"), bg_stroke: Stroke::new(1f32, hex_color!("#ff6a00")), // e.g. hover over window edge or button fg_stroke: Stroke::new(1.5f32, hex_color!("#ff6a00")), corner_radius: CornerRadius::ZERO, expansion: 1.0, }, active: WidgetVisuals { weak_bg_fill: hex_color!("#d9d7ce"), bg_fill: hex_color!("#d9d7ce"), bg_stroke: Stroke::new(1f32, hex_color!("#3e4b59")), fg_stroke: Stroke::new(1f32, hex_color!("#3e4b59")), corner_radius: CornerRadius::ZERO, expansion: 1.0, }, open: WidgetVisuals { weak_bg_fill: hex_color!("#f3f3f3"), bg_fill: hex_color!("#ffffff"), bg_stroke: Stroke::new(1f32, hex_color!("#d9d7ce")), fg_stroke: Stroke::new(1f32, hex_color!("#ff6a00")), corner_radius: CornerRadius::ZERO, expansion: 0.0, }, }, selection: Selection { bg_fill: hex_color!("#efc9a3"), stroke: Stroke::new(1f32, hex_color!("#b2340b")), }, hyperlink_color: hex_color!("#36a3d9"), faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so extreme_bg_color: hex_color!("#e6e1cf"), // e.g. TextEdit background code_bg_color: hex_color!("#fafafa"), warn_fg_color: hex_color!("#e7c547"), error_fg_color: hex_color!("#ff3333"), window_fill: hex_color!("#f0eee4"), window_stroke: Stroke::new(1f32, hex_color!("#d9d8d7")), panel_fill: hex_color!("#f0eee4"), text_cursor: TextCursorStyle { stroke: Stroke::new(1f32, hex_color!("#4cbf99")), ..Default::default() }, ..Self::dark_theme() } } } ================================================ FILE: tetanes/src/nes/renderer/painter.rs ================================================ use crate::nes::renderer::shader::{self, Shader}; use anyhow::{Context, anyhow}; use egui::{ NumExt, ViewportId, ViewportIdMap, ViewportIdSet, ahash::HashMap, epaint::{self, Primitive, Vertex}, }; use std::{ borrow::Cow, collections::hash_map::Entry, iter, num::{NonZeroU32, NonZeroU64}, ops::{Deref, Range}, sync::Arc, }; use wgpu::util::DeviceExt; use winit::{dpi::PhysicalSize, window::Window}; #[derive(Debug)] #[must_use] pub struct Surface { inner: wgpu::Surface<'static>, shader_resources: Option, width: u32, height: u32, } impl Surface { pub fn new( instance: &wgpu::Instance, window: Arc, size: PhysicalSize, ) -> anyhow::Result { Ok(Self { inner: instance.create_surface(window)?, shader_resources: None, width: size.width, height: size.height, }) } fn create_texture_view( &self, device: &wgpu::Device, format: wgpu::TextureFormat, ) -> wgpu::TextureView { device .create_texture(&wgpu::TextureDescriptor { label: Some("surface_texture"), size: wgpu::Extent3d { width: self.width, height: self.height, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, view_formats: &[], }) .create_view(&wgpu::TextureViewDescriptor::default()) } fn set_shader( &mut self, device: &wgpu::Device, format: wgpu::TextureFormat, uniform_bind_group_layout: &wgpu::BindGroupLayout, shader: Shader, ) { self.shader_resources = shader::Resources::new( device, format, self.create_texture_view(device, format), uniform_bind_group_layout, shader, ); } } impl Deref for Surface { type Target = wgpu::Surface<'static>; fn deref(&self) -> &Self::Target { &self.inner } } #[derive(Debug)] #[must_use] pub struct Painter { instance: wgpu::Instance, render_state: Option, surfaces: ViewportIdMap, } impl Default for Painter { fn default() -> Self { let descriptor = if cfg!(all(target_arch = "wasm32", not(feature = "webgpu"))) { // TODO: WebGPU is still unsafe/experimental on Linux in Chrome and still nightly on // Firefox wgpu::InstanceDescriptor { backends: wgpu::Backends::all().difference(wgpu::Backends::BROWSER_WEBGPU), ..wgpu::InstanceDescriptor::new_without_display_handle() } } else { wgpu::InstanceDescriptor::new_without_display_handle() }; Self { instance: wgpu::Instance::new(descriptor), render_state: None, surfaces: Default::default(), } } } impl Painter { pub fn new() -> Self { Self::default() } pub fn set_shader(&mut self, shader: Shader) { if let Some(render_state) = &mut self.render_state { render_state.shader = shader; for surface in self.surfaces.values_mut() { surface.set_shader( &render_state.device, render_state.format, &render_state.uniform_bind_group_layout, shader, ); } } } pub async fn set_window( &mut self, viewport_id: ViewportId, window: Option>, ) -> anyhow::Result<()> { if let Some(window) = window { if let Entry::Vacant(entry) = self.surfaces.entry(viewport_id) { let size = window.inner_size(); let mut surface = Surface::new(&self.instance, window, size)?; let render_state = match &mut self.render_state { Some(render_state) => render_state, None => { let render_state = RenderState::create(&self.instance, &surface).await?; self.render_state.get_or_insert(render_state) } }; if let (Some(width), Some(height)) = (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) { render_state.resize_surface(&mut surface, width, height); } entry.insert(surface); } } else { self.surfaces.clear(); } Ok(()) } pub fn paint( &mut self, viewport_id: ViewportId, pixels_per_point: f32, clipped_primitives: &[epaint::ClippedPrimitive], textures_delta: &epaint::textures::TexturesDelta, ) { let Some(render_state) = &mut self.render_state else { return; }; let Some(surface) = self.surfaces.get_mut(&viewport_id) else { return; }; let mut encoder = render_state .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("encoder"), }); // Upload all resources for the GPU. let size_in_pixels = [surface.width, surface.height]; let screen_descriptor = ScreenDescriptor { size_in_pixels, pixels_per_point, }; for (id, image_delta) in &textures_delta.set { render_state.update_texture(*id, image_delta); } render_state.update_buffers(clipped_primitives, &screen_descriptor); let output_frame = match surface.get_current_texture() { wgpu::CurrentSurfaceTexture::Success(frame) => frame, wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame, wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => return, wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => { if let (Some(width), Some(height)) = ( NonZeroU32::new(surface.width), NonZeroU32::new(surface.height), ) { render_state.resize_surface(surface, width, height); } return; } wgpu::CurrentSurfaceTexture::Validation => { tracing::error!("failed to acquire next frame"); return; } }; { let view = match &surface.shader_resources { Some(shader) => &shader.view, None => &output_frame .texture .create_view(&wgpu::TextureViewDescriptor::default()), }; let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("main_render_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view, resolve_target: None, depth_slice: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, multiview_mask: None, }); render_state.render(&mut render_pass, clipped_primitives, &screen_descriptor); } if let Some(shader) = &surface.shader_resources { let view = &output_frame .texture .create_view(&wgpu::TextureViewDescriptor::default()); let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("main_render_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view, resolve_target: None, depth_slice: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, multiview_mask: None, }); render_pass.set_scissor_rect(0, 0, size_in_pixels[0], size_in_pixels[1]); render_pass.set_viewport( 0.0, 0.0, size_in_pixels[0] as f32, size_in_pixels[1] as f32, 0.0, 1.0, ); render_pass.set_pipeline(&shader.render_pipeline); render_pass.set_bind_group(0, &render_state.uniform_bind_group, &[]); render_pass.set_bind_group(1, &shader.texture_bind_group, &[]); render_pass.draw(0..3, 0..1); } for id in &textures_delta.free { render_state.textures.remove(id); } render_state.queue.submit(iter::once(encoder.finish())); output_frame.present(); } pub const fn render_state(&self) -> Option<&RenderState> { self.render_state.as_ref() } pub const fn render_state_mut(&mut self) -> Option<&mut RenderState> { self.render_state.as_mut() } pub fn on_window_resized(&mut self, viewport_id: ViewportId, width: u32, height: u32) { if let (Some(width), Some(height)) = (NonZeroU32::new(width), NonZeroU32::new(height)) && let Some(surface) = self.surfaces.get_mut(&viewport_id) && let Some(render_state) = &mut self.render_state { render_state.resize_surface(surface, width, height); } } pub fn retain_surfaces(&mut self, viewport_ids: &ViewportIdSet) { self.surfaces.retain(|id, _| viewport_ids.contains(id)); } pub fn destroy(&mut self) { self.surfaces.clear(); let _ = self.render_state.take(); } } #[derive(Debug)] #[must_use] struct SlicedBuffer { buffer: wgpu::Buffer, slices: Vec>, capacity: wgpu::BufferAddress, } #[derive(Debug)] #[must_use] pub struct RenderState { pub device: wgpu::Device, pub queue: wgpu::Queue, pub format: wgpu::TextureFormat, pipeline: wgpu::RenderPipeline, index_buffer: SlicedBuffer, vertex_buffer: SlicedBuffer, uniform_buffer: wgpu::Buffer, previous_uniform_buffer_content: UniformBuffer, uniform_bind_group: wgpu::BindGroup, uniform_bind_group_layout: wgpu::BindGroupLayout, texture_bind_group_layout: wgpu::BindGroupLayout, shader: Shader, /// Map of egui texture IDs to textures and their associated bindgroups (texture view + /// sampler). The texture may be None if the `TextureId` is just a handle to a user-provided /// sampler. textures: HashMap, wgpu::BindGroup)>, next_texture_id: u64, samplers: HashMap, } impl RenderState { async fn create( instance: &wgpu::Instance, surface: &wgpu::Surface<'_>, ) -> anyhow::Result { let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, compatible_surface: Some(surface), force_fallback_adapter: false, }) .await .context("failed to find suitable wgpu adapter")?; tracing::debug!("requested wgpu adapter: {:?}", adapter.get_info()); let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl { wgpu::Limits::downlevel_webgl2_defaults() } else { wgpu::Limits::default() }; let device_descriptor = wgpu::DeviceDescriptor { label: Some("wgpu device"), // TODO: maybe CLEAR_TEXTURE? required_limits: wgpu::Limits { max_texture_dimension_2d: 8192, ..base_limits }, ..Default::default() }; let mut connection = adapter.request_device(&device_descriptor).await; // Creating device may fail if adapter doesn't support the default cfg, so try to // recover with lower limits. Specifically max_texture_dimension_2d has a downlevel default // of 2048. egui_wgpu wants 8192 for 4k displays, but not all platforms support that yet. if let Err(err) = connection { tracing::error!("failed to create wgpu device: {err:?}, retrying with lower limits"); connection = adapter .request_device(&wgpu::DeviceDescriptor { required_limits: wgpu::Limits { max_texture_dimension_2d: 4096, // Default Edge installed on Windows 10 is limited to 6 attachments, // and we never need more than 1. max_color_attachments: 6, ..base_limits }, ..device_descriptor }) .await } let capabilities = surface.get_capabilities(&adapter); let format = capabilities .formats .iter() .copied() .find(|format| { // egui prefers these formats matches!( format, wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm ) }) .unwrap_or_else(|| { tracing::warn!(format = ?capabilities.formats[0], "failling back to first available format"); capabilities.formats[0] }); let (device, queue) = connection.map_err(|err| anyhow!("failed to create wgpu device: {err:?}"))?; let shader_module_desc = wgpu::include_wgsl!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/gui.wgsl")); let shader_module = device.create_shader_module(shader_module_desc); let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("gui uniform buffer"), contents: bytemuck::cast_slice(&[UniformBuffer::default()]), usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, }); let uniform_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("gui uniform bind group layout"), entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: NonZeroU64::new( std::mem::size_of::() as _, ), }, count: None, }], }); let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("gui uniform bind group"), layout: &uniform_bind_group_layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &uniform_buffer, offset: 0, size: None, }), }], }); let texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("gui texture bind group layout"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, multisampled: false, view_dimension: wgpu::TextureViewDimension::D2, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, ], }); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("gui pipeline layout"), bind_group_layouts: &[ Some(&uniform_bind_group_layout), Some(&texture_bind_group_layout), ], immediate_size: 0, }); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("gui pipeline"), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { entry_point: Some("vs_main"), module: &shader_module, buffers: &[wgpu::VertexBufferLayout { array_stride: 5 * 4, step_mode: wgpu::VertexStepMode::Vertex, // 0: vec2 position // 1: vec2 uv coordinates // 2: uint color attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32], }], compilation_options: wgpu::PipelineCompilationOptions::default() }, fragment: Some(wgpu::FragmentState { module: &shader_module, entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { format, blend: Some(wgpu::BlendState { color: wgpu::BlendComponent { src_factor: wgpu::BlendFactor::One, dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, operation: wgpu::BlendOperation::Add, }, alpha: wgpu::BlendComponent { src_factor: wgpu::BlendFactor::OneMinusDstAlpha, dst_factor: wgpu::BlendFactor::One, operation: wgpu::BlendOperation::Add, }, }), write_mask: wgpu::ColorWrites::ALL, })], compilation_options: wgpu::PipelineCompilationOptions::default() }), primitive: wgpu::PrimitiveState::default(), depth_stencil: None, multisample: wgpu::MultisampleState::default(), multiview_mask: None, cache: None, } ); const INDEX_BUFFER_START_CAPACITY: wgpu::BufferAddress = (std::mem::size_of::() * 1024 * 3) as _; const VERTEX_BUFFER_START_CAPACITY: wgpu::BufferAddress = (std::mem::size_of::() * 1024) as _; let index_buffer = SlicedBuffer { buffer: Self::create_index_buffer(&device, INDEX_BUFFER_START_CAPACITY), slices: Vec::with_capacity(64), capacity: INDEX_BUFFER_START_CAPACITY, }; let vertex_buffer = SlicedBuffer { buffer: Self::create_vertex_buffer(&device, VERTEX_BUFFER_START_CAPACITY), slices: Vec::with_capacity(64), capacity: VERTEX_BUFFER_START_CAPACITY, }; Ok(Self { device, queue, format, pipeline, index_buffer, vertex_buffer, uniform_buffer, previous_uniform_buffer_content: Default::default(), uniform_bind_group, uniform_bind_group_layout, texture_bind_group_layout, shader: Shader::default(), textures: Default::default(), next_texture_id: 0, samplers: Default::default(), }) } pub fn max_texture_side(&self) -> u32 { self.device.limits().max_texture_dimension_2d } pub fn register_texture( &mut self, label: Option<&str>, view: &wgpu::TextureView, sampler_descriptor: wgpu::SamplerDescriptor<'_>, ) -> epaint::TextureId { let sampler = self.device.create_sampler(&sampler_descriptor); let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { label, layout: &self.texture_bind_group_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(view), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler), }, ], }); let id = epaint::TextureId::User(self.next_texture_id); self.textures.insert(id, (None, bind_group)); self.next_texture_id += 1; id } fn create_vertex_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer { device.create_buffer(&wgpu::BufferDescriptor { label: Some("gui vertex buffer"), usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, size, mapped_at_creation: false, }) } fn create_index_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer { device.create_buffer(&wgpu::BufferDescriptor { label: Some("gui index buffer"), usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, size, mapped_at_creation: false, }) } fn resize_surface(&self, surface: &mut Surface, width: NonZeroU32, height: NonZeroU32) { surface.width = width.get(); surface.height = height.get(); surface.configure( &self.device, &wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: self.format, width: width.get(), height: height.get(), // TODO: Support disabling vsync present_mode: wgpu::PresentMode::AutoVsync, desired_maximum_frame_latency: 2, alpha_mode: wgpu::CompositeAlphaMode::Auto, view_formats: vec![self.format], }, ); surface.set_shader( &self.device, self.format, &self.uniform_bind_group_layout, self.shader, ); } pub fn update_texture(&mut self, id: epaint::TextureId, image_delta: &epaint::ImageDelta) { let width = image_delta.image.width() as u32; let height = image_delta.image.height() as u32; let size = wgpu::Extent3d { width, height, depth_or_array_layers: 1, }; let data_color32 = match &image_delta.image { epaint::ImageData::Color(image) => { assert_eq!( width as usize * height as usize, image.pixels.len(), "Mismatch between texture size and texel count" ); Cow::Borrowed(&image.pixels) } }; let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice()); let queue_write_data_to_texture = |texture, origin| { self.queue.write_texture( wgpu::TexelCopyTextureInfo { texture, mip_level: 0, origin, aspect: wgpu::TextureAspect::All, }, data_bytes, wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(4 * width), rows_per_image: Some(height), }, size, ); }; if let Some(pos) = image_delta.pos { // update the existing texture let (texture, _bind_group) = self .textures .get(&id) .expect("Tried to update a texture that has not been allocated yet."); let origin = wgpu::Origin3d { x: pos[0] as u32, y: pos[1] as u32, z: 0, }; queue_write_data_to_texture( texture.as_ref().expect("Tried to update user texture."), origin, ); } else { // allocate a new texture // Use same label for all resources associated with this texture id (no point in retyping the type) let label_str = format!("texture_{id:?}"); let label = Some(label_str.as_str()); let texture = { self.device.create_texture(&wgpu::TextureDescriptor { label, size, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba8UnormSrgb, // Minspec for wgpu WebGL emulation is WebGL2, so this should always be supported. usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb], }) }; let sampler = self .samplers .entry(image_delta.options) .or_insert_with(|| Self::create_sampler(image_delta.options, &self.device)); let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { label, layout: &self.texture_bind_group_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(sampler), }, ], }); let origin = wgpu::Origin3d::ZERO; queue_write_data_to_texture(&texture, origin); self.textures.insert(id, (Some(texture), bind_group)); }; } pub fn update_buffers( &mut self, paint_jobs: &[epaint::ClippedPrimitive], screen_descriptor: &ScreenDescriptor, ) { let screen_size_in_points = screen_descriptor.screen_size_in_points(); let uniform_buffer_content = UniformBuffer { screen_size_in_points, _padding: Default::default(), }; if uniform_buffer_content != self.previous_uniform_buffer_content { self.queue.write_buffer( &self.uniform_buffer, 0, bytemuck::cast_slice(&[uniform_buffer_content]), ); self.previous_uniform_buffer_content = uniform_buffer_content; } // Determine how many vertices & indices need to be rendered, and gather prepare callbacks // let mut callbacks = Vec::new(); let (vertex_count, index_count) = paint_jobs.iter().fold((0, 0), |acc, clipped_primitive| { if let Primitive::Mesh(mesh) = &clipped_primitive.primitive { (acc.0 + mesh.vertices.len(), acc.1 + mesh.indices.len()) } else { acc } }); if index_count > 0 { self.index_buffer.slices.clear(); let required_index_buffer_size = (std::mem::size_of::() * index_count) as u64; if self.index_buffer.capacity < required_index_buffer_size { // Resize index buffer if needed. self.index_buffer.capacity = (self.index_buffer.capacity * 2).at_least(required_index_buffer_size); self.index_buffer.buffer = Self::create_index_buffer(&self.device, self.index_buffer.capacity); } let index_buffer_staging = self.queue.write_buffer_with( &self.index_buffer.buffer, 0, NonZeroU64::new(required_index_buffer_size).expect("valid index buffer size"), ); let Some(mut index_buffer_staging) = index_buffer_staging else { panic!( "Failed to create staging buffer for index data. Index count: {index_count}. Required index buffer size: {required_index_buffer_size}. Actual size {} and capacity: {} (bytes)", self.index_buffer.buffer.size(), self.index_buffer.capacity ); }; let mut index_offset = 0; for epaint::ClippedPrimitive { primitive, .. } in paint_jobs { if let Primitive::Mesh(mesh) = primitive { let size = mesh.indices.len() * std::mem::size_of::(); index_buffer_staging .slice(index_offset..(size + index_offset)) .copy_from_slice(bytemuck::cast_slice(&mesh.indices)); self.index_buffer .slices .push(index_offset..(size + index_offset)); index_offset += size; } } } if vertex_count > 0 { self.vertex_buffer.slices.clear(); let required_vertex_buffer_size = (std::mem::size_of::() * vertex_count) as u64; if self.vertex_buffer.capacity < required_vertex_buffer_size { // Resize vertex buffer if needed. self.vertex_buffer.capacity = (self.vertex_buffer.capacity * 2).at_least(required_vertex_buffer_size); self.vertex_buffer.buffer = Self::create_vertex_buffer(&self.device, self.vertex_buffer.capacity); } let vertex_buffer_staging = self.queue.write_buffer_with( &self.vertex_buffer.buffer, 0, NonZeroU64::new(required_vertex_buffer_size).expect("valid vertex buffer size"), ); let Some(mut vertex_buffer_staging) = vertex_buffer_staging else { panic!( "Failed to create staging buffer for vertex data. Vertex count: {vertex_count}. Required vertex buffer size: {required_vertex_buffer_size}. Actual size {} and capacity: {} (bytes)", self.vertex_buffer.buffer.size(), self.vertex_buffer.capacity ); }; let mut vertex_offset = 0; for epaint::ClippedPrimitive { primitive, .. } in paint_jobs { if let Primitive::Mesh(mesh) = primitive { let size = mesh.vertices.len() * std::mem::size_of::(); vertex_buffer_staging .slice(vertex_offset..(size + vertex_offset)) .copy_from_slice(bytemuck::cast_slice(&mesh.vertices)); self.vertex_buffer .slices .push(vertex_offset..(size + vertex_offset)); vertex_offset += size; } } } } pub fn render<'rp>( &'rp self, render_pass: &mut wgpu::RenderPass<'rp>, paint_jobs: &'rp [epaint::ClippedPrimitive], screen_descriptor: &ScreenDescriptor, ) { let pixels_per_point = screen_descriptor.pixels_per_point; let size_in_pixels = screen_descriptor.size_in_pixels; render_pass.set_scissor_rect(0, 0, size_in_pixels[0], size_in_pixels[1]); render_pass.set_viewport( 0.0, 0.0, size_in_pixels[0] as f32, size_in_pixels[1] as f32, 0.0, 1.0, ); render_pass.set_pipeline(&self.pipeline); render_pass.set_bind_group(0, &self.uniform_bind_group, &[]); let mut index_buffer_slices = self.index_buffer.slices.iter(); let mut vertex_buffer_slices = self.vertex_buffer.slices.iter(); for epaint::ClippedPrimitive { clip_rect, primitive, } in paint_jobs { let rect = ScissorRect::new(clip_rect, pixels_per_point, size_in_pixels); if rect.width == 0 || rect.height == 0 { // Skip rendering zero-sized clip areas. if let Primitive::Mesh(_) = primitive { // If this is a mesh, we need to advance the index and vertex buffer iterators: index_buffer_slices.next(); vertex_buffer_slices.next(); } continue; } render_pass.set_scissor_rect(rect.x, rect.y, rect.width, rect.height); if let Primitive::Mesh(mesh) = primitive { // These expects should be valid because update_buffers inserts a slice for every // primitive let index_buffer_slice = index_buffer_slices .next() .expect("valid index buffer slice"); let vertex_buffer_slice = vertex_buffer_slices .next() .expect("valid vertex buffer slice"); if let Some((_texture, bind_group)) = self.textures.get(&mesh.texture_id) { render_pass.set_bind_group(1, bind_group, &[]); render_pass.set_index_buffer( self.index_buffer .buffer .slice(index_buffer_slice.start as u64..index_buffer_slice.end as u64), wgpu::IndexFormat::Uint32, ); render_pass.set_vertex_buffer( 0, self.vertex_buffer.buffer.slice( vertex_buffer_slice.start as u64..vertex_buffer_slice.end as u64, ), ); render_pass.draw_indexed(0..mesh.indices.len() as u32, 0, 0..1); } else { tracing::warn!("Missing texture: {:?}", mesh.texture_id); } } } render_pass.set_scissor_rect(0, 0, size_in_pixels[0], size_in_pixels[1]); } fn create_sampler( options: epaint::textures::TextureOptions, device: &wgpu::Device, ) -> wgpu::Sampler { let mag_filter = match options.magnification { epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest, epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear, }; let min_filter = match options.minification { epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest, epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear, }; let address_mode = match options.wrap_mode { epaint::textures::TextureWrapMode::ClampToEdge => wgpu::AddressMode::ClampToEdge, epaint::textures::TextureWrapMode::Repeat => wgpu::AddressMode::Repeat, epaint::textures::TextureWrapMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat, }; device.create_sampler(&wgpu::SamplerDescriptor { label: Some(&format!( "gui sampler (mag: {mag_filter:?}, min {min_filter:?})" )), mag_filter, min_filter, address_mode_u: address_mode, address_mode_v: address_mode, ..Default::default() }) } } /// Uniform buffer used when rendering. #[derive(Default, Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[repr(C)] struct UniformBuffer { screen_size_in_points: [f32; 2], // Uniform buffers need to be at least 16 bytes in WebGL. // See https://github.com/gfx-rs/wgpu/issues/2072 _padding: [u32; 2], } impl PartialEq for UniformBuffer { fn eq(&self, other: &Self) -> bool { self.screen_size_in_points == other.screen_size_in_points } } /// Information about the screen used for rendering. pub struct ScreenDescriptor { /// Size of the window in physical pixels. pub size_in_pixels: [u32; 2], /// HiDPI scale factor (pixels per point). pub pixels_per_point: f32, } impl ScreenDescriptor { /// size in "logical" points fn screen_size_in_points(&self) -> [f32; 2] { [ self.size_in_pixels[0] as f32 / self.pixels_per_point, self.size_in_pixels[1] as f32 / self.pixels_per_point, ] } } /// A Rect in physical pixel space, used for setting clipping rectangles. struct ScissorRect { x: u32, y: u32, width: u32, height: u32, } impl ScissorRect { fn new(clip_rect: &epaint::Rect, pixels_per_point: f32, target_size: [u32; 2]) -> Self { // Transform clip rect to physical pixels: let clip_min_x = pixels_per_point * clip_rect.min.x; let clip_min_y = pixels_per_point * clip_rect.min.y; let clip_max_x = pixels_per_point * clip_rect.max.x; let clip_max_y = pixels_per_point * clip_rect.max.y; // Round to integer: let clip_min_x = clip_min_x.round() as u32; let clip_min_y = clip_min_y.round() as u32; let clip_max_x = clip_max_x.round() as u32; let clip_max_y = clip_max_y.round() as u32; // Clamp: let clip_min_x = clip_min_x.clamp(0, target_size[0]); let clip_min_y = clip_min_y.clamp(0, target_size[1]); let clip_max_x = clip_max_x.clamp(clip_min_x, target_size[0]); let clip_max_y = clip_max_y.clamp(clip_min_y, target_size[1]); Self { x: clip_min_x, y: clip_min_y, width: clip_max_x - clip_min_x, height: clip_max_y - clip_min_y, } } } ================================================ FILE: tetanes/src/nes/renderer/shader.rs ================================================ use serde::{Deserialize, Serialize}; use thiserror::Error; #[derive(Error, Debug)] #[must_use] #[error("failed to parse `VideoFilter`")] pub struct ParseShaderError; #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[must_use] pub enum Shader { Default, #[default] CrtEasymode, } impl Shader { pub const fn as_slice() -> &'static [Self] { &[Self::Default, Self::CrtEasymode] } } impl AsRef for Shader { fn as_ref(&self) -> &str { match self { Self::Default => "Default", Self::CrtEasymode => "CRT Easymode", } } } impl TryFrom for Shader { type Error = ParseShaderError; fn try_from(value: usize) -> Result { Ok(match value { 0 => Self::Default, 1 => Self::CrtEasymode, _ => return Err(ParseShaderError), }) } } #[derive(Debug)] #[must_use] pub struct Resources { pub view: wgpu::TextureView, pub texture_bind_group: wgpu::BindGroup, pub render_pipeline: wgpu::RenderPipeline, } impl Resources { pub fn new( device: &wgpu::Device, format: wgpu::TextureFormat, view: wgpu::TextureView, uniform_bind_group_layout: &wgpu::BindGroupLayout, shader: Shader, ) -> Option { let shader_module_desc = match shader { Shader::Default => return None, Shader::CrtEasymode => wgpu::include_wgsl!(concat!( env!("CARGO_MANIFEST_DIR"), "/shaders/crt-easymode.wgsl" )), }; let shader_module = device.create_shader_module(shader_module_desc); let texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("bind group layout"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, multisampled: false, view_dimension: wgpu::TextureViewDimension::D2, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, ], }); let sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: Some("sampler"), address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_w: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Nearest, min_filter: wgpu::FilterMode::Nearest, mipmap_filter: wgpu::MipmapFilterMode::Nearest, ..Default::default() }); let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("nes frame bind group"), layout: &texture_bind_group_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler), }, ], }); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("shader pipeline layout"), bind_group_layouts: &[ Some(uniform_bind_group_layout), Some(&texture_bind_group_layout), ], immediate_size: 0, }); let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("render pipeline"), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &shader_module, entry_point: Some("vs_main"), buffers: &[], compilation_options: wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader_module, entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { format, blend: None, write_mask: wgpu::ColorWrites::ALL, })], compilation_options: wgpu::PipelineCompilationOptions::default(), }), primitive: wgpu::PrimitiveState::default(), depth_stencil: None, multisample: wgpu::MultisampleState::default(), multiview_mask: None, cache: None, }); Some(Self { view, texture_bind_group, render_pipeline, }) } } ================================================ FILE: tetanes/src/nes/renderer/texture.rs ================================================ use crate::nes::renderer::painter::RenderState; use egui::{TextureId, Vec2, load::SizedTexture}; #[derive(Debug)] #[must_use] pub struct Texture { pub label: Option<&'static str>, pub id: TextureId, pub texture: wgpu::Texture, pub size: Vec2, pub output_size: Vec2, pub view: wgpu::TextureView, pub aspect_ratio: f32, } impl Texture { pub fn new( render_state: &mut RenderState, size: Vec2, aspect_ratio: f32, label: Option<&'static str>, ) -> Self { let max_texture_side = render_state.max_texture_side() as f32; let texture = render_state .device .create_texture(&wgpu::TextureDescriptor { label, size: wgpu::Extent3d { width: size.x.min(max_texture_side) as u32, height: size.y.min(max_texture_side) as u32, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba8UnormSrgb, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); let view = texture.create_view(&wgpu::TextureViewDescriptor { label, dimension: Some(wgpu::TextureViewDimension::D2), ..Default::default() }); let sampler_descriptor = wgpu::SamplerDescriptor { label: Some("sampler"), address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_w: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Nearest, min_filter: wgpu::FilterMode::Nearest, mipmap_filter: wgpu::MipmapFilterMode::Nearest, ..Default::default() }; let id = render_state.register_texture(label, &view, sampler_descriptor); Self { label, texture, size, output_size: Vec2 { x: size.x * aspect_ratio, y: size.y, }, view, aspect_ratio, id, } } pub fn resize(&mut self, render_state: &mut RenderState, size: Vec2, aspect_ratio: f32) { *self = Self::new(render_state, size, aspect_ratio, self.label); } pub fn sized(&self) -> SizedTexture { SizedTexture::new(self.id, self.output_size) } pub fn update(&self, queue: &wgpu::Queue, bytes: &[u8]) { self.update_partial(queue, bytes, Vec2::ZERO, self.size); } pub fn update_partial(&self, queue: &wgpu::Queue, bytes: &[u8], origin: Vec2, size: Vec2) { let size = wgpu::Extent3d { width: size.x as u32, height: size.y as u32, depth_or_array_layers: 1, }; queue.write_texture( wgpu::TexelCopyTextureInfo { aspect: wgpu::TextureAspect::All, texture: &self.texture, mip_level: 0, origin: wgpu::Origin3d { x: origin.x as u32, y: origin.y as u32, z: 0, }, }, bytes, wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(4 * size.width), rows_per_image: Some(size.height), }, size, ); } } ================================================ FILE: tetanes/src/nes/renderer.rs ================================================ use crate::{ feature, nes::{ RunState, config::Config, event::{EmulationEvent, NesEvent, NesEventProxy, RendererEvent, UiEvent}, input::Gamepads, renderer::{ clipboard::Clipboard, event::translate_cursor, gui::{Gui, MessageType}, painter::Painter, }, }, platform::{self, BuilderExt, Initialize}, thread, }; use anyhow::Context; use crossbeam::channel::{self, Receiver}; use egui::{ DeferredViewportUiCallback, OutputCommand, Vec2, ViewportBuilder, ViewportClass, ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, ViewportOutput, WindowLevel, ahash::HashMap, }; use parking_lot::Mutex; use std::{ cell::RefCell, collections::{BTreeMap, hash_map::Entry}, rc::Rc, sync::Arc, }; use tetanes_core::{ fs, ppu, time::{Duration, Instant}, video::Frame, }; use thingbuf::{ Recycle, mpsc::{blocking::Receiver as BufReceiver, errors::TryRecvError}, }; use tracing::{debug, error, info, trace}; use winit::{ dpi::{LogicalSize, PhysicalPosition, PhysicalSize}, event_loop::ActiveEventLoop, window::{CursorGrabMode, Theme, Window, WindowButtons, WindowId}, }; pub mod clipboard; pub mod event; pub mod gui; pub mod painter; pub mod shader; pub mod texture; pub const OVERSCAN_TRIM: usize = (4 * ppu::size::WIDTH * 8) as usize; #[derive(Debug)] #[must_use] pub struct FrameRecycle; impl Recycle for FrameRecycle { fn new_element(&self) -> Frame { Frame::new() } fn recycle(&self, _frame: &mut Frame) {} } #[must_use] pub struct State { pub(crate) viewports: ViewportIdMap, viewport_from_window: HashMap, pub(crate) focused: Option, pointer_touch_id: Option, pub(crate) start_time: Instant, } impl std::fmt::Debug for State { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("State") .field("viewports", &self.viewports) .field("viewport_from_window", &self.viewport_from_window) .field("focused", &self.focused) .field("start_time", &self.focused) .finish() } } #[derive(Default)] #[must_use] pub struct Viewport { pub(crate) ids: ViewportIdPair, class: ViewportClass, builder: ViewportBuilder, pub(crate) info: ViewportInfo, pub(crate) raw_input: egui::RawInput, pub(crate) viewport_ui_cb: Option>, pub(crate) window: Option>, pub(crate) occluded: bool, cursor_icon: Option, cursor_pos: Option, pub(crate) clipboard: Clipboard, } impl std::fmt::Debug for Viewport { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Viewport") .field("ids", &self.ids) // .field("class", &self.class) // why not?! .field("builder", &self.builder) .field("info", &self.info) .field("raw_input", &self.raw_input) .field( "viewport_ui_cb", &self.viewport_ui_cb.as_ref().map(|_| "fn"), ) .field("window", &self.window) .field("occluded", &self.occluded) .field("cursor_icon", &self.cursor_icon) .field("clipboard", &self.clipboard) .finish_non_exhaustive() } } #[must_use] pub struct Renderer { pub(crate) state: Rc>, painter: Rc>, frame_rx: BufReceiver, tx: NesEventProxy, redraw_tx: Arc>, pub(crate) gui: Rc>, pub(crate) ctx: egui::Context, #[cfg(not(target_arch = "wasm32"))] accesskit: accesskit_winit::Adapter, first_frame: bool, pub(crate) last_save_time: Instant, zoom_changed: bool, resize_texture: bool, } impl std::fmt::Debug for Renderer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Renderer") .field("state", &self.state) .field("painter", &self.painter) .field("frame_rx", &self.frame_rx) .field("tx", &self.tx) .field("redraw_tx", &self.redraw_tx) .field("gui", &self.gui) .field("ctx", &self.ctx) .field("first_frame", &self.first_frame) .field("last_save_time", &self.last_save_time) .field("zoom_changed", &self.zoom_changed) .field("resize_texture", &self.resize_texture) .finish_non_exhaustive() } } #[must_use] pub struct Resources { pub(crate) ctx: egui::Context, pub(crate) window: Arc, pub(crate) painter: Painter, } impl std::fmt::Debug for Resources { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Resources") .field("window", &self.window) .finish_non_exhaustive() } } impl Renderer { /// Initializes the renderer in a platform-agnostic way. pub fn new( _event_loop: &ActiveEventLoop, tx: NesEventProxy, resources: Resources, frame_rx: BufReceiver, cfg: &Config, ) -> anyhow::Result { let Resources { ctx, window, mut painter, } = resources; let redraw_tx = Arc::new(Mutex::new(tx.clone())); ctx.set_request_repaint_callback({ let redraw_tx = redraw_tx.clone(); move |info| { // IMPORTANT: Wasm can't block if let Some(tx) = redraw_tx.try_lock() { tx.event(RendererEvent::RequestRedraw { viewport_id: info.viewport_id, when: Instant::now() + info.delay, }); } else { tracing::warn!("failed to lock redraw_tx"); } } }); // Platforms like wasm don't easily support multiple viewports, and even if it could spawn // multiple canvases for each viewport, the async requirements of wgpu would make it // impossible to render until wasm-bindgen gets proper non-blocking async/await support. if feature!(OsViewports) { ctx.set_embed_viewports(cfg.renderer.embed_viewports); } let mut viewport_from_window = HashMap::default(); viewport_from_window.insert(window.id(), ViewportId::ROOT); let mut viewports = ViewportIdMap::default(); let mut viewport = Viewport { ids: ViewportIdPair::ROOT, class: ViewportClass::Root, info: ViewportInfo { title: Some(Config::WINDOW_TITLE.to_string()), ..Default::default() }, window: Some(Arc::clone(&window)), ..Default::default() }; Viewport::update_info(&mut viewport.info, &ctx, &window); viewports.insert(viewport.ids.this, viewport); painter.set_shader(cfg.renderer.shader); let render_state = painter.render_state_mut(); let Some(render_state) = render_state else { anyhow::bail!("painter state is not initialized yet"); }; let gui = Rc::new(RefCell::new(Gui::new( ctx.clone(), tx.clone(), render_state, cfg, ))); if let Err(err) = Self::load(&ctx, cfg) { tracing::error!("{err:?}"); } // Must be done before the window is shown for the first time, which is true here, because // first_frame is set to true below #[cfg(not(target_arch = "wasm32"))] let accesskit = { accesskit_winit::Adapter::with_event_loop_proxy( _event_loop, &window, tx.inner().clone(), ) }; let state = State { viewports, viewport_from_window, focused: None, pointer_touch_id: None, start_time: Instant::now(), }; Ok(Self { state: Rc::new(RefCell::new(state)), painter: Rc::new(RefCell::new(painter)), frame_rx, tx, redraw_tx, ctx, #[cfg(not(target_arch = "wasm32"))] accesskit, gui, first_frame: true, last_save_time: Instant::now(), zoom_changed: false, resize_texture: false, }) } pub fn destroy(&mut self) { let State { viewports, viewport_from_window, focused, .. } = &mut *self.state.borrow_mut(); viewports.clear(); viewport_from_window.clear(); *focused = None; self.painter.borrow_mut().destroy(); } pub fn root_window_id(&self) -> Option { self.window_id_for_viewport(ViewportId::ROOT) } pub fn window_id_for_viewport(&self, viewport_id: ViewportId) -> Option { let state = self.state.borrow(); state .viewports .get(&viewport_id) .and_then(|viewport| viewport.window.as_ref()) .map(|window| window.id()) } pub fn viewport_id_for_window(&self, window_id: WindowId) -> Option { let state = self.state.borrow(); state .viewport_from_window .get(&window_id) .and_then(|id| state.viewports.get(id).map(|viewport| viewport.ids.this)) } pub fn root_viewport(&self, reader: impl FnOnce(&Viewport) -> R) -> Option { let state = self.state.borrow(); state.viewports.get(&ViewportId::ROOT).map(reader) } pub fn root_window(&self) -> Option> { self.root_viewport(|viewport| viewport.window.clone()) .flatten() } pub fn all_window_ids(&self) -> Vec { let state = self.state.borrow(); state .viewports .values() .filter_map(|viewport| viewport.window.as_ref().map(|w| w.id())) .collect() } pub fn window(&self, window_id: WindowId) -> Option> { let state = self.state.borrow(); state.viewport_from_window.get(&window_id).and_then(|id| { state .viewports .get(id) .and_then(|viewport| viewport.window.clone()) }) } pub fn window_size(&self, cfg: &Config) -> Vec2 { self.window_size_for_scale(cfg, cfg.renderer.scale) } pub fn window_size_for_scale(&self, cfg: &Config, scale: f32) -> Vec2 { let gui = self.gui.borrow(); let aspect_ratio = gui.aspect_ratio(cfg); let mut window_size = cfg.window_size_for_scale(aspect_ratio, scale); window_size.y += gui.menu_height; window_size } pub fn find_max_scale_for_width(&self, width: f32, cfg: &Config) -> f32 { let mut scale = cfg.renderer.scale; let mut size = self.window_size_for_scale(cfg, scale); while scale > 1.0 && size.x > width { scale -= 1.0; size = self.window_size_for_scale(cfg, scale); } scale } pub fn all_viewports_occluded(&self) -> bool { let state = self.state.borrow(); state.viewports.values().all(|viewport| viewport.occluded) } pub fn inner_size(&self) -> Option> { self.root_window().map(|win| win.inner_size()) } pub fn fullscreen(&self) -> bool { self.root_window() .map(|win| win.fullscreen().is_some()) .unwrap_or(false) } pub fn set_fullscreen(&mut self, fullscreen: bool, embed_viewports: bool) { if feature!(OsViewports) { self.ctx.set_embed_viewports(fullscreen || embed_viewports); } self.ctx .send_viewport_cmd_to(ViewportId::ROOT, ViewportCommand::Focus); self.ctx .send_viewport_cmd_to(ViewportId::ROOT, ViewportCommand::Fullscreen(fullscreen)); } pub fn set_embed_viewports(&mut self, embed: bool) { self.ctx.set_embed_viewports(embed); } pub fn set_always_on_top(&mut self, always_on_top: bool) { let state = self.state.borrow(); for viewport_id in state.viewports.keys() { self.ctx.send_viewport_cmd_to( *viewport_id, ViewportCommand::WindowLevel(if always_on_top { WindowLevel::AlwaysOnTop } else { WindowLevel::Normal }), ); } } fn initialize_all_windows(&mut self, event_loop: &ActiveEventLoop) { if self.ctx.embed_viewports() { return; } let State { viewports, viewport_from_window, .. } = &mut *self.state.borrow_mut(); for viewport in viewports.values_mut() { viewport.initialize_window( self.tx.clone(), event_loop, &self.ctx, viewport_from_window, &self.painter, ); } } pub fn rom_loaded(&self) -> bool { self.gui.borrow().loaded_rom.is_some() } pub fn add_message(&mut self, ty: MessageType, text: S) where S: Into, { self.gui.borrow_mut().add_message(ty, text); self.ctx.request_repaint(); } pub fn on_error(&mut self, err: anyhow::Error) { error!("error: {err:?}"); self.tx .event(EmulationEvent::RunState(RunState::AutoPaused)); self.gui.borrow_mut().error = Some(err.to_string()); } pub fn load(ctx: &egui::Context, cfg: &Config) -> anyhow::Result<()> { let path = Config::default_config_dir().join("gui.dat"); if fs::exists(&path) { let data = fs::load_raw(path).context("failed to load gui memory")?; let config = bincode::config::legacy(); let (memory, _) = bincode::serde::decode_from_slice(&data, config) .context("failed to deserialize gui memory")?; ctx.memory_mut(|mem| { *mem = memory; }); info!("Loaded UI state"); } ctx.memory_mut(|mem| { mem.options.zoom_factor = cfg.renderer.zoom; }); Ok(()) } pub fn auto_save(&mut self, cfg: &Config) -> anyhow::Result<()> { let time_since_last_save = Instant::now() - self.last_save_time; if time_since_last_save > Duration::from_secs(10) { self.save(cfg)?; } Ok(()) } pub fn save(&mut self, cfg: &Config) -> anyhow::Result<()> { cfg.save()?; let path = Config::default_config_dir().join("gui.dat"); self.ctx.memory(|mem| { let config = bincode::config::legacy(); let data = bincode::serde::encode_to_vec(mem, config) .context("failed to serialize gui memory")?; fs::save_raw(path, &data).context("failed to save gui memory") })?; self.last_save_time = Instant::now(); Ok(()) } /// Request renderer resources (creating gui context, window, painter, etc). /// /// # Errors /// /// Returns an error if any resources can't be created correctly or `init_running` has already /// been called. pub fn request_resources( event_loop: &ActiveEventLoop, tx: &NesEventProxy, cfg: &Config, ) -> anyhow::Result<(egui::Context, Arc, Receiver)> { let ctx = egui::Context::default(); let window_size = cfg.window_size(cfg.deck.region.aspect_ratio()); let mut builder = egui::ViewportBuilder::default() .with_title(Config::WINDOW_TITLE) .with_visible(false) // hide until first frame is rendered. required by AccessKit .with_fullscreen(cfg.renderer.fullscreen) .with_active(true) .with_resizable(true) .with_inner_size(window_size) .with_min_inner_size(Vec2::new(ppu::size::WIDTH as f32, ppu::size::HEIGHT as f32)); if cfg.renderer.always_on_top { builder = builder.with_always_on_top(); } let window = Arc::new(Self::create_window(&ctx, event_loop, builder)?); window.set_theme(Some(if cfg.renderer.dark_theme { Theme::Dark } else { Theme::Light })); let (painter_tx, painter_rx) = channel::bounded(1); thread::spawn({ let window = Arc::clone(&window); let event_tx = tx.clone(); async move { debug!("creating painter..."); match Self::create_painter(window).await { Ok(painter) => { painter_tx.send(painter).expect("failed to send painter"); event_tx.event(RendererEvent::ResourcesReady); } Err(err) => { error!("failed to create painter: {err:?}"); event_tx.event(UiEvent::Terminate); } } } }); Ok((ctx, window, painter_rx)) } pub fn create_window( ctx: &egui::Context, event_loop: &ActiveEventLoop, builder: ViewportBuilder, ) -> anyhow::Result { let native_pixels_per_point = event_loop .primary_monitor() .or_else(|| event_loop.available_monitors().next()) .map_or_else( || { tracing::debug!( "Failed to find a monitor - assuming native_pixels_per_point of 1.0" ); 1.0 }, |m| m.scale_factor() as f32, ); let zoom_factor = ctx.zoom_factor(); let pixels_per_point = zoom_factor * native_pixels_per_point; let ViewportBuilder { title, position, inner_size, min_inner_size, max_inner_size, fullscreen, maximized, resizable, icon, active, visible, window_level, .. } = builder; let title = title.unwrap_or_else(|| Config::WINDOW_TITLE.to_owned()); let mut window_attrs = Window::default_attributes() .with_title(title.clone()) .with_resizable(resizable.unwrap_or(true)) .with_visible(visible.unwrap_or(true)) .with_maximized(maximized.unwrap_or(false)) .with_window_level(match window_level.unwrap_or_default() { WindowLevel::AlwaysOnBottom => winit::window::WindowLevel::AlwaysOnBottom, WindowLevel::AlwaysOnTop => winit::window::WindowLevel::AlwaysOnTop, WindowLevel::Normal => winit::window::WindowLevel::Normal, }) .with_fullscreen( fullscreen.and_then(|e| e.then_some(winit::window::Fullscreen::Borderless(None))), ) .with_active(active.unwrap_or(true)) .with_platform(&title); if let Some(size) = inner_size { window_attrs = window_attrs.with_inner_size(PhysicalSize::new( pixels_per_point * size.x, pixels_per_point * size.y, )); } if let Some(size) = min_inner_size { window_attrs = window_attrs.with_min_inner_size(PhysicalSize::new( pixels_per_point * size.x, pixels_per_point * size.y, )); } if let Some(size) = max_inner_size { window_attrs = window_attrs.with_max_inner_size(PhysicalSize::new( pixels_per_point * size.x, pixels_per_point * size.y, )); } if let Some(pos) = position { window_attrs = window_attrs.with_position(PhysicalPosition::new( pixels_per_point * pos.x, pixels_per_point * pos.y, )); } if let Some(icon) = icon { let winit_icon = gui::lib::to_winit_icon(&icon); window_attrs = window_attrs.with_window_icon(winit_icon); } let window = event_loop.create_window(window_attrs)?; if let Some(size) = inner_size && window .request_inner_size(PhysicalSize::new( pixels_per_point * size.x, pixels_per_point * size.y, )) .is_some() { debug!("Failed to set window size"); } if let Some(size) = min_inner_size { window.set_min_inner_size(Some(PhysicalSize::new( pixels_per_point * size.x, pixels_per_point * size.y, ))); } debug!("created new window: {:?}", window.id()); Ok(window) } pub async fn create_painter(window: Arc) -> anyhow::Result { // The window must be ready with a non-zero size before `Painter::set_window` is called, // otherwise the wgpu surface won't be configured correctly. let start = Instant::now(); loop { let size = window.inner_size(); if size.width > 0 && size.height > 0 { break; } thread::sleep(Duration::from_millis(10)).await; } debug!( "waited {:.02}s for window creation", start.elapsed().as_secs_f32() ); let mut painter = Painter::new(); painter .set_window(ViewportId::ROOT, Some(Arc::clone(&window))) .await?; Ok(painter) } pub fn recreate_window(&mut self, event_loop: &ActiveEventLoop) { if self.ctx.embed_viewports() { return; } let State { viewports, viewport_from_window, .. } = &mut *self.state.borrow_mut(); let builder = viewports .get(&ViewportId::ROOT) .map(|viewport| viewport.builder.clone()) .unwrap_or_default(); let viewport = Self::create_or_update_viewport( &self.ctx, viewports, ViewportIdPair::ROOT, ViewportClass::Root, builder, None, ); viewport.initialize_window( self.tx.clone(), event_loop, &self.ctx, viewport_from_window, &self.painter, ); } pub fn drop_window(&mut self) -> anyhow::Result<()> { if self.ctx.embed_viewports() { return Ok(()); } let mut state = self.state.borrow_mut(); state.viewports.remove(&ViewportId::ROOT); Renderer::set_painter_window( self.tx.clone(), Rc::clone(&self.painter), ViewportId::ROOT, None, ); Ok(()) } fn set_painter_window( tx: NesEventProxy, painter: Rc>, viewport_id: ViewportId, window: Option>, ) { // This is fine because we won't be yielding. Native platforms call `block_on` and // wasm is single-threaded with `spawn_local` and runs on the next microtick. #[allow(clippy::await_holding_refcell_ref)] thread::spawn(async move { if let Err(err) = painter.borrow_mut().set_window(viewport_id, window).await { error!("failed to set painter window on viewport id {viewport_id:?}: {err:?}"); tx.event(NesEvent::Ui(UiEvent::Terminate)); } }); } fn create_or_update_viewport<'a>( ctx: &egui::Context, viewports: &'a mut ViewportIdMap, ids: ViewportIdPair, class: ViewportClass, mut builder: ViewportBuilder, viewport_ui_cb: Option>, ) -> &'a mut Viewport { if builder.icon.is_none() { builder.icon = viewports .get_mut(&ids.parent) .and_then(|viewport| viewport.builder.icon.clone()); } match viewports.entry(ids.this) { Entry::Vacant(entry) => entry.insert(Viewport { ids, class, builder, viewport_ui_cb, ..Default::default() }), Entry::Occupied(mut entry) => { let viewport = entry.get_mut(); viewport.class = class; viewport.ids.parent = ids.parent; viewport.info.parent = Some(ids.parent); viewport.viewport_ui_cb = viewport_ui_cb; let (delta_commands, recreate) = viewport.builder.patch(builder); if recreate { viewport.window = None; viewport.raw_input = Default::default(); viewport.cursor_icon = None; } else if let Some(window) = &viewport.window { Self::process_viewport_commands( ctx, &mut viewport.info, delta_commands, window, ); } entry.into_mut() } } } pub fn handle_platform_output(viewport: &mut Viewport, platform_output: egui::PlatformOutput) { let egui::PlatformOutput { cursor_icon, commands, .. } = platform_output; viewport.set_cursor(cursor_icon); for command in commands { match command { OutputCommand::OpenUrl(open_url) => Self::open_url_in_browser(&open_url.url), OutputCommand::CopyText(copied_text) => { if !copied_text.is_empty() { viewport.clipboard.set(copied_text); } } OutputCommand::CopyImage(_) => (), } } } fn open_url_in_browser(url: &str) { if let Err(err) = webbrowser::open(url) { tracing::warn!("failed to open url: {err:?}"); } } fn handle_viewport_output( ctx: &egui::Context, viewports: &mut ViewportIdMap, outputs: BTreeMap, ) { for (id, output) in outputs { let ids = ViewportIdPair::from_self_and_parent(id, output.parent); let viewport = Self::create_or_update_viewport( ctx, viewports, ids, output.class, output.builder, output.viewport_ui_cb, ); if let Some(window) = viewport.window.as_ref() { Self::process_viewport_commands(ctx, &mut viewport.info, output.commands, window); } } } fn process_viewport_commands( ctx: &egui::Context, info: &mut ViewportInfo, commands: impl IntoIterator, window: &Window, ) { let pixels_per_point = gui::lib::pixels_per_point(ctx, window); for command in commands { match command { ViewportCommand::Close => { info.events.push(egui::ViewportEvent::Close); } ViewportCommand::StartDrag => { // If `.has_focus()` is not checked on x11 the input will be permanently taken until the app is killed! if window.has_focus() && let Err(err) = window.drag_window() { tracing::warn!("{command:?}: {err}"); } } ViewportCommand::InnerSize(size) => { let width_px = pixels_per_point * size.x.max(1.0); let height_px = pixels_per_point * size.y.max(1.0); let requested_size = PhysicalSize::new(width_px, height_px); if let Some(_returned_inner_size) = window.request_inner_size(requested_size) { // On platforms where the size is entirely controlled by the user the // applied size will be returned immediately, resize event in such case // may not be generated. // e.g. Linux // On platforms where resizing is disallowed by the windowing system, the current // inner size is returned immediately, and the user one is ignored. // e.g. Android, iOS, … // However, comparing the results is prone to numerical errors // because the linux backend converts physical to logical and back again. // So let's just assume it worked: info.inner_rect = gui::lib::inner_rect_in_points(window, pixels_per_point); info.outer_rect = gui::lib::outer_rect_in_points(window, pixels_per_point); } else { // e.g. macOS, Windows // The request went to the display system, // and the actual size will be delivered later with the [`WindowEvent::Resized`]. } } ViewportCommand::BeginResize(direction) => { use egui::viewport::ResizeDirection as EguiResizeDirection; use winit::window::ResizeDirection; if let Err(err) = window.drag_resize_window(match direction { EguiResizeDirection::North => ResizeDirection::North, EguiResizeDirection::South => ResizeDirection::South, EguiResizeDirection::East => ResizeDirection::East, EguiResizeDirection::West => ResizeDirection::West, EguiResizeDirection::NorthEast => ResizeDirection::NorthEast, EguiResizeDirection::SouthEast => ResizeDirection::SouthEast, EguiResizeDirection::NorthWest => ResizeDirection::NorthWest, EguiResizeDirection::SouthWest => ResizeDirection::SouthWest, }) { tracing::warn!("{command:?}: {err}"); } } ViewportCommand::Title(title) => { window.set_title(&title); } ViewportCommand::Transparent(v) => window.set_transparent(v), ViewportCommand::Visible(v) => window.set_visible(v), ViewportCommand::OuterPosition(pos) => { window.set_outer_position(PhysicalPosition::new( pixels_per_point * pos.x, pixels_per_point * pos.y, )); } ViewportCommand::MinInnerSize(s) => { window.set_min_inner_size((s.is_finite() && s != Vec2::ZERO).then_some( PhysicalSize::new(pixels_per_point * s.x, pixels_per_point * s.y), )); } ViewportCommand::MaxInnerSize(s) => { window.set_max_inner_size((s.is_finite() && s != Vec2::INFINITY).then_some( PhysicalSize::new(pixels_per_point * s.x, pixels_per_point * s.y), )); } ViewportCommand::ResizeIncrements(s) => { window.set_resize_increments(s.map(|s| { PhysicalSize::new(pixels_per_point * s.x, pixels_per_point * s.y) })); } ViewportCommand::Resizable(v) => window.set_resizable(v), ViewportCommand::EnableButtons { close, minimized, maximize, } => window.set_enabled_buttons( if close { WindowButtons::CLOSE } else { WindowButtons::empty() } | if minimized { WindowButtons::MINIMIZE } else { WindowButtons::empty() } | if maximize { WindowButtons::MAXIMIZE } else { WindowButtons::empty() }, ), ViewportCommand::Minimized(v) => { window.set_minimized(v); info.minimized = Some(v); } ViewportCommand::Maximized(v) => { window.set_maximized(v); info.maximized = Some(v); } ViewportCommand::Fullscreen(v) => { window.set_fullscreen(v.then_some(winit::window::Fullscreen::Borderless(None))); info.fullscreen = Some(v); } ViewportCommand::Decorations(v) => window.set_decorations(v), ViewportCommand::WindowLevel(l) => { use egui::viewport::WindowLevel as EguiWindowLevel; use winit::window::WindowLevel; window.set_window_level(match l { EguiWindowLevel::AlwaysOnBottom => WindowLevel::AlwaysOnBottom, EguiWindowLevel::AlwaysOnTop => WindowLevel::AlwaysOnTop, EguiWindowLevel::Normal => WindowLevel::Normal, }); } ViewportCommand::Icon(icon) => { let winit_icon = icon.and_then(|icon| gui::lib::to_winit_icon(&icon)); window.set_window_icon(winit_icon); } ViewportCommand::IMERect(rect) => { window.set_ime_cursor_area( PhysicalPosition::new( pixels_per_point * rect.min.x, pixels_per_point * rect.min.y, ), PhysicalSize::new( pixels_per_point * rect.size().x, pixels_per_point * rect.size().y, ), ); } ViewportCommand::IMEAllowed(v) => window.set_ime_allowed(v), ViewportCommand::IMEPurpose(p) => window.set_ime_purpose(match p { egui::viewport::IMEPurpose::Password => winit::window::ImePurpose::Password, egui::viewport::IMEPurpose::Terminal => winit::window::ImePurpose::Terminal, egui::viewport::IMEPurpose::Normal => winit::window::ImePurpose::Normal, }), ViewportCommand::Focus if !window.has_focus() => { window.focus_window(); } ViewportCommand::RequestUserAttention(a) => { window.request_user_attention(match a { egui::UserAttentionType::Reset => None, egui::UserAttentionType::Critical => { Some(winit::window::UserAttentionType::Critical) } egui::UserAttentionType::Informational => { Some(winit::window::UserAttentionType::Informational) } }); } ViewportCommand::SetTheme(t) => window.set_theme(match t { egui::SystemTheme::Light => Some(winit::window::Theme::Light), egui::SystemTheme::Dark => Some(winit::window::Theme::Dark), egui::SystemTheme::SystemDefault => None, }), ViewportCommand::ContentProtected(v) => window.set_content_protected(v), ViewportCommand::CursorPosition(pos) => { if let Err(err) = window.set_cursor_position(PhysicalPosition::new( pixels_per_point * pos.x, pixels_per_point * pos.y, )) { tracing::warn!("{command:?}: {err}"); } } ViewportCommand::CursorGrab(o) => { if let Err(err) = window.set_cursor_grab(match o { egui::viewport::CursorGrab::None => CursorGrabMode::None, egui::viewport::CursorGrab::Confined => CursorGrabMode::Confined, egui::viewport::CursorGrab::Locked => CursorGrabMode::Locked, }) { tracing::warn!("{command:?}: {err}"); } } ViewportCommand::CursorVisible(v) => window.set_cursor_visible(v), ViewportCommand::MousePassthrough(passthrough) => { if let Err(err) = window.set_cursor_hittest(!passthrough) { tracing::warn!("{command:?}: {err}"); } } _ => (), } } } /// Request redraw. pub fn redraw( &mut self, window_id: WindowId, event_loop: &ActiveEventLoop, gamepads: &mut Gamepads, cfg: &mut Config, ) -> anyhow::Result<()> { if self.first_frame { self.initialize()?; self.resize_window(cfg); } self.initialize_all_windows(event_loop); if self.all_viewports_occluded() { return Ok(()); } let Some(viewport_id) = self.viewport_id_for_window(window_id) else { return Ok(()); }; self.handle_resize(viewport_id, cfg); let (viewport_ui_cb, viewport_info, raw_input) = { let State { viewports, start_time, .. } = &mut *self.state.borrow_mut(); let Some(viewport) = viewports.get_mut(&viewport_id) else { return Ok(()); }; let Some(window) = &viewport.window else { return Ok(()); }; // Always render the root viewport unless all viewports are occluded to ensure deferred // viewports correctly get Config and Gamepads updates. if viewport.occluded && viewport_id != ViewportId::ROOT { return Ok(()); } Viewport::update_info(&mut viewport.info, &self.ctx, window); let viewport_ui_cb = viewport.viewport_ui_cb.clone(); // On Windows, a minimized window will have 0 width and height. // See: https://github.com/rust-windowing/winit/issues/208 // This solves an issue where egui window positions would be changed when minimizing on Windows. let screen_size_in_pixels = gui::lib::screen_size_in_pixels(window); let screen_size_in_points = screen_size_in_pixels / gui::lib::pixels_per_point(&self.ctx, window); let viewport_info = viewport.info.clone(); let mut raw_input = viewport.raw_input.take(); raw_input.time = Some(start_time.elapsed().as_secs_f64()); raw_input.screen_rect = (screen_size_in_points.x > 0.0 && screen_size_in_points.y > 0.0) .then(|| egui::Rect::from_min_size(egui::Pos2::ZERO, screen_size_in_points)); raw_input.viewport_id = viewport_id; raw_input .viewports .entry(viewport_id) .or_default() .native_pixels_per_point = Some(window.scale_factor() as f32); (viewport_ui_cb, viewport_info, raw_input) }; // Copy NES frame buffer before drawing UI because a UI interaction might cause a texture // resize tied to a configuration change. if viewport_id == ViewportId::ROOT && let Some(render_state) = &self.painter.borrow().render_state() { let mut frame_buffer = self.frame_rx.try_recv_ref(); while self.frame_rx.remaining() < 2 { trace!("skipping frame"); frame_buffer = self.frame_rx.try_recv_ref(); } match frame_buffer { Ok(frame_buffer) => { let gui = self.gui.borrow_mut(); let is_ntsc = gui.loaded_region().unwrap_or(cfg.deck.region).is_ntsc(); gui.nes_texture.update( &render_state.queue, if cfg.renderer.hide_overscan && is_ntsc { &frame_buffer[OVERSCAN_TRIM..frame_buffer.len() - OVERSCAN_TRIM] } else { &frame_buffer }, ); } Err(TryRecvError::Closed) => { error!("frame channel closed unexpectedly, exiting"); event_loop.exit(); return Ok(()); } // Empty frames are fine as we may repaint more often than 60fps due to // UI interactions with keyboard/mouse _ => (), } } // Mutated by accesskit below on platforms that support it #[allow(unused_mut)] let mut output = self.ctx.run_ui(raw_input, |ui| { match &viewport_ui_cb { Some(viewport_ui_cb) => viewport_ui_cb(ui), None => self.gui.borrow_mut().ui(ui, cfg, gamepads), } self.gui .borrow_mut() .show_viewport_info_window(&self.ctx, viewport_id, &viewport_info); }); { let State { viewports, viewport_from_window, .. } = &mut *self.state.borrow_mut(); let Some(viewport) = viewports.get_mut(&viewport_id) else { return Ok(()); }; viewport.info.events.clear(); // they should have been processed let Viewport { window: Some(window), .. } = viewport else { return Ok(()); }; let clipped_primitives = self.ctx.tessellate(output.shapes, output.pixels_per_point); window.pre_present_notify(); self.painter.borrow_mut().paint( viewport_id, output.pixels_per_point, &clipped_primitives, &output.textures_delta, ); if std::mem::take(&mut self.first_frame) { window.set_visible(true); } let active_viewports_ids = output .viewport_output .keys() .copied() .collect::(); if feature!(ScreenReader) && self.ctx.options(|o| o.screen_reader) { platform::speak_text(&output.platform_output.events_description()); } #[cfg(not(target_arch = "wasm32"))] if let Some(update) = output.platform_output.accesskit_update.take() { tracing::trace!("update accesskit: {update:?}"); self.accesskit.update_if_active(|| update); } Self::handle_platform_output(viewport, output.platform_output); Self::handle_viewport_output(&self.ctx, viewports, output.viewport_output); if std::mem::take(&mut self.zoom_changed) { cfg.renderer.zoom = self.ctx.zoom_factor(); } // Prune dead viewports viewports.retain(|id, _| active_viewports_ids.contains(id)); viewport_from_window.retain(|_, id| active_viewports_ids.contains(id)); self.painter .borrow_mut() .retain_surfaces(&active_viewports_ids); } if let Err(err) = self.auto_save(cfg) { error!("failed to auto save UI state: {err:?}"); } Ok(()) } fn handle_resize(&mut self, viewport_id: ViewportId, cfg: &Config) { if viewport_id == ViewportId::ROOT && self.resize_texture { tracing::debug!("resizing window and texture"); self.tx.event(EmulationEvent::RequestFrame); self.resize_window(cfg); if let Some(render_state) = self.painter.borrow_mut().render_state_mut() { let texture_size = cfg.texture_size(); let mut gui = self.gui.borrow_mut(); let aspect_ratio = gui.aspect_ratio(cfg); gui.nes_texture .resize(render_state, texture_size, aspect_ratio); } self.resize_texture = false; } } fn resize_window(&self, cfg: &Config) { if !self.fullscreen() { let desired_window_size = self.window_size(cfg); // On some platforms, e.g. wasm, window width is constrained by the // viewport width, so try to find the max scale that will fit if feature!(ConstrainedViewport) { let res = platform::renderer::constrain_window_to_viewport( self, desired_window_size.x, cfg, ); if res.consumed { return; } } if let Some(window) = self.root_window() { tracing::debug!("resizing window: {desired_window_size:?}"); let _ = window.request_inner_size(LogicalSize::new( desired_window_size.x, desired_window_size.y, )); } } } } impl Viewport { pub fn initialize_window( &mut self, tx: NesEventProxy, event_loop: &ActiveEventLoop, ctx: &egui::Context, viewport_from_window: &mut HashMap, painter: &Rc>, ) { if self.window.is_some() { return; } let viewport_id = self.ids.this; match Renderer::create_window(ctx, event_loop, self.builder.clone()) { Ok(window) => { viewport_from_window.insert(window.id(), viewport_id); let window = Arc::new(window); Renderer::set_painter_window( tx, Rc::clone(painter), viewport_id, Some(Arc::clone(&window)), ); debug!( "created new viewport window: {:?} ({:?})", self.builder.title, window.id() ); self.info.title = self.builder.title.clone(); self.info.minimized = window.is_minimized(); self.info.maximized = Some(window.is_maximized()); self.window = Some(window); } Err(err) => error!("Failed to create window: {err}"), } } pub fn update_info(info: &mut ViewportInfo, ctx: &egui::Context, window: &Window) { let pixels_per_point = gui::lib::pixels_per_point(ctx, window); let has_position = window.is_minimized().is_none_or(|minimized| !minimized); let inner_rect = has_position .then(|| gui::lib::inner_rect_in_points(window, pixels_per_point)) .flatten(); let outer_rect = has_position .then(|| gui::lib::outer_rect_in_points(window, pixels_per_point)) .flatten(); let monitor_size = window.current_monitor().map(|monitor| { let size = monitor.size().to_logical::(pixels_per_point.into()); egui::vec2(size.width, size.height) }); let title = window.title(); if !title.is_empty() { info.title = Some(title); } info.native_pixels_per_point = Some(window.scale_factor() as f32); info.monitor_size = monitor_size; info.inner_rect = inner_rect; info.outer_rect = outer_rect; if !cfg!(target_os = "macos") { // Asking for minimized/maximized state at runtime can lead to a deadlock on macOS info.maximized = Some(window.is_maximized()); info.minimized = Some(window.is_minimized().unwrap_or(false)); } info.fullscreen = Some(window.fullscreen().is_some()); info.focused = Some(window.has_focus()); } fn set_cursor(&mut self, cursor_icon: egui::CursorIcon) { if self.cursor_icon == Some(cursor_icon) { // Prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing. // On other platforms: just early-out to save CPU. return; } let Some(window) = &self.window else { return; }; let is_pointer_in_window = self.cursor_pos.is_some(); if is_pointer_in_window { self.cursor_icon = Some(cursor_icon); if let Some(cursor) = translate_cursor(cursor_icon) { window.set_cursor_visible(true); window.set_cursor(cursor); } else { window.set_cursor_visible(false); } } else { self.cursor_icon = None; } } } ================================================ FILE: tetanes/src/nes/rom.rs ================================================ #[derive(Clone, PartialEq)] pub struct RomData(pub Vec); impl std::fmt::Debug for RomData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "RomData({} bytes)", self.0.len()) } } impl AsRef<[u8]> for RomData { fn as_ref(&self) -> &[u8] { &self.0 } } #[derive(Copy, Clone)] #[must_use] pub struct RomAsset { pub name: &'static str, pub authors: &'static str, pub description: &'static str, pub source: &'static str, pub data_fn: &'static dyn Fn() -> Vec, } impl std::fmt::Debug for RomAsset { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("RomAsset") .field("name", &self.name) .field("authors", &self.authors) .field("description", &self.description) .field("source", &self.source) .finish_non_exhaustive() } } impl RomAsset { pub const fn new( name: &'static str, authors: &'static str, description: &'static str, source: &'static str, data_fn: &'static dyn Fn() -> Vec, ) -> Self { Self { name, authors, description, source, data_fn, } } pub fn data(&self) -> RomData { RomData((self.data_fn)()) } } macro_rules! rom_assets { ($(($name:expr, $filename:expr, $authors:expr, $description:expr, $source:expr$(,)?)),*$(,)?) => {[$( { fn data_fn() -> Vec { include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), "/assets/roms/", $filename )).to_vec() } RomAsset::new( $name, $authors, $description, $source, &data_fn ) }, )*]}; } pub const HOMEBREW_ROMS: [RomAsset; 18] = rom_assets!( ( "Alter Ego", "alter_ego.nes", "Denis Grachev, Shiru, and Kulor", "The game is a logic platformer. You control a hero and his alter ego. You have to switch between them to clear a level. It is a bit similar to Binary Land.", "https://shiru.untergrund.net/software.shtml", ), ( "AO Demo", "ao_demo.nes", "Second Dimension", "If you like puzzle games and you're up for a challenge, AO is the game for you. The objective is to roll the brick around the game board and drop it through the goal pit. It sounds easy, right? You'll have to avoid falling off of the edge while maneuvering around tight areas. Do you think you can make it through all the levels before your score reaches zero? If you run into a wall and get stuck, you won't lose when the timer runs out, so you can still finish all of the levels. AO features 30 challenging puzzles for 1 or 2 players.", "https://www.second-dimension.com/catalog/ao", ), ( "Assimilate", "assimilate.nes", "Nessylum Games", "Ever wanted to anal probe someone? Well the wait is over! Assimilate is the game you and your dopey little friends have been waiting for. Join the super-ship Ossan for twenty plus levels of zombifying, brainwashing, human-dominating excitement. Will you succeed in conquering the entire human race? Or will you cry yourself to sleep after watching Ossan explode in a fiery ball of humiliating destruction? What happens is up to you. Begin assimilation.", "https://forums.nesdev.org/viewtopic.php?t=7087&hilit=assimilate" ), ( "Blade Buster", "blade_buster.nes", "High Level Challenge", "You have 2 minutes in a caravan format to compete the score at 5 minutes. It has become a game with a sense of exhilaration that has good old shoot before being shot playing style.", "http://hlc6502.web.fc2.com/Bbuster.htm", ), ( "Cheril the Goddess", "cheril_the_goddess.nes", "The Mojon Twins", "You control Cheril. There’s plenty of things to do, and you’ll need special objects. Cheril can only carry ONE object at a time, so if you try to get a new one while you are carring an object, you will drop the one you carry in the place of the one you take. To get/interchange objects press DOWN. Besides, you’ll have to give those objects a use. To use an object, just walk to the place you want to use it and press DOWN. For Cheril to fly, she has to «push». You can make her «push» by pressing UP. But beware: «pushing» for too long drains Cheril’s vitality, so you have to do this carefully. Timing short «pushes» can make her gain momentum. Take your time to master the technique, it won’t take long. Cheril can also fire power balls. You can fire pressing FIRE. Power balls also drain Cheril, but think that enemies do quite a lot of harm. Use this feature wisely, and just when needed! Cheril can also regain her vitality by means of a special action we won’t reveal ‘cause it’s easy enough to find out! You can choose the difficulty level, but the easiest level won’t show the real ending.", "https://forums.nesdev.org/viewtopic.php?t=15367", ), ( "Data Man Demo", "data_man_demo.nes", "Darkbits (Olof Naessen, Per Larsson, Ted Steen)", "Do you have what it takes to save the System? The Master and his evil minions have invaded the system and will not stop until every piece of data is corrupted. It’s up to you to save it before time runs out and the system crashes! It won’t be easy. You’ll have to protect the Central Processing Unit from hordes of attacking minions and ultimately from the Master himself. Face it alone, or with a friend!", "https://datamangame.com/", ), ( "Dushlan", "dushlan.nes", "Peter McQuillan", "The game itself is based on the classic Tetris, but with a few twists on the game and some extra features that are not commonly available like ghost (where you can see where your piece would go if you dropped it) and save (where you can swap a piece in play for later usage).", "https://github.com/soiaf/Dushlan", ), ( "From Below", "from_below.nes", "Matt Hughson", "FROM BELOW is a falling block puzzle game featuring: Soft Drops Hard Drops Wall Kicks T-Spins Lock Delay 3 modes of play: Kraken Battle Mode The signature mode of FROM BELOW. Battle the Kraken by clear lines across the onslaught of attacking Kraken Tentacles. The Tentacles push more blocks onto the screen every few seconds, forcing to act quickly, and strategize on an every changing board.", "https://mhughson.itch.io/from-below/devlog/212679/vs-system-beta-0100", ), ( "Lan Master", "lan_master.nes", "Shiru", "Lan Master is a puzzle game for NES, inspired by the game NetWalk. The goal is to connect all of the computers on each level. Rotate the pieces and connect the wires before the timer runs out! There are fifty levels in all, with increasing difficulty. A password system is included so whether you’re playing in an emulator or on a console, you can come back later and pick up where you left off.", "https://shiru.untergrund.net/software.shtml", ), ( "Lawn Mower", "lawn_mower.nes", "Shiru", "The goal of this game is to mow all the grass before you run out of gas. Collect gas cans to keep yourself from running on empty.", "https://shiru.untergrund.net/software.shtml", ), ( "Mad Wizard", "mad_wizard.nes", "Sly Dog Studios", "Take an adventure in the world of Candelabra! The evil summoner Amondus from The Order of the Talon has taken over Prim, Hekl's once happy homeland. And nothing drives a wizard more crazy than having their territory trampled on! Can you help Hekl defeat the enemies that Amondus has populated throughout the landscape? To do so, you will need to master the art of levitation, find magic spells that will assist you in reaching new areas, and upgrade your weapons. All of these will be necessary in order to give Hekl the power he needs to restore peace to Prim. Do you have what it takes? If you dare, venture into this, the first installment of the Candelabra series!", "The goal of this game is to mow all the grass before you run out of gas. Collect gas cans to keep yourself from running on empty.", ), ( "Micro Knight", "micro_knight.nes", "SDM", "", "https://forums.nesdev.org/viewtopic.php?t=13450", ), ( "Nebs 'n Debs Demo", "nebs_n_debs_demo.nes", "Dullahan Software", "Run, jump, and dash your way through 12 levels as you search for the missing parts of Debs's ship to escape the hostile alien planet Vespasian 7MV! Nebs 'n Debs runs on the same type of game cartridge as the original Super Mario Bros.", "https://dullahan-software.itch.io/nebs-n-debs", ), ( "Owlia", "owlia.nes", "Gradual Games", "The Legends of Owlia is Gradual Games' second release for the NES. It is an action-adventure game inspired by StarTropics, Crystalis, and the Legend of Zelda. ", "https://www.infiniteneslives.com/owlia.php", ), ( "Streemerz", "streemerz.nes", "Mr. Podunkian & Faux Game Co.", r#""Try climbing to the top of this one by throwing streamers and climbing them. On your way up you better watch out for the various pie throwing clowns, burning candles and bouncing balls, because if they get you, you'll die a little each time." These were the orders given to you, Operative JOE when you were ordered to infiltrate the evil MASTER Y's floating fortress to destroy the TIGER ARMY's top secret weapon."#, "https://www.fauxgame.com/", ), ( "Super Painter", "super_painter.nes", "RetroSouls & Kulor", "Trapped in a colorless world, armed with a paintbrush - there’s only one thing to do! As Super Painter, you’ll have to fill in all the missing color from the walls and ledges of 25 charming stages. Watch out for enemies and pits to the bottom, and don’t box yourself in - when you’re done painting, you’ll have to race to the magic door to the next level. It’s platform puzzling at its finest!", "https://www.retrosouls.net/?page_id=901", ), ( "Tiger Jenny", "tiger_jenny.nes", "Ludosity", "Tiger Jenny by Ludosity is a NES game set in the same universe as “Ittle Dew” it takes place a thousand years before the events of that game. Battle your way through the forests to seek vengeance on the Turnip Witch who dwells in her castle.", "https://pdroms.de/files/nintendo-nintendoentertainmentsystem-nes-famicom-fc/tiger-jenny", ), ( "Yun", "yun.nes", "The Mojon Twins", "The main goal is helping yun capturing every single being to fill the pantry of her restaurant. The Big Marsh near Lake Potoña (province of Badajoz), formed by three areas (the marsh wood, the marsh abandoned factory and the mash desert) is full of walking flesh Yun must capture. To capture her enemies, Yun must stun them by means of hitting them with a bubble. Once they are stunned, they can be captured just touching them. Yun’s bubbles are quite resistant. You can hump on them and let them carry you upwards, which is sometimes the only way to progress in the level. Besides, there’s some points where Yun might need a key to keep going.", "https://www.mojontwins.com/juegos_mojonos/yun/", ), ); ================================================ FILE: tetanes/src/nes/version.rs ================================================ use std::cell::RefCell; #[cfg(not(target_arch = "wasm32"))] mod fetcher { use reqwest::blocking::Client; use std::cell::Cell; use std::time::{Duration, Instant}; #[derive(Debug, Clone)] #[must_use] pub struct Fetcher { client: Option, rate_limit: Duration, last_request_time: Cell, } impl Default for Fetcher { fn default() -> Self { Self { client: Self::create_client(), rate_limit: Duration::from_secs(1), last_request_time: Cell::new(Instant::now()), } } } impl Fetcher { fn create_client() -> Option { use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT}; let mut headers = HeaderMap::new(); headers.insert( USER_AGENT, HeaderValue::from_str("tetanes (me@lukeworks.tech)").ok()?, ); reqwest::blocking::Client::builder() .default_headers(headers) .build() .ok() } pub fn update_available(&self, version: &'static str) -> anyhow::Result> { #[derive(Debug, serde::Deserialize)] #[must_use] struct ApiError { detail: Option, } #[derive(Debug, serde::Deserialize)] #[must_use] struct ApiErrors { errors: Vec, } // Partial deserialization of the full response #[derive(Debug, serde::Deserialize)] #[must_use] struct Crate { newest_version: String, } // Partial deserialization of the full response #[derive(Debug, serde::Deserialize)] #[must_use] struct CrateResponse { #[serde(rename = "crate")] cr: Crate, } if self.last_request_time.get().elapsed() < self.rate_limit { std::thread::sleep( (self.last_request_time.get() + self.rate_limit) - Instant::now(), ); } self.last_request_time.set(Instant::now()); let Some(client) = &self.client else { anyhow::bail!("failed to create http client"); }; let content = client .get("https://crates.io/api/v1/crates/tetanes") .send() .and_then(|res| res.text())?; if let Ok(res) = serde_json::from_str::(&content) { anyhow::bail!( "encountered crates.io API errors: {}", res.errors .into_iter() .filter_map(|error| error.detail) .collect::>() .join(",") ); } match serde_json::from_str::(&content) { Ok(CrateResponse { cr: Crate { newest_version, .. }, }) => { if Self::is_newer(&newest_version, version) { Ok(Some(newest_version)) } else { Ok(None) } } Err(err) => anyhow::bail!("failed to deserialize crates.io response: {err:?}"), } } fn is_newer(new: &str, old: &str) -> bool { match (semver::Version::parse(old), semver::Version::parse(new)) { (Ok(old), Ok(new)) => new > old, _ => false, } } } } #[derive(Debug, Clone)] #[must_use] pub struct Version { current: &'static str, latest: RefCell, } impl Default for Version { fn default() -> Self { Self::new() } } impl Version { pub fn new() -> Self { Self { current: env!("CARGO_PKG_VERSION"), latest: RefCell::new(env!("CARGO_PKG_VERSION").to_string()), } } pub const fn current(&self) -> &str { self.current } pub fn latest(&self) -> String { self.latest.borrow().clone() } pub fn set_latest(&mut self, version: String) { self.latest.replace(version); } pub const fn requires_updates(&self) -> bool { cfg!(not(target_arch = "wasm32")) } #[cfg(target_arch = "wasm32")] pub const fn check_for_updates( &mut self, _tx: &crate::nes::event::NesEventProxy, _notify_latest: bool, ) { } #[cfg(not(target_arch = "wasm32"))] pub fn check_for_updates( &mut self, tx: &crate::nes::event::NesEventProxy, notify_latest: bool, ) { use crate::nes::{ event::{ConfigEvent, UiEvent}, renderer::gui::MessageType, }; let spawn_update = std::thread::Builder::new() .name("check_updates".into()) .spawn({ let current_version = self.current; let fetcher = fetcher::Fetcher::default(); let tx = tx.clone(); move || { let newest_version = fetcher.update_available(current_version); match newest_version { Ok(Some(version)) => tx.event(UiEvent::UpdateAvailable(version)), Ok(None) => { if notify_latest { tx.event(UiEvent::Message(( MessageType::Info, format!("TetaNES v{current_version} is up to date!"), ))); } } Err(err) => { tx.event(UiEvent::Message((MessageType::Error, err.to_string()))); } } if notify_latest { tx.event(ConfigEvent::ShowUpdates(true)); } } }); if let Err(err) = spawn_update { tx.event(UiEvent::Message(( MessageType::Error, format!("Failed to check for updates: {err}"), ))); } } pub fn install_update_and_restart(&mut self) -> anyhow::Result<()> { // TODO: Implement install/restart for each platform anyhow::bail!("not yet implemented"); } } ================================================ FILE: tetanes/src/nes.rs ================================================ //! User Interface representing the the NES Control Deck use crate::{ nes::{ emulation::Emulation, event::{NesEvent, NesEventProxy}, input::{Gamepads, InputBindings}, renderer::{FrameRecycle, Renderer, Resources, painter::Painter}, }, platform::Initialize, }; use anyhow::Context; use cfg_if::cfg_if; use config::Config; use crossbeam::channel::Receiver; use egui::ahash::HashMap; use std::sync::{ Arc, atomic::{AtomicBool, Ordering}, }; use tetanes_core::{time::Instant, video::Frame}; use thingbuf::mpsc::blocking; use winit::{ event::Modifiers, event_loop::{ActiveEventLoop, EventLoop}, window::{Window, WindowId}, }; pub mod action; pub mod audio; pub mod config; pub mod emulation; pub mod event; pub mod input; pub mod renderer; pub mod rom; pub mod version; /// Represents all the NES Emulation state. #[derive(Debug)] #[must_use] pub struct Nes { /// Set during initialization, then taken and set to `None` when running because /// `EventLoopProxy` can only be created on the initial `EventLoop` and not on /// `&EventLoopWindowTarget`. pub(crate) init_state: Option<(Config, NesEventProxy)>, /// Initially `Suspended`. `Pending` after `Resume` event received and spanwed. `Running` after /// resources future completes. pub(crate) state: State, } #[derive(Debug)] #[must_use] pub(crate) enum State { Suspended { should_terminate: Arc, }, Pending { ctx: egui::Context, window: Arc, painter_rx: Receiver, should_terminate: Arc, }, Running(Box), Exiting, } impl Default for State { fn default() -> Self { Self::Suspended { should_terminate: Default::default(), } } } impl State { pub const fn is_exiting(&self) -> bool { matches!(self, Self::Exiting) } } #[derive(Debug, Copy, Clone, PartialEq)] #[must_use] pub enum RunState { Running, ManuallyPaused, AutoPaused, } impl RunState { pub const fn paused(&self) -> bool { matches!(self, Self::ManuallyPaused | Self::AutoPaused) } pub const fn auto_paused(&self) -> bool { matches!(self, Self::AutoPaused) } pub const fn manually_paused(&self) -> bool { matches!(self, Self::ManuallyPaused) } } /// Represents the NES running state. #[derive(Debug)] pub(crate) struct Running { pub(crate) cfg: Config, // Only used by wasm currently #[cfg_attr(target_arch = "wasm32", allow(unused))] pub(crate) tx: NesEventProxy, pub(crate) should_terminate: Arc, pub(crate) emulation: Emulation, pub(crate) renderer: Renderer, pub(crate) input_bindings: InputBindings, pub(crate) gamepads: Gamepads, pub(crate) modifiers: Modifiers, pub(crate) replay_recording: bool, pub(crate) audio_recording: bool, pub(crate) rewinding: bool, pub(crate) occluded: bool, pub(crate) repaint_times: HashMap, } impl Nes { /// Runs the NES application by starting the event loop. /// /// # Errors /// /// If event loop fails to build or run, then an error is returned. pub fn run(cfg: Config) -> anyhow::Result<()> { // Set up window, events and NES state let event_loop = EventLoop::::with_user_event().build()?; let nes = Nes::new(cfg, &event_loop); cfg_if! { if #[cfg(target_arch = "wasm32")] { use winit::platform::web::EventLoopExtWebSys; event_loop.spawn_app(nes); } else { let mut nes = nes; event_loop.run_app(&mut nes)?; } } Ok(()) } /// Return whether the application should terminate. pub fn should_terminate(&self) -> bool { match &self.state { State::Suspended { should_terminate } | State::Pending { should_terminate, .. } => should_terminate.load(Ordering::Relaxed), State::Running(running) => running.should_terminate.load(Ordering::Relaxed), State::Exiting => true, } } /// Create the NES instance. pub fn new(cfg: Config, event_loop: &EventLoop) -> Self { let should_terminate = Arc::new(AtomicBool::new(false)); #[cfg(not(target_arch = "wasm32"))] // Minor issue if this fails, but not enough to terminate the program let _ = ctrlc::set_handler({ let should_terminate = Arc::clone(&should_terminate); move || { should_terminate.store(true, Ordering::Relaxed); } }); Self { init_state: Some((cfg, NesEventProxy::new(event_loop))), state: State::Suspended { should_terminate }, } } /// Request renderer resources (creating gui context, window, painter, etc). /// /// # Errors /// /// Returns an error if any resources can't be created correctly or `init_running` has already /// been called. pub(crate) fn request_renderer_resources( &mut self, event_loop: &ActiveEventLoop, should_terminate: Arc, ) -> anyhow::Result<()> { let (cfg, tx) = self .init_state .as_ref() .context("config unexpectedly already taken")?; let (ctx, window, painter_rx) = Renderer::request_resources(event_loop, tx, cfg)?; self.state = State::Pending { ctx, window, painter_rx, should_terminate, }; Ok(()) } /// Initialize the running state after a window and GPU resources are created. Transitions /// `state` from `Some(PendingGpuResources { .. })` to `Some(Running { .. })`. /// /// # Errors /// /// If GPU resources failed to be requested, the emulation or renderer fails to build, then an /// error is returned. pub(crate) fn init_running(&mut self, event_loop: &ActiveEventLoop) -> anyhow::Result<()> { match std::mem::take(&mut self.state) { State::Pending { ctx, window, painter_rx, should_terminate, } => { let resources = Resources { ctx, window, painter: painter_rx.recv()?, }; let (frame_tx, frame_rx) = blocking::with_recycle::(10, FrameRecycle); let (mut cfg, tx) = self .init_state .take() .context("config unexpectedly already taken")?; let input_bindings = InputBindings::from_input_config(&cfg.input); let gamepads = Gamepads::new(); cfg.input.update_gamepad_assignments(&gamepads); let emulation = Emulation::new(tx.clone(), frame_tx.clone(), &cfg)?; let renderer = Renderer::new(event_loop, tx.clone(), resources, frame_rx, &cfg)?; let mut running = Running { cfg, tx, should_terminate, emulation, renderer, input_bindings, gamepads, modifiers: Modifiers::default(), replay_recording: false, audio_recording: false, rewinding: false, occluded: false, repaint_times: HashMap::default(), }; running.initialize()?; self.state = State::Running(Box::new(running)); Ok(()) } State::Running(running) => { self.state = State::Running(running); Ok(()) } State::Suspended { .. } | State::Exiting => anyhow::bail!("not in pending state"), } } } ================================================ FILE: tetanes/src/opts.rs ================================================ use clap::{Parser, ValueEnum}; use std::path::PathBuf; use tetanes::nes::config::Config; use tetanes_core::genie::GenieCode; #[derive(Debug, Clone)] pub(crate) struct FourPlayer(tetanes_core::input::FourPlayer); impl ValueEnum for FourPlayer { fn value_variants<'a>() -> &'a [Self] { use tetanes_core::input::FourPlayer::*; &[Self(Disabled), Self(FourScore), Self(Satellite)] } fn to_possible_value(&self) -> Option { Some(clap::builder::PossibleValue::new(self.0.as_str())) } } #[derive(Debug, Clone)] pub(crate) struct RamState(tetanes_core::mem::RamState); impl ValueEnum for RamState { fn value_variants<'a>() -> &'a [Self] { use tetanes_core::mem::RamState::*; &[Self(AllZeros), Self(AllOnes), Self(Random)] } fn to_possible_value(&self) -> Option { Some(clap::builder::PossibleValue::new(self.0.as_str())) } } #[derive(Debug, Clone)] pub(crate) struct NesRegion(tetanes_core::common::NesRegion); impl ValueEnum for NesRegion { fn value_variants<'a>() -> &'a [Self] { use tetanes_core::common::NesRegion::*; &[Self(Ntsc), Self(Pal), Self(Dendy)] } fn to_possible_value(&self) -> Option { Some(clap::builder::PossibleValue::new(self.0.as_str())) } } /// `TetaNES` CLI Config Options #[derive(Parser, Debug)] #[command(version, author, about, long_about = None)] #[must_use] pub struct Opts { /// The NES ROM to load or a directory containing `.nes` ROM files. [default: current directory] pub(crate) path: Option, /// Enable rewinding. #[arg(long)] pub(crate) rewind: bool, /// Silence audio. #[arg(short, long)] pub(crate) silent: bool, /// Start fullscreen. #[arg(short, long)] pub(crate) fullscreen: bool, /// Set four player adapter. [default: 'disabled'] #[arg(short = '4', long, value_enum)] pub(crate) four_player: Option, /// Enable zapper gun. #[arg(short, long)] pub(crate) zapper: bool, /// Disable multi-threaded. #[arg(long)] pub(crate) no_threaded: bool, /// Choose power-up RAM state. [default: "all-zeros"] #[arg(short = 'm', long, value_enum)] pub(crate) ram_state: Option, /// Whether to emulate PPU warmup where writes to certain registers are ignored. Can result in /// some games not working correctly. #[arg(short = 'w', long)] pub(crate) emulate_ppu_warmup: bool, /// Choose default NES region. [default: "ntsc"] #[arg(short = 'r', long, value_enum)] pub(crate) region: Option, /// Save slot. [default: 1] #[arg(short = 'i', long)] pub(crate) save_slot: Option, /// Don't load save state on start. #[arg(long)] pub(crate) no_load: bool, /// Don't auto save state or save on exit. #[arg(long)] pub(crate) no_save: bool, #[arg(short = 'x', long)] /// Emulation speed. [default: 1.0] pub(crate) speed: Option, /// Add Game Genie Code(s). e.g. `AATOZE` (Start Super Mario Bros. with 9 lives). #[arg(short, long)] pub(crate) genie_code: Vec, /// Custom Config path. #[arg(long)] pub(crate) config: Option, /// "Default Config" (skip user config and previous save states) #[arg(short, long)] pub(crate) clean: bool, /// Start with debugger open. #[arg(short, long)] pub(crate) debug: bool, } impl Opts { /// Loads a base `Config`, merging with CLI options pub fn load(self) -> anyhow::Result { let mut cfg = if self.clean { Config::default() } else { Config::load(self.config.clone()) }; if let Some(FourPlayer(four_player)) = self.four_player { cfg.deck.four_player = four_player; } cfg.deck.zapper = self.zapper || cfg.deck.zapper; if let Some(RamState(ram_state)) = self.ram_state { cfg.deck.ram_state = ram_state; } cfg.deck.emulate_ppu_warmup = self.emulate_ppu_warmup || cfg.deck.emulate_ppu_warmup; if let Some(NesRegion(region)) = self.region { cfg.deck.region = region; } cfg.deck.genie_codes.reserve(self.genie_code.len()); for genie_code in self.genie_code.into_iter() { cfg.deck.genie_codes.push(GenieCode::new(genie_code)?); } cfg.emulation.auto_load = if self.clean { false } else { !self.no_load && cfg.emulation.auto_load }; cfg.emulation.rewind = self.rewind || cfg.emulation.rewind; cfg.emulation.auto_save = if self.clean { false } else { !self.no_save && cfg.emulation.auto_save }; if let Some(save_slot) = self.save_slot { cfg.emulation.save_slot = save_slot } if let Some(speed) = self.speed { cfg.emulation.speed = speed } cfg.emulation.threaded = !self.no_threaded && cfg.emulation.threaded; cfg.audio.enabled = !self.silent && cfg.audio.enabled; cfg.renderer.roms_path = self .path .or(cfg.renderer.roms_path) .and_then(|path| path.canonicalize().ok()); cfg.renderer.fullscreen = self.fullscreen || cfg.renderer.fullscreen; Ok(cfg) } } ================================================ FILE: tetanes/src/platform.rs ================================================ use crate::sys::platform; use std::path::{Path, PathBuf}; pub use platform::*; /// Trait for any type requiring platform-specific initialization. pub trait Initialize { /// Initialize type. fn initialize(&mut self) -> anyhow::Result<()>; } /// Extension trait for any builder that provides platform-specific behavior. pub trait BuilderExt { /// Sets platform-specific options. fn with_platform(self, title: &str) -> Self; } /// Method for platforms supporting opening a file dialog. pub fn open_file_dialog( title: impl Into, name: impl Into, extensions: &[impl ToString], dir: Option>, ) -> anyhow::Result> { platform::open_file_dialog_impl(title, name, extensions, dir) } /// Speak the given text out loud for platforms that support it. #[allow(clippy::missing_const_for_fn)] pub fn speak_text(text: &str) { platform::speak_text_impl(text); } pub mod renderer { use super::*; use crate::nes::{config::Config, event::Response, renderer::Renderer}; pub fn constrain_window_to_viewport( renderer: &Renderer, desired_window_width: f32, cfg: &Config, ) -> Response { platform::renderer::constrain_window_to_viewport_impl(renderer, desired_window_width, cfg) } } /// Platform-specific feature capabilities. #[derive(Debug, Copy, Clone, PartialEq, Eq)] #[must_use] pub enum Feature { AbortOnExit, Blocking, ConstrainedViewport, ConsumePaste, Filesystem, ScreenReader, Storage, Suspend, OsViewports, } /// Checks if the current platform supports a given feature. #[macro_export] macro_rules! feature { ($feature: tt) => {{ use $crate::platform::Feature::*; match $feature { // Wasm should never be able to exit AbortOnExit => cfg!(target_arch = "wasm32"), Blocking | Filesystem | OsViewports => { cfg!(not(target_arch = "wasm32")) } ConstrainedViewport | ConsumePaste | ScreenReader => { cfg!(target_arch = "wasm32") } Storage => true, Suspend => cfg!(target_os = "android"), } }}; } ================================================ FILE: tetanes/src/sys/info/os.rs ================================================ use crate::sys::{DiskUsage, SystemInfo, SystemStats}; use std::time::{Duration, Instant}; use sysinfo::{ProcessRefreshKind, RefreshKind}; #[derive(Debug)] pub struct System { sys: Option, updated: Instant, } impl Default for System { fn default() -> Self { let sys = if sysinfo::IS_SUPPORTED_SYSTEM { let mut sys = sysinfo::System::new_with_specifics( RefreshKind::nothing().with_processes( ProcessRefreshKind::nothing() .with_cpu() .with_memory() .with_disk_usage(), ), ); sys.refresh_specifics( RefreshKind::nothing().with_processes( ProcessRefreshKind::nothing() .with_cpu() .with_memory() .with_disk_usage(), ), ); Some(sys) } else { None }; Self { sys, updated: Instant::now(), } } } impl SystemInfo for System { fn update(&mut self) { if let Some(sys) = &mut self.sys { // NOTE: refreshing sysinfo is cpu-intensive if done too frequently and skews the // results let update_interval = Duration::from_secs(1); assert!(update_interval > sysinfo::MINIMUM_CPU_UPDATE_INTERVAL); if self.updated.elapsed() >= update_interval { sys.refresh_specifics( sysinfo::RefreshKind::nothing().with_processes( sysinfo::ProcessRefreshKind::nothing() .with_cpu() .with_memory() .with_disk_usage(), ), ); self.updated = Instant::now(); } } } fn stats(&self) -> Option { self.sys .as_ref() .and_then(|sys| sys.process(sysinfo::Pid::from_u32(std::process::id()))) .map(|proc| { let du = proc.disk_usage(); SystemStats { cpu_usage: proc.cpu_usage(), memory: proc.memory(), disk_usage: DiskUsage { read_bytes: du.read_bytes, total_read_bytes: du.total_read_bytes, written_bytes: du.written_bytes, total_written_bytes: du.total_written_bytes, }, } }) } } ================================================ FILE: tetanes/src/sys/info/wasm.rs ================================================ use crate::sys::{SystemInfo, SystemStats}; #[derive(Default, Debug)] pub struct System {} impl SystemInfo for System { fn update(&mut self) {} fn stats(&self) -> Option { None } } ================================================ FILE: tetanes/src/sys/info.rs ================================================ use cfg_if::cfg_if; cfg_if! { if #[cfg(target_arch = "wasm32")] { mod wasm; pub use wasm::*; } else { mod os; pub use os::*; } } ================================================ FILE: tetanes/src/sys/logging/os.rs ================================================ use anyhow::Context; use std::path::PathBuf; use tracing_appender::{ non_blocking::{NonBlockingBuilder, WorkerGuard}, rolling::{RollingFileAppender, Rotation}, }; use tracing_subscriber::{ fmt, layer::SubscriberExt, registry::LookupSpan, util::SubscriberInitExt, }; #[must_use] pub struct Log { _guard: WorkerGuard, } pub fn init_impl(registry: S) -> anyhow::Result<(impl SubscriberInitExt, Log)> where S: SubscriberExt + for<'a> LookupSpan<'a> + Sync + Send, { let file_appender = RollingFileAppender::builder() .rotation(Rotation::DAILY) .max_log_files(3) .filename_prefix("tetanes") .filename_suffix("log") .build( dirs::data_local_dir() .map(|dir| dir.join("tetanes/logs")) .unwrap_or_else(|| PathBuf::from("logs")), ) .context("failed to create log file")?; let (file_writer, guard) = NonBlockingBuilder::default() .buffered_lines_limit(4096) .thread_name("tetanes-logger") .finish(file_appender); let registry = registry .with( fmt::layer() .compact() .with_line_number(true) .with_thread_ids(true) .with_thread_names(true) .with_writer(file_writer), ) .with( fmt::layer() .compact() .with_line_number(true) .with_thread_ids(true) .with_thread_names(true) .with_writer(std::io::stderr), ); Ok((registry, Log { _guard: guard })) } ================================================ FILE: tetanes/src/sys/logging/wasm.rs ================================================ use std::panic; use tracing_subscriber::{ fmt::{self, format::Pretty}, layer::SubscriberExt, registry::LookupSpan, util::SubscriberInitExt, }; use tracing_web::{MakeWebConsoleWriter, performance_layer}; pub struct Log; pub fn init_impl(registry: S) -> anyhow::Result<(impl SubscriberInitExt, Log)> where S: SubscriberExt + for<'a> LookupSpan<'a> + Sync + Send, { panic::set_hook(Box::new(|info: &panic::PanicHookInfo<'_>| { let error_div = web_sys::window() .and_then(|window| window.document()) .and_then(|document| document.get_element_by_id("error")); if let Some(error_div) = error_div && let Err(err) = error_div.class_list().remove_1("hidden") { tracing::error!("{err:?}") } console_error_panic_hook::hook(info); })); let console_layer = fmt::layer() .compact() .with_line_number(true) .with_ansi(false) .without_time() // Not available in wasm .with_writer(MakeWebConsoleWriter::new()); let perf_layer = performance_layer().with_details_from_fields(Pretty::default()); let registry = registry.with(console_layer).with(perf_layer); Ok((registry, Log)) } ================================================ FILE: tetanes/src/sys/logging.rs ================================================ use cfg_if::cfg_if; cfg_if! { if #[cfg(target_arch = "wasm32")] { mod wasm; pub use wasm::*; } else { mod os; pub use os::*; } } ================================================ FILE: tetanes/src/sys/platform/os.rs ================================================ use crate::{ nes::{Running, event::EmulationEvent, renderer::Renderer}, platform::{BuilderExt, Initialize}, }; use std::path::{Path, PathBuf}; use tracing::error; use winit::window::WindowAttributes; /// Method for platforms supporting opening a file dialog. pub fn open_file_dialog_impl( title: impl Into, name: impl Into, extensions: &[impl ToString], dir: Option>, ) -> anyhow::Result> { let mut dialog = rfd::FileDialog::new() .set_title(title) .add_filter(name, extensions); if let Some(dir) = dir { dialog = dialog.set_directory(dir.as_ref()); } Ok(dialog.pick_file()) } /// Speak the given text out loud. pub const fn speak_text_impl(_text: &str) {} impl Initialize for Running { /// Initialize by loading a ROM from the command line, if provided. fn initialize(&mut self) -> anyhow::Result<()> { if let Some(path) = self.cfg.renderer.roms_path.take() { if path.is_file() { if let Some(parent) = path.parent() { self.cfg.renderer.roms_path = Some(parent.to_path_buf()); } self.event(EmulationEvent::LoadRomPath(path)); } else if path.exists() { self.cfg.renderer.roms_path = Some(path); } } Ok(()) } } impl Initialize for Renderer { fn initialize(&mut self) -> anyhow::Result<()> { Ok(()) } } impl BuilderExt for WindowAttributes { /// Sets platform-specific window options. fn with_platform(self, _title: &str) -> Self { use anyhow::Context; use image::{ImageFormat, ImageReader}; use std::io::Cursor; static WINDOW_ICON: &[u8] = include_bytes!("../../../assets/tetanes_icon.png"); let icon = ImageReader::with_format(Cursor::new(WINDOW_ICON), ImageFormat::Png) .decode() .context("failed to decode window icon"); let window_attrs = self.with_window_icon( icon.and_then(|png| { let width = png.width(); let height = png.height(); winit::window::Icon::from_rgba(png.into_rgba8().into_vec(), width, height) .with_context(|| "failed to create window icon") }) .map_err(|err| error!("{err:?}")) .ok(), ); #[cfg(target_os = "linux")] let window_attrs = { use winit::platform::wayland::WindowAttributesExtWayland as _; window_attrs.with_name(_title, "") }; // Ensures that viewport windows open in a separate window instead of a tab, which has // issues with certain preference toggles like fullscreen that effect the root viewport. #[cfg(target_os = "macos")] let window_attrs = { use winit::platform::macos::{OptionAsAlt, WindowAttributesExtMacOS}; window_attrs .with_tabbing_identifier(_title) .with_option_as_alt(OptionAsAlt::Both) }; window_attrs } } pub mod renderer { use super::*; use crate::nes::{config::Config, event::Response}; pub fn constrain_window_to_viewport_impl( _renderer: &Renderer, _desired_window_width: f32, _cfg: &Config, ) -> Response { Response::default() } } ================================================ FILE: tetanes/src/sys/platform/wasm.rs ================================================ // TODO: Remove. See: https://github.com/rustwasm/wasm-bindgen/issues/4283 #![allow(unexpected_cfgs)] use crate::{ nes::{ Running, event::{EmulationEvent, NesEventProxy, RendererEvent, ReplayData, UiEvent}, renderer::{Renderer, State, gui}, rom::RomData, }, platform::{BuilderExt, Initialize}, thread, }; use anyhow::{Context, bail}; use std::{ path::{Path, PathBuf}, rc::Rc, }; use wasm_bindgen::prelude::*; use web_sys::{ FileReader, HtmlAnchorElement, HtmlCanvasElement, HtmlInputElement, js_sys::Uint8Array, }; use winit::{platform::web::WindowAttributesExtWebSys, window::WindowAttributes}; const BIN_NAME: &str = env!("CARGO_PKG_NAME"); const VERSION: &str = env!("CARGO_PKG_VERSION"); const OS_OPTIONS: [(Os, Arch, &str); 5] = [ (Os::Unknown, Arch::X86_64, html_ids::SELECTED_VERSION), (Os::Windows, Arch::X86_64, html_ids::WINDOWS_X86_LINK), (Os::MacOs, Arch::Aarch64, html_ids::MACOS_AARCH64_LINK), (Os::MacOs, Arch::X86_64, html_ids::MACOS_X86_LINK), (Os::Linux, Arch::X86_64, html_ids::LINUX_X86_LINK), ]; #[derive(Debug)] pub struct System; /// Method for platforms supporting opening a file dialog. pub fn open_file_dialog_impl( _title: impl Into, _name: impl Into, extensions: &[impl ToString], _dir: Option>, ) -> anyhow::Result> { let input_id = match extensions[0].to_string().as_str() { "nes" => html_ids::ROM_INPUT, "replay" => html_ids::REPLAY_INPUT, _ => bail!("unsupported file extension"), }; let input = web_sys::window() .and_then(|window| window.document()) .and_then(|document| document.get_element_by_id(input_id)) .and_then(|input| input.dyn_into::().ok()); match input { Some(input) => { // To prevent event loop receiving events while dialog is open if let Some(canvas) = get_canvas() { let _ = canvas.blur(); } input.click(); } None => bail!("failed to find file input element"), } Ok(None) } /// Speak the given text out loud. pub fn speak_text_impl(text: &str) { if text.is_empty() { return; } if let Some(window) = web_sys::window() { tracing::debug!("Speaking {text:?}"); if let Ok(speech_synthesis) = window.speech_synthesis() { speech_synthesis.cancel(); // interrupt previous speech, if any if let Ok(utterance) = web_sys::SpeechSynthesisUtterance::new_with_text(text) { utterance.set_rate(1.0); utterance.set_pitch(1.0); utterance.set_volume(1.0); speech_synthesis.speak(&utterance); } } } } /// Helper method to log and send errors to the UI thread from javascript. fn on_error(tx: &NesEventProxy, err: JsValue) { tracing::error!("{err:?}"); tx.event(UiEvent::Error( err.as_string() .unwrap_or_else(|| "failed to load rom".to_string()), )); } /// Sets up the window resize handler for responding to changes in the viewport size. fn set_resize_handler(window: &web_sys::Window, tx: &NesEventProxy) { let on_resize = Closure::::new({ let tx = tx.clone(); move |_: web_sys::Event| { if let Some(window) = web_sys::window() { let width = window .inner_width() .ok() .and_then(|w| w.as_f64()) .map_or(0.0, |w| w as f32); let height = window .inner_height() .ok() .and_then(|h| h.as_f64()) .map_or(0.0, |h| h as f32); tx.event(RendererEvent::ViewportResized((width, height))); } } }); let on_resize_cb = on_resize.as_ref().unchecked_ref(); if let Err(err) = window.add_event_listener_with_callback("resize", on_resize_cb) { on_error(tx, err); } on_resize.forget(); } /// Sets up the onload handler for reading loaded files. fn set_file_onload_handler( tx: NesEventProxy, input_id: &'static str, reader: web_sys::FileReader, file_name: String, ) -> anyhow::Result<()> { let on_load = Closure::::new({ let reader = reader.clone(); move || match reader.result() { Ok(result) => { let data = Uint8Array::new(&result).to_vec(); let event = match input_id { html_ids::ROM_INPUT => { EmulationEvent::LoadRom((file_name.clone(), RomData(data))) } html_ids::REPLAY_INPUT => { EmulationEvent::LoadReplay((file_name.clone(), ReplayData(data))) } _ => unreachable!("unsupported input id"), }; tx.event(event); focus_canvas(); } Err(err) => on_error(&tx, err), } }); reader.set_onload(Some(on_load.as_ref().unchecked_ref())); on_load.forget(); Ok(()) } /// Sets up the onchange and oncancel handlers for file input elements. fn set_file_onchange_handlers( document: &web_sys::Document, tx: &NesEventProxy, input_id: &'static str, ) -> anyhow::Result<()> { let on_change = Closure::::new({ let tx = tx.clone(); move |evt: web_sys::Event| match FileReader::new() { Ok(reader) => { let Some(file) = evt .current_target() .and_then(|target| target.dyn_into::().ok()) .and_then(|input| input.files()) .and_then(|files| files.item(0)) else { tx.event(UiEvent::FileDialogCancelled); return; }; if let Err(err) = reader .read_as_array_buffer(&file) .map(|_| set_file_onload_handler(tx.clone(), input_id, reader, file.name())) { on_error(&tx, err); } } Err(err) => on_error(&tx, err), } }); let on_cancel = Closure::::new({ let tx = tx.clone(); move |_: web_sys::Event| { focus_canvas(); tx.event(UiEvent::FileDialogCancelled); } }); let input = document .get_element_by_id(input_id) .with_context(|| format!("valid {input_id} button"))?; let on_change_cb = on_change.as_ref().unchecked_ref(); let on_cancel_cb = on_cancel.as_ref().unchecked_ref(); if let Err(err) = input .add_event_listener_with_callback("change", on_change_cb) .and_then(|_| input.add_event_listener_with_callback("cancel", on_cancel_cb)) { on_error(tx, err) } on_change.forget(); on_cancel.forget(); Ok(()) } pub mod renderer { use super::*; use crate::nes::{ config::Config, event::Response, input::Gamepads, renderer::{Viewport, gui::Gui}, }; use std::cell::RefCell; use wasm_bindgen_futures::JsFuture; use winit::dpi::LogicalSize; pub fn constrain_window_to_viewport_impl( renderer: &Renderer, desired_window_width: f32, cfg: &Config, ) -> Response { if let Some(window) = renderer.root_window() && let Some(canvas) = crate::platform::get_canvas() { // Can't use `Window::inner_size` here because it's reported incorrectly so // use `get_client_bounding_rect` instead. let window_width = canvas.get_bounding_client_rect().width() as f32; if window_width < desired_window_width { tracing::debug!( "window width ({window_width}) is less than desired ({desired_window_width})" ); let scale = if let Some(viewport_width) = web_sys::window() .and_then(|win| win.inner_width().ok()) .and_then(|width| width.as_f64()) .map(|width| width as f32) { renderer.find_max_scale_for_width(0.8 * viewport_width, cfg) } else { 1.0 }; tracing::debug!("max scale for viewport: {scale}"); let new_window_size = renderer.window_size_for_scale(cfg, scale); if (window_width - new_window_size.x).abs() > 1.0 { tracing::debug!("constraining window to viewport: {new_window_size:?}"); let _ = window .request_inner_size(LogicalSize::new(new_window_size.x, new_window_size.y)); } return Response { consumed: true, repaint: true, }; } } Response::default() } pub fn set_clipboard_text(state: &Rc>, text: String) -> Response { let State { viewports, focused, .. } = &mut *state.borrow_mut(); let Some(viewport) = focused.and_then(|id| viewports.get_mut(&id)) else { return Response::default(); }; // Requires creating an event and setting the clipboard // here because internally we try to manage a // fallback clipboard for platforms not supported by the current // clipboard backends. // // This has associated behavior in the renderer to prevent // sending 'paste events' (ctrl/cmd+V) to bypass its internal // clipboard handling. viewport .raw_input .events .push(egui::Event::Paste(text.clone())); viewport.clipboard.set(text); Response { consumed: true, repaint: true, } } pub fn process_input( ctx: &egui::Context, state: &Rc>, gui: &Rc>, ) -> Response { let (viewport_ui_cb, raw_input) = { let State { viewports, start_time, focused, .. } = &mut *state.borrow_mut(); let Some(viewport) = focused.and_then(|id| viewports.get_mut(&id)) else { return Response::default(); }; let Some(window) = &viewport.window else { return Response::default(); }; if viewport.occluded { return Response::default(); } Viewport::update_info(&mut viewport.info, ctx, window); let viewport_ui_cb = viewport.viewport_ui_cb.clone(); // On Windows, a minimized window will have 0 width and height. // See: https://github.com/rust-windowing/winit/issues/208 // This solves an issue where egui window positions would be changed when minimizing on Windows. let screen_size_in_pixels = gui::lib::screen_size_in_pixels(window); let screen_size_in_points = screen_size_in_pixels / gui::lib::pixels_per_point(ctx, window); let mut raw_input = viewport.raw_input.take(); raw_input.time = Some(start_time.elapsed().as_secs_f64()); raw_input.screen_rect = (screen_size_in_points.x > 0.0 && screen_size_in_points.y > 0.0) .then(|| egui::Rect::from_min_size(egui::Pos2::ZERO, screen_size_in_points)); raw_input.viewport_id = viewport.ids.this; raw_input.viewports = viewports .iter() .map(|(id, viewport)| (*id, viewport.info.clone())) .collect(); (viewport_ui_cb, raw_input.take()) }; // For the purposes of processing inputs, we don't need or care about gamepad or cfg state let config = Config::default(); let gamepads = Gamepads::default(); let mut output = ctx.run_ui(raw_input, |ui| match &viewport_ui_cb { Some(viewport_ui_cb) => viewport_ui_cb(ui), None => gui.borrow_mut().ui(ui, &config, &gamepads), }); let State { viewports, focused, .. } = &mut *state.borrow_mut(); let Some(viewport) = focused.and_then(|id| viewports.get_mut(&id)) else { return Response::default(); }; viewport.info.events.clear(); let commands = std::mem::take(&mut output.platform_output.commands); for command in commands { use egui::OutputCommand; if let OutputCommand::CopyText(copied_text) = command { tracing::warn!("Copied text: {copied_text}"); if !copied_text.is_empty() && let Some(clipboard) = web_sys::window().map(|window| window.navigator().clipboard()) { let promise = clipboard.write_text(&copied_text); let future = JsFuture::from(promise); let future = async move { if let Err(err) = future.await { tracing::error!( "Cut/Copy failed: {}", err.as_string().unwrap_or_else(|| format!("{err:#?}")) ); } }; thread::spawn(future); } } } Response { consumed: true, repaint: true, } } } /// Enumeration of supported operating systems. #[derive(Debug, Copy, Clone)] #[must_use] enum Os { Unknown, Windows, #[allow(clippy::enum_variant_names)] MacOs, Linux, Mobile, } impl std::fmt::Display for Os { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let os = match self { Os::Windows => "Windows", Os::MacOs => "macOS", Os::Linux => "Linux", _ => "Desktop", }; write!(f, "{os}") } } /// Enumeration of supported CPU architectures. #[derive(Debug, Copy, Clone)] #[must_use] enum Arch { X86_64, Aarch64, } impl std::fmt::Display for Arch { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let arch = match self { Arch::X86_64 => "x86_64", Arch::Aarch64 => "aarch64", }; write!(f, "{arch}") } } /// Converts the operating system and architecture to a human-readable string. const fn platform_to_string(os: Os, arch: Arch) -> &'static str { match (os, arch) { (Os::Windows, Arch::X86_64) => "Windows", (Os::MacOs, Arch::X86_64) => "Mac - Intel Chip", (Os::MacOs, Arch::Aarch64) => "Mac - Apple Chip", (Os::Linux, Arch::X86_64) => "Linux", (Os::Mobile, _) => "Mobile", _ => "Desktop", } } #[wasm_bindgen] extern "C" { /// Extends the `Navigator` object to support the `userAgentData` method. #[wasm_bindgen(extends = web_sys::Navigator)] type NavigatorExt; /// The `NavigatorUAData` is what's returned from `navigator.userAgentData` on browsers that /// support it. type NavigatorUAData; /// The `HighEntropyValues` object is returned from `navigator.userAgentData.getHighEntropyValues`. #[derive(Debug)] #[wasm_bindgen(js_name = Object)] type HighEntropyValues; /// `navigator.userAgentData` for browsers that support it. #[wasm_bindgen(method, getter, js_name = userAgentData)] fn user_agent_data(this: &NavigatorExt) -> Option; /// `navigator.userAgentData.getHighEntropyValues()` for browsers that support it. #[wasm_bindgen(method, js_name = getHighEntropyValues)] async fn get_high_entropy_values(this: &NavigatorUAData, hints: Vec) -> JsValue; /// `HighEntropyValues.mobile` indicates whether the detected platform is a mobile device. #[wasm_bindgen(method, getter, js_class = "HighEntropyValues")] fn mobile(this: &HighEntropyValues) -> bool; /// `HighEntropyValues.platform` indicates the detected OS platform (e.g. `Windows`). #[wasm_bindgen(method, getter, js_class = "HighEntropyValues")] fn platform(this: &HighEntropyValues) -> String; /// `HighEntropyValues.platform` indicates the detected CPU architecture. (e.g. `x86`). #[wasm_bindgen(method, getter, js_class = "HighEntropyValues")] fn architecture(this: &HighEntropyValues) -> String; } /// Detects the user's platform and architecture. async fn detect_user_platform() -> anyhow::Result<(Os, Arch)> { let navigator = web_sys::window() .map(|win| win.navigator()) .context("failed to get navigator")?; let user_agent = navigator.user_agent().unwrap_or_default(); let mut os = if user_agent.contains("Mobile") { anyhow::bail!("mobile download is unsupported"); } else if user_agent.contains("Windows") { Os::Windows } else if user_agent.contains("Mac") { Os::MacOs } else if user_agent.contains("Linux") { Os::Linux } else { Os::Unknown }; let mut arch = Arch::X86_64; // FIXME: Currently unsupported on Firefox/Safari but it's the only way to derive // macOS aarch64 let navigator_ext = NavigatorExt { obj: navigator }; let Some(ua_data) = navigator_ext.user_agent_data() else { return Ok((os, arch)); }; let Ok(ua_values) = ua_data .get_high_entropy_values(vec![ "architecture".into(), "platform".into(), "bitness".into(), ]) .await .dyn_into::() else { return Ok((os, arch)); }; if ua_values.mobile() { os = Os::Mobile; } else { match ua_values.platform().as_str() { "Windows" => os = Os::Windows, "macOS" => { os = Os::MacOs; arch = if ua_values.architecture().starts_with("x86") { Arch::X86_64 } else { Arch::Aarch64 }; } "Linux" => os = Os::Linux, _ => (), } }; Ok((os, arch)) } /// Constructs the download URL for the given operating system and architecture. fn download_url_by_os(os: Os, arch: Arch) -> String { let base_url = format!("https://github.com/lukexor/tetanes/releases/download/tetanes-v{VERSION}"); match os { Os::MacOs => format!("{base_url}/{BIN_NAME}-{arch}.dmg"), Os::Windows => format!("{base_url}/{BIN_NAME}-{arch}.msi"), Os::Linux => format!("{base_url}/{BIN_NAME}-{arch}-unknown-linux-gnu.tar.gz"), _ => format!("https://github.com/lukexor/tetanes/releases/tag/tetanes-v{VERSION}"), } } /// Sets the download links to the correct release artifacts. fn set_download_versions(document: &web_sys::Document) { if let Some(version) = document.get_element_by_id(html_ids::VERSION) { version.set_inner_html(concat!("v", env!("CARGO_PKG_VERSION"))); } let document = document.clone(); thread::spawn(async move { // Update download links to the correct release artifacts for (os, arch, id) in OS_OPTIONS { if let Some(download_link) = document .get_element_by_id(id) .and_then(|el| el.dyn_into::().ok()) { download_link.set_href(&download_url_by_os(os, arch)); let platform = platform_to_string(os, arch); download_link.set_inner_text(&format!("Download for {platform}")); } } // Set selected version to detected platform if let Some(selected_version) = document .get_element_by_id(html_ids::SELECTED_VERSION) .and_then(|el| el.dyn_into::().ok()) && let Ok((os, arch)) = detect_user_platform().await { selected_version.set_href(&download_url_by_os(os, arch)); let platform = platform_to_string(os, arch); selected_version.set_inner_text(&format!("Download for {platform}")); // Add mouseover/mouseout event listeners to version download links and make them visible if let (Some(version_download), Some(version_options)) = ( document.get_element_by_id(html_ids::VERSION_DOWNLOAD), document.get_element_by_id(html_ids::VERSION_OPTIONS), ) { let on_mouseover = Closure::::new({ let version_options = version_options.clone(); move |_: web_sys::MouseEvent| { if let Err(err) = version_options.class_list().remove_1("hidden") { tracing::error!("{err:?}"); } } }); let on_mouseout = Closure::::new(move |_: web_sys::MouseEvent| { if let Err(err) = version_options.class_list().add_1("hidden") { tracing::error!("{err:?}"); } }); let on_mouseover_cb = on_mouseover.as_ref().unchecked_ref(); let on_mouseout_cb = on_mouseout.as_ref().unchecked_ref(); if let Err(err) = version_download .add_event_listener_with_callback("mouseover", on_mouseover_cb) .and_then(|_| { version_download .add_event_listener_with_callback("mouseout", on_mouseout_cb) }) .and_then(|_| version_download.class_list().remove_1("hidden")) { tracing::error!("{err:?}"); } on_mouseover.forget(); on_mouseout.forget(); if let Err(err) = version_download.class_list().remove_1("hidden") { tracing::error!("{err:?}"); } } } }); } /// Hides the loading status when the WASM module has finished loading. fn finish_loading(document: &web_sys::Document, tx: &NesEventProxy) -> anyhow::Result<()> { if let Some(status) = document.get_element_by_id(html_ids::LOADING_STATUS) && let Err(err) = status.class_list().add_1("hidden") { on_error(tx, err); } Ok(()) } impl Initialize for Running { /// Initialize JS event handlers and DOM elements. fn initialize(&mut self) -> anyhow::Result<()> { let window = web_sys::window().context("valid window")?; let document = window.document().context("valid html document")?; set_download_versions(&document); set_resize_handler(&window, &self.tx); for input_id in [html_ids::ROM_INPUT, html_ids::REPLAY_INPUT] { set_file_onchange_handlers(&document, &self.tx, input_id)?; } finish_loading(&document, &self.tx)?; Ok(()) } } impl Initialize for Renderer { /// Initialize JS event handlers and DOM elements. fn initialize(&mut self) -> anyhow::Result<()> { let document = web_sys::window() .and_then(|window| window.document()) .context("failed to get html document")?; let on_paste = Closure::::new({ let ctx = self.ctx.clone(); let state = Rc::clone(&self.state); move |evt: web_sys::ClipboardEvent| { if let Some(data) = evt.clipboard_data() && let Ok(text) = data.get_data("text") { let text = text.replace("\r\n", "\n"); if !text.is_empty() { let res = renderer::set_clipboard_text(&state, text); if res.repaint { ctx.request_repaint(); } if res.consumed { evt.stop_propagation(); evt.prevent_default(); } } } } }); if let Err(err) = document.add_event_listener_with_callback("paste", on_paste.as_ref().unchecked_ref()) { tracing::error!("failed to set paste handler: {err:?}"); } on_paste.forget(); let on_cut = Closure::::new({ let ctx = self.ctx.clone(); let state = Rc::clone(&self.state); let gui = Rc::clone(&self.gui); move |evt: web_sys::ClipboardEvent| { // Some browsers require transient activation, so we have to write to the clipboard // now let res = renderer::process_input(&ctx, &state, &gui); if res.repaint { ctx.request_repaint(); } if res.consumed { evt.stop_propagation(); evt.prevent_default(); } } }); if let Err(err) = document.add_event_listener_with_callback("cut", on_cut.as_ref().unchecked_ref()) { tracing::error!("failed to set cut handler: {err:?}"); } on_cut.forget(); let on_copy = Closure::::new({ let ctx = self.ctx.clone(); let state = Rc::clone(&self.state); let gui = Rc::clone(&self.gui); move |evt: web_sys::ClipboardEvent| { // Some browsers require transient activation, so we have to write to the clipboard // now let res = renderer::process_input(&ctx, &state, &gui); if res.repaint { ctx.request_repaint(); } if res.consumed { evt.stop_propagation(); evt.prevent_default(); } } }); if let Err(err) = document.add_event_listener_with_callback("copy", on_copy.as_ref().unchecked_ref()) { tracing::error!("failed to set copy handler: {err:?}"); } on_copy.forget(); if let Some(canvas) = get_canvas() { let on_keydown = Closure::::new(move |evt: web_sys::KeyboardEvent| { use egui::Key; let prevent_default = Key::from_name(&evt.key()).is_none_or(|key| { // Allow ctrl/meta + X, C, V through !matches!(key, Key::X | Key::C | Key::V) || !(evt.ctrl_key() || evt.meta_key()) }); if prevent_default { evt.prevent_default(); } }); if let Err(err) = canvas .add_event_listener_with_callback("keydown", on_keydown.as_ref().unchecked_ref()) { tracing::error!("failed to set keydown handler: {err:?}"); } on_keydown.forget(); // Because we want to capture cut/copy/paste, `prevent_default` is disabled on winit, // so restore default behavior on other winit events for event in [ "touchstart", "keyup", "wheel", "contextmenu", "pointerdown", "pointermove", ] { let on_event = Closure::::new({ let canvas = canvas.clone(); move |evt: web_sys::Event| { evt.prevent_default(); if event == "pointerdown" { let _ = canvas.focus(); } } }); if let Err(err) = canvas .add_event_listener_with_callback(event, on_event.as_ref().unchecked_ref()) { tracing::error!("failed to set {event} handler: {err:?}"); } on_event.forget(); } } Ok(()) } } pub fn download_save_states() -> anyhow::Result<()> { use crate::nes::config::Config; use anyhow::{Context, anyhow}; use base64::Engine; use std::io::{Cursor, Write}; use tetanes_core::{control_deck::Config as DeckConfig, sys::fs::local_storage}; use wasm_bindgen::JsCast; use web_sys::{self, js_sys}; use zip::write::{SimpleFileOptions, ZipWriter}; let local_storage = local_storage()?; let mut zip = ZipWriter::new(Cursor::new(Vec::with_capacity(30 * 1024))); for key in js_sys::Object::keys(&local_storage) .iter() .filter_map(|key| key.as_string()) .filter(|key| { key.ends_with(Config::SAVE_EXTENSION) || key.ends_with(DeckConfig::SRAM_EXTENSION) }) { zip.start_file(&*key, SimpleFileOptions::default())?; let Some(data) = local_storage .get_item(&key) .map_err(|_| anyhow!("failed to find data for {key}"))? .and_then(|value| serde_json::from_str::>(&value).ok()) else { continue; }; zip.write_all(&data)?; } let res = zip.finish()?; let document = web_sys::window() .and_then(|window| window.document()) .context("failed to get document")?; let link = document .create_element("a") .map_err(|err| anyhow!("failed to create link element: {err:?}"))?; link.set_attribute( "href", &format!( "data:text/plain;base64,{}", base64::prelude::BASE64_STANDARD.encode(res.into_inner()) ), ) .map_err(|err| anyhow!("failed to set href attribute: {err:?}"))?; link.set_attribute("download", "tetanes-save-states.zip") .map_err(|err| anyhow!("failed to set download attribute: {err:?}"))?; let link: web_sys::HtmlAnchorElement = web_sys::HtmlAnchorElement::unchecked_from_js(link.into()); link.click(); Ok(()) } impl BuilderExt for WindowAttributes { /// Sets platform-specific window options. fn with_platform(self, _title: &str) -> Self { // Prevent default false allows cut/copy/paste self.with_canvas(get_canvas()).with_prevent_default(false) } } mod html_ids { //! HTML element IDs used to interact with the DOM. pub(super) const CANVAS: &str = "frame"; pub(super) const LOADING_STATUS: &str = "loading-status"; pub(super) const ROM_INPUT: &str = "load-rom"; pub(super) const REPLAY_INPUT: &str = "load-replay"; pub(super) const VERSION: &str = "version"; pub(super) const VERSION_DOWNLOAD: &str = "version-download"; pub(super) const VERSION_OPTIONS: &str = "version-options"; pub(super) const SELECTED_VERSION: &str = "selected-version"; pub(super) const WINDOWS_X86_LINK: &str = "x86_64-pc-windows-msvc"; pub(super) const MACOS_X86_LINK: &str = "x86_64-apple-darwin"; pub(super) const MACOS_AARCH64_LINK: &str = "aarch64-apple-darwin"; pub(super) const LINUX_X86_LINK: &str = "x86_64-unknown-linux-gnu"; } /// Gets the primary canvas element. pub fn get_canvas() -> Option { web_sys::window() .and_then(|win| win.document()) .and_then(|doc| doc.get_element_by_id(html_ids::CANVAS)) .and_then(|canvas| canvas.dyn_into::().ok()) } /// Focuses the canvas element. pub fn focus_canvas() { if let Some(canvas) = get_canvas() { let _ = canvas.focus(); } } ================================================ FILE: tetanes/src/sys/platform.rs ================================================ use cfg_if::cfg_if; cfg_if! { if #[cfg(target_arch = "wasm32")] { mod wasm; pub use wasm::*; } else { mod os; pub use os::*; } } ================================================ FILE: tetanes/src/sys/thread/os.rs ================================================ use std::{future::Future, thread}; use tetanes_core::time::{Duration, Instant}; /// Spawn a future to be run until completion. pub fn spawn_impl(future: F) where F: Future + 'static, { pollster::block_on(future) } /// Blocks unless or until the current thread's token is made available or /// the specified duration has been reached (may wake spuriously). pub fn park_timeout_impl(dur: Duration) { let beginning_park = Instant::now(); let mut timeout_remaining = dur; loop { thread::park_timeout(timeout_remaining); let elapsed = beginning_park.elapsed(); if elapsed >= dur { break; } timeout_remaining = dur - elapsed; } } /// Sleeps the current thread for the specified duration. pub async fn sleep_impl(dur: Duration) { // TODO: Async is a lie and is only required to allow the web impl to be non-blocking thread::sleep(dur); } ================================================ FILE: tetanes/src/sys/thread/wasm.rs ================================================ use std::future::Future; use tetanes_core::time::Duration; use wasm_bindgen_futures::JsFuture; use web_sys::js_sys::{Function, Promise}; /// Spawn a future to be run until completion. pub fn spawn_impl(future: F) where F: Future + 'static, { wasm_bindgen_futures::spawn_local(future); } /// Blocking, and thus parking is not allowed in wasm. #[allow(clippy::missing_const_for_fn)] pub fn park_timeout_impl(_dur: Duration) {} /// Sleeps the current thread for the specified duration. pub async fn sleep_impl(dur: Duration) { let mut cb = |resolve: Function, _reject: Function| { if let Some(window) = web_sys::window() && let Err(err) = window.set_timeout_with_callback_and_timeout_and_arguments_0( &resolve, dur.as_secs() as i32, ) { tracing::error!("failed to call window.set_timeout: {err:?}"); } }; if let Err(err) = JsFuture::from(Promise::new(&mut cb)).await { tracing::error!("failed to create sleep future: {err:?}"); } } ================================================ FILE: tetanes/src/sys/thread.rs ================================================ use cfg_if::cfg_if; cfg_if! { if #[cfg(target_arch = "wasm32")] { mod wasm; pub use wasm::*; } else { mod os; pub use os::*; } } ================================================ FILE: tetanes/src/sys.rs ================================================ pub mod info; pub mod logging; pub mod platform; pub mod thread; #[derive(Debug)] pub struct DiskUsage { pub read_bytes: u64, pub total_read_bytes: u64, pub written_bytes: u64, pub total_written_bytes: u64, } #[derive(Debug)] pub struct SystemStats { pub cpu_usage: f32, pub memory: u64, pub disk_usage: DiskUsage, } pub trait SystemInfo { fn update(&mut self); fn stats(&self) -> Option; } ================================================ FILE: tetanes/src/thread.rs ================================================ use crate::sys::thread; use std::future::Future; use tetanes_core::time::Duration; /// Spawn a future to be run until completion. pub fn spawn(future: F) where F: Future + 'static, { thread::spawn_impl(future); } /// Blocks unless or until the current thread's token is made available or /// the specified duration has been reached (may wake spuriously). pub fn park_timeout(dur: Duration) { thread::park_timeout_impl(dur); } /// Sleeps the current thread for the specified duration. pub async fn sleep(dur: Duration) { thread::sleep_impl(dur).await } ================================================ FILE: tetanes/wix/main.wxs ================================================ 1 WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed ================================================ FILE: tetanes-core/CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.14.1](https://github.com/lukexor/tetanes/compare/0.13.0..0.14.1) - 2026-04-20 ### 🐛 Bug Fixes - Fixed palette x offset - ([350a892](https://github.com/lukexor/tetanes/commit/350a8925dffcd49e737884971b90f11d2a737996)) ### 🎨 Styling - Fixed 1.85 lints - ([ddfb3eb](https://github.com/lukexor/tetanes/commit/ddfb3eb318397984df76fbd3cb36c8a383ab0368)) - Fixed nightly lints - ([801bb3c](https://github.com/lukexor/tetanes/commit/801bb3c95a95a7f98efb5a993f2efe7061daf00b)) ## [0.13.0](https://github.com/lukexor/tetanes/compare/0.12.2..0.13.0) - 2026-02-14 ### 🐛 Bug Fixes - Change cycle to u32 to improve 32-bit platforms like wasm - ([86f597c](https://github.com/lukexor/tetanes/commit/86f597c3c1422040e8f98ac4dfb29f3a2ffdc81f)) - Fixed turbo - ([5f149c3](https://github.com/lukexor/tetanes/commit/5f149c3be9eaa74bd5583dd41daf4cbae2cbf0a9)) - Ignore header bytes 14/15. closed #430 - ([8ff2bd9](https://github.com/lukexor/tetanes/commit/8ff2bd96d055c5a2551e44deb69c8a5e1c5b191e)) - Fixed memory methods to allow working with &[u8] - ([6c2cf4a](https://github.com/lukexor/tetanes/commit/6c2cf4aa33e5fa7ba3c903c775b6ecfcf47f059d)) ### ⚡ Performance - Cpu opcode refactor - ([8916097](https://github.com/lukexor/tetanes/commit/8916097c39ee18828d9fd7bf70185a19eaf8e098)) - Convert Vec to Box<[u8]> for ~2.5% gain - ([6f34112](https://github.com/lukexor/tetanes/commit/6f34112aa193499ed69115fe8f70774bbe6e4a74)) ### ⚙️ Miscellaneous Tasks - Updated deps - ([ccb7463](https://github.com/lukexor/tetanes/commit/ccb7463c98be1e35fe7e6a47c2df9d76796ee349)) - Update deps - ([782dc21](https://github.com/lukexor/tetanes/commit/782dc213f25f34cc47af0c2f71cdf9bfd44ae28b)) ## [0.12.2](https://github.com/lukexor/tetanes/compare/0.12.1..0.12.2) - 2025-04-05 ### 🐛 Bug Fixes - Revert input serialization change, as it broke run-ahead - ([6489c57](https://github.com/lukexor/tetanes/commit/6489c579c738f88068423affb3833edd9105e523)) - Fix touch events - ([1a8d3da](https://github.com/lukexor/tetanes/commit/1a8d3dad5243bb31858575e99e6961c33a2521d4)) - Basic cpu error recovery options - ([c60d8d1](https://github.com/lukexor/tetanes/commit/c60d8d1d02dde9e3383849e1af43111f26db29c7)) ### 🚜 Refactor - Changed input serializing - ([ad37b47](https://github.com/lukexor/tetanes/commit/ad37b47ab07a54597eeb91b95fb524a0ea6b4e43)) - Cleaned up Memory struct - ([7cb31fb](https://github.com/lukexor/tetanes/commit/7cb31fb3878ee4e7e9b4ca9eabe123d570cc3176)) ### 📚 Documentation - Fix urls - ([8136c42](https://github.com/lukexor/tetanes/commit/8136c42bdab474e17ceda78819225349a3f1c520)) - Add notes about stability - ([af0ce20](https://github.com/lukexor/tetanes/commit/af0ce20410f4088453788a264f8102823c7cf7da)) - Ensure features display in docs.rs - ([f58b31c](https://github.com/lukexor/tetanes/commit/f58b31cc9d27f0187d759796c64e34ccea3f784a)) ### 🧪 Testing - Fix tracing init in tests - ([e368ad5](https://github.com/lukexor/tetanes/commit/e368ad5ab41d3c484dcaf17a7a8ff8abae48aef9)) ## [0.12.1](https://github.com/lukexor/tetanes/compare/0.12.0..0.12.1) - 2025-03-13 ### ⛰️ Features - Added shortcuts for shaders and ppu warmup flag - ([408b122](https://github.com/lukexor/tetanes/commit/408b122ed98f7edb7a26085fb921aa006bde7091)) ### 🐛 Bug Fixes - Fixed issues with some mmc1 games - ([496cf41](https://github.com/lukexor/tetanes/commit/496cf41ced63949fd6d8be5402989e927baf92b8)) ### 📚 Documentation - Fixed cargo doc url - ([782f7c5](https://github.com/lukexor/tetanes/commit/782f7c51b68c5fb52b483a3f151bd3db227286a9)) - Updated changelog and readmes - ([a4a3e8c](https://github.com/lukexor/tetanes/commit/a4a3e8c0775a7261b91f4238756ac5a20d2c4b48)) ### ⚙️ Miscellaneous Tasks - Fix/update ci, docs, and fixed nightly issue with tetanes-core - ([a6150ba](https://github.com/lukexor/tetanes/commit/a6150bad6703bbc661d7d5c8b63f5a6d47991868)) # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.12.0](https://github.com/lukexor/tetanes/compare/tetanes-v0.11.0..tetanes-v0.12.0) - 2025-03-12 ### ⛰️ Features - Jalecoss88006 - ([406777a](https://github.com/lukexor/tetanes/commit/406777abad8d61490aae2a33e2e71fc617db3f55)) - Namco163 - ([89d7fb4](https://github.com/lukexor/tetanes/commit/89d7fb4617bf844ad2090cd92f0e92cda9cc91fc)) - Added sunsoft/fme-7 - ([303dad8](https://github.com/lukexor/tetanes/commit/303dad85d0a6586a88b7067f2070c5ac9e4da6e4)) - Added nina003/nina006 - ([29503c3](https://github.com/lukexor/tetanes/commit/29503c3efc81fb3110eef63e9666de1e0c912015)) - Added dxrom ([#340](https://github.com/lukexor/tetanes/issues/340)) - ([906af59](https://github.com/lukexor/tetanes/commit/906af59038e95874dda254e02998030c913d8c61)) - Ppu-viewer ([#339](https://github.com/lukexor/tetanes/issues/339)) - ([fce7d89](https://github.com/lukexor/tetanes/commit/fce7d89f78148e9a367d47122eef7e6e8fe45b34)) - Bandai mappers 016, 153, 157, 159 ([#335](https://github.com/lukexor/tetanes/issues/335)) - ([f555ea4](https://github.com/lukexor/tetanes/commit/f555ea48d0273bc9d41b998926d451398acbb73c)) - Allow exporting save states in web ([#311](https://github.com/lukexor/tetanes/issues/311)) - ([627bbec](https://github.com/lukexor/tetanes/commit/627bbece49739ff479e69ba9e83df828c4d4a633)) - Add a debug build label - ([46b3d94](https://github.com/lukexor/tetanes/commit/46b3d94e5fd24900a95554a295257f0891ac1c53)) - Add test panic debug button - ([3866efa](https://github.com/lukexor/tetanes/commit/3866efab39ced8f3431ee27d24962a55397a9f07)) - Added screen reader/accesskit support - ([5fd1a73](https://github.com/lukexor/tetanes/commit/5fd1a73f112f74a6c0a81e722485842dd37e0a38)) - Added ui setting/debug windows - ([db8b122](https://github.com/lukexor/tetanes/commit/db8b122af6c5a52ad23ed89ffd6f2feb35515603)) - Enable webgpu for browsers that support it. closes #297 ([#298](https://github.com/lukexor/tetanes/issues/298)) - ([a6bde61](https://github.com/lukexor/tetanes/commit/a6bde619454bf8d77f98d462d89eccea4b0e42fc)) ### 🐛 Bug Fixes - Fixed several issues - ([60fcd90](https://github.com/lukexor/tetanes/commit/60fcd90e740833e94deb98896a17a51fcda38998)) - Fix cycle overflow - ([a4e1f05](https://github.com/lukexor/tetanes/commit/a4e1f058c6e899e9fd11578bfb40a36d6ea1980e)) - Add temporary webgpu flag - ([179e868](https://github.com/lukexor/tetanes/commit/179e868c9e1cee92df1d0568b60403c2df7579cb)) - Temporary wasm fix for check-cfg - ([30c6a61](https://github.com/lukexor/tetanes/commit/30c6a61c0d562f875a0d979ad655e199d7c7019a)) - Fix tetanes-core compiling on stable. closes #360 - ([adc5673](https://github.com/lukexor/tetanes/commit/adc5673a3ed5d80aff339c3ab6d95013fcb2d715)) - Fixed deny.toml - ([2c1f186](https://github.com/lukexor/tetanes/commit/2c1f18603f043c2dcb17db8fa8958ea1cbfd88d4)) - Fixed bank size check - ([c84c012](https://github.com/lukexor/tetanes/commit/c84c012c310ad466c5167b94f0228f5a482dec43)) - Fixed wasm - ([bd27814](https://github.com/lukexor/tetanes/commit/bd278140bcc7e7d433917f14e99038f2e6453027)) - Fixed video frame size - ([153094d](https://github.com/lukexor/tetanes/commit/153094d81d444376b112224409544375588c4f97)) - Fix scroll issues - ([218d786](https://github.com/lukexor/tetanes/commit/218d7860421eb4cfc4d7b833132f4c476935777a)) - Fixed increasing scale on web - ([8c4265e](https://github.com/lukexor/tetanes/commit/8c4265e10fc8b62cd7dcaa8a828fed1a07100a9f)) - Fixed shortcut text - ([cb73c21](https://github.com/lukexor/tetanes/commit/cb73c216936ad49dca4e2595485df4ccea957eaa)) - Fixed joypad keybinds and some UI styling - ([bc2f093](https://github.com/lukexor/tetanes/commit/bc2f093b4d02c54744f791f336a102424a7e5af1)) - Enable puffin on wasm - ([0b6f794](https://github.com/lukexor/tetanes/commit/0b6f79429c5d2a642c0ef6301bbcc9818973a234)) - Fix window theme - ([e3c42c7](https://github.com/lukexor/tetanes/commit/e3c42c7720f558c7348e2b82b3573d4748158850)) - Fixed window aspect ratio - ([17db5c8](https://github.com/lukexor/tetanes/commit/17db5c8a037ab3aefab560bca67545964069658f)) - Don't log/error when sending frames while paused - ([50825f8](https://github.com/lukexor/tetanes/commit/50825f82e9f04418fdefd56707ef2ec50cddd5ed)) - Fixed pause state when loading replay - ([d743b31](https://github.com/lukexor/tetanes/commit/d743b31c190cd93e42e3ab78b497e59bcc4ade88)) - Fixed roms path to default to current directory, if valid, and canonicalize - ([e00273f](https://github.com/lukexor/tetanes/commit/e00273f740f7fc095bc02c7ce6d0ba132a14c9bc)) - Ensure pixel brightness is using the same palette - ([ad2f873](https://github.com/lukexor/tetanes/commit/ad2f873f5652016b96317c000b4abbe0e35de421)) - Move some calculations to vertex shader that don't depend on v_uv - ([a6f262d](https://github.com/lukexor/tetanes/commit/a6f262db5d83950e86e0ec78bb74fc63e5c2bf85)) - Fixed logging location - ([ff36033](https://github.com/lukexor/tetanes/commit/ff36033d7bbbf64924d97d6e9a88dcf4db7dc60c)) - Fixed issue with lower end platforms not supporting larger texture dimensions - ([ef214db](https://github.com/lukexor/tetanes/commit/ef214dbc2f2eee016b7abdb0c2b0ee1858381ee4)) - Fix window resizing while handling zoom changes - ([6b3f690](https://github.com/lukexor/tetanes/commit/6b3f690b8ec21b907d353a7cad8561217e8d9dcf)) ### 🚜 Refactor - [**breaking**] Split mapper traits - ([3e4a372](https://github.com/lukexor/tetanes/commit/3e4a372dfdc4295851c93cca96044f84645ae14e)) - Removed egui-wgpu and egui-winit dependencies. ([#315](https://github.com/lukexor/tetanes/issues/315)) - ([b3d4e2c](https://github.com/lukexor/tetanes/commit/b3d4e2c70c6ee4cfa9aaf53a11c1ae802610ff99)) - Platform/ui cleanup - ([39f66e6](https://github.com/lukexor/tetanes/commit/39f66e6e912f9c95cf9c458cd072e5e041af09e3)) - Moved around platform code to condense it - ([0f18928](https://github.com/lukexor/tetanes/commit/0f18928b8f8ed031cac7a170557c0296916c99bc)) - Prefer deferred viewports ([#306](https://github.com/lukexor/tetanes/issues/306)) - ([e1e60d1](https://github.com/lukexor/tetanes/commit/e1e60d19599ab883cbb034047519e6eb831d6c6c)) ### 📚 Documentation - Extra cpu comments - ([80f3366](https://github.com/lukexor/tetanes/commit/80f3366e3fab1257201ab0d9af673c4318edabef)) ### ⚡ Performance - Restore sprite presence check, ~2% gain - ([c6d353a](https://github.com/lukexor/tetanes/commit/c6d353a8fc12b506656a8cd70561ef1830ba9284)) - More perf and added flamegraph - ([31edf0c](https://github.com/lukexor/tetanes/commit/31edf0c63bcc30867f0049a231e7d366db4bde8d)) - Performance tweaks - ([d9a3019](https://github.com/lukexor/tetanes/commit/d9a3019ec0c0014d8850158d38c27289dc885020)) ### 🎨 Styling - Fix lints - ([bc9f6bc](https://github.com/lukexor/tetanes/commit/bc9f6bc293d413cf780a2aa0253ad7d64951d193)) - Slight cleanup - ([63e31a9](https://github.com/lukexor/tetanes/commit/63e31a9755266bec88d5c79e064506999f03aea2)) - Fixed format - ([d62ea28](https://github.com/lukexor/tetanes/commit/d62ea285cb5fe73ac41e7364f0ca3f32281a0e88)) ### ⚙️ Miscellaneous Tasks - Update deps - ([5b077c0](https://github.com/lukexor/tetanes/commit/5b077c01b1e68a60d3e295fe108732a3b8abbbd6)) - Bumped version - ([28fa93f](https://github.com/lukexor/tetanes/commit/28fa93f226447fd409b5d3846cd0f7e14a793f83)) - Update deps - ([509dbd4](https://github.com/lukexor/tetanes/commit/509dbd48a34cd6a360da0fba3786ed73445381fc)) - Fix ci - ([da64229](https://github.com/lukexor/tetanes/commit/da64229966295d85b0f62b0e3827d76767116602)) - Fix deny.toml - ([64a2401](https://github.com/lukexor/tetanes/commit/64a24010c72926c555ae74ffb4f1acb2c0aefffb)) - Updated deps - ([906c877](https://github.com/lukexor/tetanes/commit/906c877700d551fd74e0545e03f544ea2255823f)) - Updated deps - ([825719e](https://github.com/lukexor/tetanes/commit/825719e7f56ef6263f22a6da82d31f02d05af570)) - Updated deps - ([4712d6d](https://github.com/lukexor/tetanes/commit/4712d6d6de3ce7eccec8f1971fcb0f2411f91e3d)) - Restore nightly ci - ([eb2a2c5](https://github.com/lukexor/tetanes/commit/eb2a2c58ecd802810709f5e367253857d51a47d0)) - Update dependencies - ([4947a8c](https://github.com/lukexor/tetanes/commit/4947a8cf6883eda0b0c55fcd7bcf98cf8fd7dee9)) - Remove puffin_egui reference in wasm - ([16845f3](https://github.com/lukexor/tetanes/commit/16845f39e28c816c847a9d403dbedde38c815c1d)) - More dependency cleanup - ([1971e4f](https://github.com/lukexor/tetanes/commit/1971e4f2c5aaf6f8a2d6ce2a03c978362d44afe1)) - Clean up dependencies - ([254fe54](https://github.com/lukexor/tetanes/commit/254fe543293b0c96c78ce25bdaeef2f250a9fb14)) - Remove auto-assign from triage - ([9a2804b](https://github.com/lukexor/tetanes/commit/9a2804b94b1a412214159495d2e6410a63555572)) - Restrict homebrew cd to .rb files - ([3c1e390](https://github.com/lukexor/tetanes/commit/3c1e3907d7477dbe9f6953d9b8b9b0aeb1ef5966)) - Fix update homebrew formula runs-on - ([9e66a07](https://github.com/lukexor/tetanes/commit/9e66a073fa1ef9e276a2ca85ccc4e4281b50e7bc)) - Fix cd upload - ([892d184](https://github.com/lukexor/tetanes/commit/892d184cc25ca7903cb4a5f7372f47e722866125)) - Restore RELEASE_PLZ_TOKEN - ([18de294](https://github.com/lukexor/tetanes/commit/18de2946b82a44efdba96e5918eba381ad3a1a75)) - Remove need for RELEASE_PLZ_TOKEN - ([b6c8478](https://github.com/lukexor/tetanes/commit/b6c84780123ca5d9dfc841e2a3e6266b7d3cc4b9)) - Try to fix release cd - ([c7d5f51](https://github.com/lukexor/tetanes/commit/c7d5f514a84bd3b728686893e3211b63ec21a9c9)) ## [0.11.0](https://github.com/lukexor/tetanes/compare/tetanes-core-v0.10.0..tetanes-core-v0.11.0) - 2024-06-12 ### ⛰️ Features - Added config and save/sram state persistence to web ([#274](https://github.com/lukexor/tetanes/pull/274)) - ([8c7f6df](https://github.com/lukexor/tetanes/commit/8c7f6df4a8894b544da1c6480659ee26ea28f342)) - Added mapper 11 - ([03d2074](https://github.com/lukexor/tetanes/commit/03d2074d3d58fcf652fecb9d77f4e96e8c007aae)) - Updated game database mapper names - ([86d246b](https://github.com/lukexor/tetanes/commit/86d246be9a52b64ed4191c970c6a727a31c21cb5)) ### 🐛 Bug Fixes - Ntsc tweaks - ([3042fa7](https://github.com/lukexor/tetanes/commit/3042fa7b928faf69e10040b4eb981a4c4f8f3ce3)) - Fixed fast forwarding - ([a6f87bb](https://github.com/lukexor/tetanes/commit/a6f87bb58ac3728471f673ade821e18579686b1a)) - Cleaned up pausing, parking, and control flow. Closes [#251](https://github.com/lukexor/tetanes/pull/251) - ([72cf88a](https://github.com/lukexor/tetanes/commit/72cf88ac6991953222bd3dd1d395f7f9035c98ef)) - Disable rewind when low on memory. clear rewind memory when disabled - ([4d5e1c4](https://github.com/lukexor/tetanes/commit/4d5e1c4dbe43cceb9ab8d4c33ca832830b2d31d8)) ### 🚜 Refactor - Removed a number of panic cases and cleaned up platform checks - ([bdb71a9](https://github.com/lukexor/tetanes/commit/bdb71a96792778cb0ad6bedf44e0ef5cbfa703e4)) - Add Sram trait and some mapper cleanup - ([ad03755](https://github.com/lukexor/tetanes/commit/ad0375506644f990e726c536f29bdf62d34d9e84)) ### 📚 Documentation - Fixed docs and changelog - ([4c7a694](https://github.com/lukexor/tetanes/commit/4c7a6949e52b6734fd6a78f6d9567c70e12b3ae4)) - Fixed docs - ([7a491c1](https://github.com/lukexor/tetanes/commit/7a491c14a2cb93db489c8bcb05d65f63bd1ed9d7)) ### 🧪 Testing - Update tests after ntsc change - ([f47f6c0](https://github.com/lukexor/tetanes/commit/f47f6c08ec2678c90e66b58d1297d20a6a72090b)) - Avoid serde_json::from_reader in tests as it's faster to just … ([#244](https://github.com/lukexor/tetanes/pull/244)) - ([3ca03ac](https://github.com/lukexor/tetanes/commit/3ca03ac68fab4d809dee39466fd661f887d2575d)) ## [0.10.0](https://github.com/lukexor/tetanes/compare/tetanes-v0.9.0..tetanes-core-v0.10.0) - 2024-05-16 Initial release. ================================================ FILE: tetanes-core/Cargo.toml ================================================ [package] name = "tetanes-core" version.workspace = true rust-version = "1.85.0" edition.workspace = true license.workspace = true description = "A NES Emulator written in Rust" authors.workspace = true readme = "README.md" repository.workspace = true homepage.workspace = true categories = ["emulators"] keywords = ["nes", "emulator"] [lib] crate-type = ["cdylib", "rlib"] [[bench]] name = "clock_frame" harness = false [lints] workspace = true [features] default = [] profiling = [] trace = [] [dependencies] bincode.workspace = true bitflags = { version = "2.10", features = ["serde"] } cfg-if.workspace = true dirs.workspace = true flate2 = "1.0" rand = "0.10" serde.workspace = true thiserror.workspace = true tracing.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] serde_json.workspace = true web-time.workspace = true web-sys = { workspace = true, features = ["Storage", "Window"] } [dev-dependencies] anyhow.workspace = true image.workspace = true serde_json.workspace = true tracing-subscriber.workspace = true ================================================ FILE: tetanes-core/README.md ================================================ # TetaNES Core [![Build Status]][build] [![Doc Status]][docs] [![Latest Version]][crates.io] [![Downloads]][crates.io] [![License]][gnu] [build status]: https://img.shields.io/github/actions/workflow/status/lukexor/tetanes/ci.yml?branch=main [build]: https://github.com/lukexor/tetanes/actions/workflows/ci.yml [doc status]: https://img.shields.io/docsrs/tetanes-core?style=plastic [docs]: https://docs.rs/tetanes-core/ [latest version]: https://img.shields.io/crates/v/tetanes-core?style=plastic [crates.io]: https://crates.io/crates/tetanes-core [downloads]: https://img.shields.io/crates/d/tetanes-core?style=plastic [license]: https://img.shields.io/crates/l/tetanes-core?style=plastic [gnu]: https://github.com/lukexor/tetanes/blob/main/LICENSE-MIT 📖 [Summary](#summary) - ✨ [Features](#features) - 🚧 [Building](#building) - 🚀 [Getting Started](#getting-started) - ⚠️ [Known Issues](#known-issues) - 💬 [Contact](#contact) ## Summary TetaNES > photo credit for background: [Zsolt Palatinus](https://unsplash.com/@sunitalap) > on [unsplash](https://unsplash.com/photos/pEK3AbP8wa4) This is the core emulation library for `TetaNES`. Savvy developers can build their own custom emulation libraries or applications in Rust on top of `tetanes-core`. Some community examples: - [NES Bundler](https://github.com/tedsteen/nes-bundler) - Transform your NES-game into a single executable targeting your favourite OS! - [Dappicom](https://github.com/tonk-gg/dappicom) - Dappicom is a provable Nintendo Entertainment System emulator written in Noir and Rust. - [NESBox](https://github.com/mantou132/nesbox/) - NESBox's vision is to become the preferred platform for people playing online multiplayer games, providing an excellent user experience for all its users. ## Minimum Supported Rust Version (MSRV) The current minimum Rust version is `1.85.0`. ## Features - NTSC, PAL and Dendy emulation. - Headless Mode. - Pixellate and NTSC filters. - Zapper (Light Gun) support. - iNES and NES 2.0 ROM header formats supported. - Over 30 supported mappers covering >90% of licensed games. - Game Genie Codes. - Preference snd keybonding menus using [egui](https://egui.rs). - Increase/Decrease speed & Fast Forward - Save & Load States - Battery-backed RAM saves ### Building To build the project, you'll need a nightly version of the compiler and run `cargo build` or `cargo build --release` (if you want better framerates). ### Getting Started Below is a basic example of setting up `tetanes_core` with a ROM and running the emulation. For a more in-depth example see the `tetanes::nes::emulation` module. ```rust no_run use tetanes_core::prelude::*; fn main() -> anyhow::Result<()> { let mut control_deck = ControlDeck::new(); // Load a ROM from the filesystem. // See also: `ControlDeck::load_rom` for loading anything that implements `Read`. control_deck.load_rom_path("some_awesome_game.nes")?; while control_deck.is_running() { // See also: `ControlDeck::clock_frame_output` and `ControlDeck::clock_frame_into` control_deck.clock_frame()?; let audio_samples = control_deck.audio_samples(); // Process audio samples (e.g. by sending it to an audio device) control_deck.clear_audio_samples(); let frame_buffer = control_deck.frame_buffer(); // Process frame buffer (e.g. by rendering it to the screen) // If not relying on vsync, sleep or otherwise wait the remainder of the // 16ms frame time to clock again } Ok(()) } ``` ## Stability The aim is for general stability, but the version isn't `1.0` yet and there are several large features on the roadmap that may result in breaking changes. This applies to both APIs and save file formats. Once some of these larger features are completed, and `1.0` is released, more effort will be dedicatged to versioning these files for backward compatibility in the event of future breaking changes. ## Known Issues See the [github issue tracker][]. ### Contact For issue reporting, please use the [github issue tracker][]. You can also contact me directly at . [github issue tracker]: https://github.com/lukexor/tetanes/issues ================================================ FILE: tetanes-core/benches/clock_frame.rs ================================================ #![allow(clippy::expect_used, reason = "fine in a benchmark")] use std::{ fs::File, hint::black_box, path::{Path, PathBuf}, time::Instant, }; use tetanes_core::prelude::*; fn main() { const FRAMES_TO_RUN: u32 = 400; const ITERATIONS: u32 = 30; let rom_path = std::env::args() .find(|arg| arg.ends_with(".nes")) .map(PathBuf::from) .map(|path| { if path.exists() { path } else { // The working directory of every benchmark is set to the root directory of // the package the benchmark belongs to. // // So if path is relative, it might be relative to the workspace not the package // the benchmark is in std::env::current_dir() .expect("valid cwd") .join("..") .join(path) } }) .unwrap_or_else(|| { let base_path = Path::new(env!("CARGO_MANIFEST_DIR")); base_path.join("test_roms/spritecans.nes") }); let rom_path = rom_path.canonicalize().expect("valid rom path"); let mut rom = File::open(&rom_path).expect("failed to open path"); let mut deck = ControlDeck::with_config(Config { ram_state: RamState::AllZeros, ..Default::default() }); deck.load_rom(rom_path.to_string_lossy(), &mut rom) .expect("failed to load rom"); // Warmup for _ in 0..3 { deck.reset(ResetKind::Hard); while deck.frame_number() < FRAMES_TO_RUN { black_box(deck.clock_frame()).expect("valid frame clock"); deck.clear_audio_samples(); } } let start = Instant::now(); for _ in 0..ITERATIONS { deck.reset(ResetKind::Hard); while deck.frame_number() < FRAMES_TO_RUN { black_box(deck.clock_frame()).expect("valid frame clock"); deck.clear_audio_samples(); } } let elapsed = start.elapsed().as_secs_f64(); let ms_per_frame = (elapsed / f64::from(FRAMES_TO_RUN * ITERATIONS)) * 1000.0; println!("=== RESULTS ==="); println!("{elapsed:.2} s total"); println!("{ms_per_frame:.3} ms/frame"); } ================================================ FILE: tetanes-core/game_database.txt ================================================ # CRC, Region, Mapper, Sub-Mapper, ChrBanks, PrgRomBanks, PrgRamBanks, Battery, Mirroring, SubMapper, Title 1388B3, PAL, 4, 0, 16, 16, 0, false, Vertical, "Mega Man 3 (Europe) (Rev A).nes" 21ED29, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Reigen Doushi (Japan).nes" 837960, NTSC, 4, 0, 32, 16, 0, true, Horizontal, "King's Quest V (USA).nes" 8E2D30, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Blodia Land - Puzzle Quest (Japan).nes" 9AF6BE, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Wheel of Fortune - Family Edition (USA).nes" A53242, NTSC, 113, 0, 16, 8, 0, false, Horizontal, "Fun Blaster Pak (Australia) (Unl).nes" AD1189, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Teenage Mutant Hero Turtles (Europe).nes" E95D86, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Jimmy Connors Tennis (USA).nes" 123BFFE, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Color a Dinosaur (USA).nes" 143EEB4, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Airball (Unknown) (Proto 1).nes" 15D4555, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Sunman (Europe) (Proto).nes" 16C93D8, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Salad no Kuni no Tomato Hime (Japan).nes" 18A8699, NTSC, 4, 0, 32, 8, 0, false, Vertical, "Roger Clemens' MVP Baseball (USA).nes" 1934171, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Terminator 2 - Judgment Day (USA) (Beta).nes" 1B4CA89, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "King's Knight (USA).nes" 22589B9, NTSC, 4, 0, 16, 16, 0, true, Horizontal, "Tecmo Super Bowl (Japan).nes" 23A5A32, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Gyromite (World).nes" 2589598, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Highway Star (Japan).nes" 26C1E1A, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Cosmos Cop (Asia) (Mega Soft) (Unl).nes" 26C5FCA, NTSC, 70, 0, 0, 8, 0, false, Horizontal, "Saint Seiya - Ougon Densetsu (Japan).nes" 26E41C5, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Mad Max (USA).nes" 28374F2, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Yamamura Misa Suspense - Kyouto Zaiteku Satsujin Jiken (Japan).nes" 2863604, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Sukeban Deka III (Japan).nes" 2B9E7C2, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Monster Party (USA).nes" 2C41438, NTSC, 177, 0, 0, 32, 0, true, Horizontal, "Xing He Zhan Shi (China) (Unl).nes" 2CC3973, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Ninja Kid (USA).nes" 2D7976B, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Penguin-kun Wars (Japan).nes" 2E0ADA4, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Totally Rad (Europe).nes" 2EE3706, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Strider (USA).nes" 3272E9B, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Krion Conquest, The (USA).nes" 354868A, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Family Trainer 1 - Athletic World (Japan).nes" 35DC2E9, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Pinball (World).nes" 37006F7, NTSC, 116, 0, 0, 16, 0, false, FourScreen, "Chuugoku Taitei (Asia) (Unl).nes" 39B4A9C, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Chou-Wakusei Senki - MetaFight (Japan).nes" 3B8DEFA, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Monopoly (France).nes" 3D56CF7, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Olympus no Tatakai (Japan).nes" 3E2898F, NTSC, 4, 0, 32, 16, 0, true, Horizontal, "Wario no Mori (Japan).nes" 3EC46AF, NTSC, 69, 0, 32, 8, 0, false, Horizontal, "Batman - Return of the Joker (USA).nes" 3F899CD, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Hunt for Red October, The (USA).nes" 3FB57B6, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Zombie Nation (USA).nes" 4109355, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Gekitotsu Yonku Battle (Japan).nes" 4142764, PAL, 4, 0, 16, 8, 0, false, Vertical, "Joe & Mac - Caveman Ninja (Europe).nes" 41553C3, NTSC, 168, 0, 0, 4, 0, true, Horizontal, "Racermate Challenge II (USA) (v3.12.027) (Unl).nes" 430DB08, PAL, 4, 0, 16, 8, 0, false, Vertical, "Zen - Intergalactic Ninja (Europe).nes" 45E8CD8, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Jigoku Gokuraku Maru (Japan).nes" 4766130, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Legend of the Ghost Lion (USA).nes" 49325D9, NTSC, 112, 0, 0, 8, 0, false, Vertical, "Huang Di (Asia) (Unl).nes" 504B007, PAL, 0, 0, 1, 1, 0, false, Vertical, "Donkey Kong Jr. Math (USA, Europe).nes" 5104517, PAL, 7, 0, 1, 8, 0, false, Horizontal, "Ivan 'Ironman' Stewart's Super Off Road (Europe).nes" 51CD5F2, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Pachi-Slot Adventure 3 - Bitaoshii 7 Kenzan! (Japan).nes" 537322A, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "NARC (USA).nes" 5378607, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Metal Mech - Man & Machine (USA).nes" 546BD12, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Fushigi no Umi no Nadia (Japan).nes" 54CB4EB, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Astyanax (USA).nes" 554394F, NTSC, 113, 0, 4, 2, 0, false, Vertical, "Metal Fighter (Asia) (Hacker) (Unl).nes" 58F23A2, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Image Fight (USA).nes" 59E0CDF, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Eliminator Boat Duel (USA).nes" 5A688C8, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Casino Kid (USA).nes" 5C4AF33, NTSC, 93, 0, 0, 8, 0, false, Vertical, "Shanghai (Japan) (Sample).nes" 5CE560C, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Legends of the Diamond - The Baseball Championship Game (USA).nes" 5F04EAC, NTSC, 82, 0, 0, 8, 0, true, Horizontal, "SD Keiji - Blader (Japan).nes" 6144B4A, NTSC, 77, 0, 0, 8, 0, false, FourScreen, "Napoleon Senki (Japan).nes" 63E5653, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Addams Family, The - Pugsley's Scavenger Hunt (USA).nes" 6406EB9, NTSC, 113, 0, 8, 8, 0, false, Horizontal, "Total Funpak (Australia) (Unl).nes" 6689AA4, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Wrath of the Black Manta (Europe).nes" 6961BE4, NTSC, 1, 0, 16, 16, 0, false, Horizontal, "Skate or Die 2 - The Search for Double Trouble (USA).nes" 6D72C83, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Fist of the North Star (USA).nes" 6F15215, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Thexder (Japan).nes" 6F9C714, NTSC, 18, 0, 16, 8, 0, false, Horizontal, "Ninja Jajamaru - Ginga Daisakusen (Japan).nes" 719260C, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Cadillac (Japan).nes" 71D4C2D, PAL, 4, 0, 32, 8, 0, false, Vertical, "WWF King of the Ring (Europe).nes" 73A0EBE, PAL, 1, 0, 16, 8, 0, false, Vertical, "P.O.W. - Prisoners of War (Europe).nes" 74EC424, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Arabian Dream Scheherazade (Japan).nes" 775DC68, NTSC, 150, 0, 0, 2, 0, false, Horizontal, "Taiwan Mahjong 2 (Asia) (Unl).nes" 78CED30, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Choujin - Ultra Baseball (Japan).nes" 7910BF9, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Family Quiz - 4-nin wa Rival (Japan).nes" 794F2A5, NTSC, 1, 0, 1, 32, 0, true, Horizontal, "Dragon Quest IV - Michibikareshi Monotachi (Japan) (Rev A).nes" 7977186, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Hiryuu no Ken Special - Fighting Wars (Japan).nes" 7D92C31, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "RPG Jinsei Game (Japan).nes" 83E4FC1, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Parodius (Europe).nes" 8439D55, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Special Tag Team Pro Wrestling (Japan).nes" 85DE7C9, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Die Hard (USA).nes" 897021B, NTSC, 69, 0, 32, 8, 0, false, Horizontal, "Gremlin 2 - Shinshu Tanjou (Japan).nes" 8E11357, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Super Dyna'mix Badminton (Japan).nes" 902C8F0, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Sanada Juu Yuushi (Japan).nes" 91ED5A9, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Kyuukyoku Tiger (Japan).nes" 92EC15C, NTSC, 3, 0, 2, 2, 0, false, Vertical, "Ikinari Musician (Japan).nes" 939852F, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "M.U.L.E. (USA).nes" 93E845F, NTSC, 1, 0, 4, 8, 0, true, Horizontal, "Artelius (Japan).nes" 955B54C, NTSC, 79, 0, 4, 2, 0, false, Horizontal, "Dudes with Attitude (USA) (Rev 1) (Unl).nes" 96D8364, PAL, 2, 0, 1, 8, 0, false, Vertical, "DuckTales 2 (Europe).nes" 973F714, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Jangou (Japan).nes" 9874777, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Marble Madness (USA).nes" 98C672A, NTSC, 19, 0, 32, 16, 0, true, Horizontal, "Sangokushi II - Haou no Tairiku (Japan).nes" 99B8CAA, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Mahjong RPG Dora Dora Dora (Japan).nes" 9C083B7, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Track & Field II (USA).nes" 9C1FC7D, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Mizushima Shinji no Daikoushien (Japan).nes" 9C31CD4, NTSC, 11, 0, 4, 2, 0, false, Horizontal, "Galactic Crusader (USA) (Unl).nes" 9FFDF45, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Super Momotarou Dentetsu (Japan).nes" A0926BD, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "MotorCity Patrol (USA).nes" A3FC393, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Lode Runner (Japan).nes" A42D84F, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Takeda Shingen (Japan).nes" A73A792, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Meitantei Holmes - M kara no Chousenjou (Japan).nes" A7E62D4, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "F-117A - Stealth Fighter (USA).nes" AB26DB6, NTSC, 11, 0, 16, 8, 0, false, Vertical, "Exodus - Journey to the Promised Land (USA) (v4.0) (Unl).nes" ABDD5CA, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Spot - The Video Game (Japan).nes" AC1AA8F, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Castlevania (USA).nes" ACFC3CD, NTSC, 173, 0, 0, 2, 0, false, Horizontal, "Mahjong Block (Unknown) (Unl).nes" AE3CC5E, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Pennant League, The - Home Run Nighter '90 (Japan).nes" AE6C9E2, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Castelian (USA).nes" AEA38F7, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Escape from Atlantis, The (USA) (Proto 2) (Unl).nes" AFB395E, NTSC, 5, 0, 16, 8, 0, false, Horizontal, "Gun Sight (Japan).nes" B0E128F, NTSC, 105, 0, 0, 16, 0, true, Horizontal, "Nintendo World Championships 1990 (USA).nes" B13658B, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Hyokkori Hyoutan-jima - Nazo no Kaizokusen (Japan).nes" B3513A0, NTSC, 3, 0, 1, 2, 0, false, Vertical, "Monstruo de los Globos, El (Spain) (Rev 1) (Gluk Video) (Unl).nes" B404915, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Captain Planet and the Planeteers (USA).nes" B58880C, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Ice Climber (Japan).nes" B6443D4, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "Star Wars - The Empire Strikes Back (Japan).nes" B8E8649, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Hiryuu no Ken - Ougi no Sho (Japan).nes" B8F8128, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Smash T.V. (Europe).nes" BB5B3A0, NTSC, 66, 0, 4, 8, 0, false, Vertical, "Family Block (Japan).nes" BBF80CB, NTSC, 82, 0, 0, 8, 0, false, Vertical, "Kyuukyoku Harikiri Stadium - Heisei Gannen Ban (Japan).nes" BCAA4D7, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "F-15 Strike Eagle (USA).nes" BDD8DD9, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Jeopardy! 25th Anniversary Edition (USA).nes" BE0A328, NTSC, 16, 0, 1, 16, 0, false, Horizontal, "Datach - SD Gundam - Gundam Wars (Japan).nes" C1792DA, NTSC, 19, 0, 16, 8, 0, false, Vertical, "Famista '90 (Japan).nes" C1FE23D, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Little Red Hood - Xiao Hong Mao (Asia) (Unl).nes" C2E7863, NTSC, 4, 0, 16, 16, 0, false, Vertical, "Dirty Harry (USA).nes" C401790, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Bomber Man II (Japan).nes" C462638, NTSC, 1, 0, 16, 8, 0, false, Vertical, "Dengeki - Big Bang! (Japan) (Beta).nes" C47946D, NTSC, 210, 1, 16, 8, 0, false, Vertical, "Chibi Maruko-chan - Uki Uki Shopping (Japan).nes" C5A6297, NTSC, 67, 0, 0, 8, 0, false, Vertical, "Fantasy Zone II - Opa-Opa no Namida (Japan).nes" C5F3973, PAL, 0, 0, 1, 2, 0, false, Horizontal, "Pro Action Replay (Europe) (v1.2) (Cart Present) (Unl).nes" C783F0C, PAL, 0, 0, 1, 1, 0, false, Horizontal, "Devil World (Europe).nes" C918A65, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Kekkyoku Nankyoku Daibouken (Japan) (Rev 1).nes" CC9FFEC, NTSC, 23, 0, 0, 8, 0, false, Horizontal, "Ganbare Goemon 2 (Japan).nes" CD79B71, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Cosmo Genesis (Japan).nes" CF42E69, NTSC, 159, 0, 16, 8, 0, false, Horizontal, "Magical Taruruuto-kun - Fantastic World!! (Japan).nes" D14285A, NTSC, 2, 0, 1, 8, 0, false, Vertical, "DuckTales 2 (Germany).nes" D15687D, NTSC, 0, 0, 32, 16, 0, false, Horizontal, "Super Cartridge Ver 7 - 4 in 1 (Asia) (Unl).nes" D203DE5, NTSC, 0, 0, 32, 16, 0, false, Horizontal, "Super Cartridge Ver 9 - 3 in 1 (Asia) (Unl).nes" D3482D7, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Seirei Densetsu Lickle (Japan).nes" D473EE6, NTSC, 186, 0, 0, 16, 0, false, Horizontal, "Study Box (Japan).nes" D65E7C7, NTSC, 69, 0, 16, 16, 0, false, Vertical, "Gimmick! (Japan).nes" D9F5BD1, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Maniac Mansion (USA).nes" DA00298, NTSC, 1, 0, 16, 16, 0, false, Horizontal, "Gozonji - Yaji Kita Chin Douchuu (Japan).nes" DA0E723, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Ski or Die (Europe).nes" DA28A50, PAL, 3, 0, 4, 2, 0, false, Horizontal, "Stadium Events (Europe).nes" DA5E32E, NTSC, 87, 0, 0, 2, 0, false, Vertical, "Urusei Yatsura - Lum no Wedding Bell (Japan).nes" DB4B382, NTSC, 18, 0, 16, 8, 0, false, Horizontal, "Plasma Ball (Japan).nes" DC53188, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Jesus - Kyoufu no Bio Monster (Japan).nes" E0060C8, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Happily Ever After (USA) (Proto).nes" E1683C5, NTSC, 80, 0, 0, 8, 0, false, Vertical, "Mirai Shinwa Jarvas (Japan).nes" E997CF6, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Takeda Shingen 2 (Japan).nes" EAA7515, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Yousei Monogatari - Rod Land (Japan).nes" EC6C023, NTSC, 5, 0, 32, 16, 0, true, Horizontal, "Gemfire (USA).nes" ED96F42, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Gremlins 2 - The New Batch (USA).nes" EF730E7, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Twin Cobra (USA).nes" F05FF0A, NTSC, 185, 0, 0, 2, 0, false, Vertical, "Seicross (Japan).nes" F1BABE7, NTSC, 18, 0, 16, 8, 0, true, Horizontal, "Jajamaru Gekimaden - Maboroshi no Kinmajou (Japan).nes" F1CC048, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Double Dragon (USA).nes" F20210D, NTSC, 241, 0, 0, 8, 0, false, Vertical, "Journey to the West (Asia) (Unl).nes" F3B89E3, NTSC, 0, 0, 1, 1, 0, false, Vertical, "F-1 Race (Japan) (Beta).nes" F5F1F86, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Zenbei Pro Basket (Japan).nes" F86FEB4, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Times of Lore (USA).nes" FC8E9B7, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "North & South - Wakuwaku Nanboku Sensou (Japan).nes" FCFC04D, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Mega Man 2 (USA).nes" FD6BFC8, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "Western Kids (Japan).nes" FEC90D2, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Section-Z (USA).nes" FF6A3B5, NTSC, 69, 0, 32, 8, 0, false, Horizontal, "Dynamite Batman (Japan).nes" FFDE258, NTSC, 4, 0, 8, 4, 0, false, Horizontal, "Fantasy Zone (USA) (Unl).nes" 1011394F, NTSC, 112, 0, 0, 8, 0, false, Horizontal, "Zhen Ben Xi You Ji (Asia) (Unl).nes" 10119E6B, NTSC, 93, 0, 0, 8, 0, false, Horizontal, "Fantasy Zone (Japan).nes" 10124E09, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Robodemons (USA) (Unl).nes" 10180072, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Flying Warriors (USA).nes" 1027C432, NTSC, 2, 0, 1, 16, 0, false, Vertical, "Momotarou Dentetsu (Japan).nes" 103E7E7F, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Willow (USA).nes" 1066B66D, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "SD Gundam - Gachapon Senshi 3 - Eiyuu Senki (Japan).nes" 10B0F8B0, NTSC, 80, 0, 0, 8, 0, false, Vertical, "Taito Grand Prix - Eikou e no License (Japan).nes" 10BAEEF3, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Son Son (Japan).nes" 10C06E27, NTSC, 1, 0, 1, 16, 0, false, Vertical, "Chuck Yeager's Fighter Combat (USA) (Proto).nes" 10C8F2FA, NTSC, 19, 0, 16, 8, 0, true, Horizontal, "Dokuganryuu Masamune (Japan).nes" 10C9A789, NTSC, 19, 0, 32, 16, 0, true, Horizontal, "Digital Devil Story - Megami Tensei II (Japan) (Rev A).nes" 10D62149, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Nangoku Shirei!! - Spy vs Spy (Japan).nes" 11C9AC37, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Princess Tomato in Salad Kingdom (USA) (Beta).nes" 11D08CC6, NTSC, 11, 0, 4, 2, 0, false, Vertical, "Metal Fighter (USA) (Unl).nes" 12078AFD, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Mendel Palace (USA).nes" 1208E754, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Matsumoto Tooru no Kabushiki Hisshou Gaku - Part II (Japan).nes" 12481CC0, NTSC, 4, 0, 16, 16, 0, false, Vertical, "Mega Man 3 (USA) (Beta).nes" 1248326D, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Akuma no Shoutaijou (Japan).nes" 126EBF66, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Bugs Bunny Birthday Blowout, The (USA).nes" 12748678, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Days of Thunder (USA).nes" 127D76F4, NTSC, 4, 0, 32, 32, 0, true, Horizontal, "Kirby's Adventure (Germany).nes" 12906664, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Castlequest (USA).nes" 12B2C361, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Who Framed Roger Rabbit (USA).nes" 12C6D5C7, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "1943 - The Battle of Midway (USA).nes" 12E6CB79, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Door Door (Japan).nes" 12F048DF, NTSC, 75, 0, 0, 8, 0, false, Vertical, "Jajamaru Ninpou Chou (Japan).nes" 1300A8B7, NTSC, 4, 0, 4, 4, 0, false, Horizontal, "Pro Yakyuu - Family Stadium '87 (Japan).nes" 1335CB05, NTSC, 4, 0, 16, 16, 0, true, Horizontal, "Crystalis (USA).nes" 1352F1B9, NTSC, 1, 0, 1, 8, 0, true, Horizontal, "Greg Norman's Golf Power (USA).nes" 1353A134, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Mission Impossible (Europe).nes" 136CA449, NTSC, 16, 0, 32, 16, 0, false, Horizontal, "Dragon Ball Z Gaiden - Saiya Jin Zetsumetsu Keikaku (Japan).nes" 138862C5, PAL, 2, 0, 1, 8, 0, false, Vertical, "WWF Wrestlemania Challenge (Europe).nes" 1394F57E, NTSC, 1, 0, 2, 2, 0, false, Horizontal, "Tetris (USA).nes" 139B15BA, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Golf '92, The (Japan).nes" 139EB5B5, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Indiana Jones and the Temple of Doom (USA) (Unl).nes" 13C6617E, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Batman - The Video Game (USA).nes" 13C774DD, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Double Dragon II - The Revenge (USA).nes" 13D5B1A4, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Time Lord (USA).nes" 13DA2122, NTSC, 88, 0, 16, 8, 0, false, Vertical, "Quinty (Japan).nes" 13E01649, PAL, 4, 0, 16, 8, 0, true, Horizontal, "Shadowgate (Europe).nes" 13E09D7A, NTSC, 4, 0, 16, 16, 0, true, FourScreen, "Dragon Wars (USA) (Proto).nes" 14105C13, NTSC, 11, 0, 16, 8, 0, false, Vertical, "Spiritual Warfare (USA) (v6.1) (Unl).nes" 1411005B, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Sugoro Quest - Dice no Senshitachi (Japan).nes" 14255C57, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Corvette ZR-1 Challenge (Europe).nes" 1425D7F4, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Arkista's Ring (USA).nes" 14374128, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Hiryuu no Ken Special - Fighting Wars (Japan) (Beta).nes" 145A9A6C, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Devil World (Japan) (Rev A).nes" 1488E95F, PAL, 0, 0, 8, 4, 0, false, Vertical, "Silent Assault (Asia) (PAL) (Unl).nes" 149C0EC3, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Famicom Shougi - Ryuuousen (Japan).nes" 14A81635, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Moon Ranger (USA) (Unl).nes" 14CD576E, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Abadox (Japan).nes" 14F477C3, NTSC, 66, 0, 4, 4, 0, false, Horizontal, "AV Mahjong Club (Asia) (Unl).nes" 1500E835, NTSC, 33, 0, 0, 8, 0, false, Horizontal, "Jetsons, The - Cogswell's Caper (Japan).nes" 15141401, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Asmik-kun Land (Japan).nes" 15454CB2, NTSC, 143, 0, 0, 2, 0, false, Vertical, "Magical Mathematics (Asia) (NTSC) (Unl).nes" 1545BD13, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Gimmi a Break - Shijou Saikyou no Quiz Ou Ketteisen 2 (Japan).nes" 1570A0C8, NTSC, 189, 0, 0, 8, 0, false, Horizontal, "Gluk the Thunder Warrior (Spain) (Gluk Video) (Unl).nes" 1590CF62, PAL, 4, 0, 16, 8, 0, true, Horizontal, "Capcom's Gold Medal Challenge '92 (Europe).nes" 15A1CBB0, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Shatterhand (USA) (Beta).nes" 15F0D3F1, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Wayne Gretzky Hockey (USA).nes" 15FE6D0F, NTSC, 5, 0, 16, 16, 0, true, Horizontal, "Bandit Kings of Ancient China (USA).nes" 161D717B, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Bad Dudes (USA).nes" 162CCBD0, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Toukyou Pachi-Slot Adventure (Japan) (Rev A).nes" 162F328E, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Sansuu 4 Nen - Keisan Game (Japan) (Beta).nes" 163ECCAE, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Kekkyoku Nankyoku Daibouken (Japan).nes" 1675A6C1, NTSC, 4, 0, 32, 8, 0, false, Vertical, "War on Wheels (USA) (Proto).nes" 1677D21D, PAL, 1, 0, 16, 8, 0, false, Vertical, "Nigel Mansell's World Championship Racing (Europe) (En,Fr,De,Es,It).nes" 16A0A3A3, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Hitler no Fukkatsu - Top Secret (Japan).nes" 16E93F39, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Tashiro Masashi no Princess ga Ippai (Japan).nes" 16EBA50A, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "Star Trek - 25th Anniversary (USA).nes" 171251E3, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "1942 (Japan, USA).nes" 17389E3D, PAL, 1, 0, 16, 8, 0, false, Vertical, "RoadBlasters (Europe).nes" 174F860A, NTSC, 1, 0, 1, 8, 0, true, Horizontal, "Indora no Hikari (Japan).nes" 175C4A3C, NTSC, 75, 0, 0, 8, 0, false, Horizontal, "Moero!! Junior Basket - Two on Two (Japan).nes" 175EDA0B, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Great Boxing - Rush Up (Japan).nes" 1771EA8F, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Athletic World (USA).nes" 1773F76D, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Flipull - An Exciting Cube Game (Japan) (En).nes" 178DBA78, PAL, 0, 0, 1, 2, 0, false, Vertical, "Pro Action Replay (Europe) (v1.0) (Unl).nes" 179A0D57, NTSC, 4, 0, 16, 16, 0, true, Horizontal, "Tecmo Super Bowl (USA).nes" 18027A1F, PAL, 2, 0, 1, 8, 0, false, Vertical, "Phantom Air Mission (Europe).nes" 1829616A, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Gorby no Pipeline Daisakusen (Japan).nes" 183859D2, NTSC, 16, 0, 32, 16, 0, false, Horizontal, "Dragon Ball Z - Kyoushuu! Saiya Jin (Japan).nes" 184C2124, NTSC, 5, 0, 32, 16, 0, true, Horizontal, "Sangokushi II (Japan).nes" 18A04825, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Terminator 2 - Judgment Day (Europe).nes" 18A2E74F, NTSC, 4, 0, 1, 32, 0, false, Horizontal, "Mega Man 4 (USA) (Rev A).nes" 18A885B0, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "2010 - Street Fighter (Japan).nes" 18A9F0D9, NTSC, 4, 0, 16, 16, 0, true, Horizontal, "Baseball Stars II (USA).nes" 18B249E5, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Barbie (USA) (Rev A).nes" 18D44BBA, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Isaki Shuugorou no Keiba Hisshou Gaku (Japan).nes" 190E52FF, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Bio Force Ape (Japan) (En) (Proto).nes" 192D546F, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "RoboCop (USA).nes" 1948810E, NTSC, 11, 0, 16, 8, 0, false, Vertical, "Spiritual Warfare (USA) (v5.1) (Unl).nes" 1973AEA8, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "American Gladiators (USA).nes" 198C2F41, NTSC, 1, 0, 4, 2, 0, false, Horizontal, "Dr. Mario (Japan, USA).nes" 198F1C37, NTSC, 0, 0, 4, 4, 0, false, Horizontal, "Super Pang II (Asia) (Unl).nes" 1992D163, NTSC, 4, 0, 4, 2, 0, false, Horizontal, "Adventures of Lolo 2 (USA).nes" 19CE7F12, PAL, 0, 0, 1, 2, 0, false, Vertical, "Magical Mathematics (Asia) (PAL) (Unl).nes" 19E81461, NTSC, 16, 0, 1, 16, 0, false, Horizontal, "Datach - Dragon Ball Z - Gekitou Tenkaichi Budoukai (Japan).nes" 1A018A26, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Huge Insect (Asia) (Unl).nes" 1A2A7EF7, NTSC, 178, 0, 0, 32, 0, true, Vertical, "San Guo Zhong Lie Zhuan (China) (Unl).nes" 1A2EA6B9, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Super Pinball (Japan).nes" 1A7E97ED, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Meitantei Holmes - Kiri no London Satsujin Jiken (Japan).nes" 1AC701B5, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Tenchi o Kurau (Japan).nes" 1AE7B933, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Bad Street Brawler (USA).nes" 1B421E9C, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Star Soldier (Japan).nes" 1B71CCDB, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Gauntlet II (USA).nes" 1B7BD879, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Fushigi na Blobby - Blobania no Kiki (Japan).nes" 1B932BEA, PAL, 4, 0, 1, 32, 0, false, Horizontal, "Mega Man 4 (Europe).nes" 1BC686A8, NTSC, 71, 0, 1, 8, 0, false, Horizontal, "Fire Hawk (USA) (Unl).nes" 1C212E9D, PAL, 119, 0, 0, 8, 0, false, Horizontal, "High Speed (Europe).nes" 1C2A58FF, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Nakajima Satoru Kanshuu - F-1 Hero 2 (Japan).nes" 1C31DD60, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Exploding Fist (USA) (Proto 2).nes" 1C66BAF6, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Super Pitfall (Japan).nes" 1C9EA55C, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Three Stooges, The (USA) (Beta).nes" 1CB9A019, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Hit the Ice - VHL - The Video Hockey League (USA) (Proto).nes" 1CED086F, NTSC, 5, 0, 16, 16, 0, true, Horizontal, "Ishin no Arashi (Japan).nes" 1CEE0C21, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Digger - The Legend of the Lost City (USA).nes" 1CF48EF1, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Shuang Xiang Pao (Asia) (Unl).nes" 1D0F4D6B, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Black Bass, The (USA).nes" 1D20A5C6, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Galaxy 5000 (USA).nes" 1D2D93FF, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "G.I. Joe - A Real American Hero (USA).nes" 1D41CC8C, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Gyruss (USA).nes" 1D5B03A5, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Jackal (USA).nes" 1D6DECCC, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Rocketeer, The (USA).nes" 1D89610E, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Great Battle Cyber (Japan).nes" 1D8BF724, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Venus Senki - Back the City (Japan).nes" 1DAC6208, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Snow Brothers (USA).nes" 1DB07C0D, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Galaga - Demons of Death (USA).nes" 1DBD1D2B, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Geimos (Japan).nes" 1DC0F740, NTSC, 210, 2, 16, 16, 0, false, Vertical, "Wagyan Land 2 (Japan).nes" 1E407387, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Baltron (Japan) (Beta).nes" 1E472E7A, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Advanced Dungeons & Dragons - Heroes of the Lance (Japan).nes" 1E4D3831, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Yoshi no Tamago (Japan).nes" 1EB4A920, NTSC, 79, 0, 4, 2, 0, false, Vertical, "Double Strike - Aerial Attack Force (USA) (v1.1) (Unl).nes" 1EBB5B42, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Bomberman II (USA).nes" 1ED48C5C, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Dash Yarou (Japan).nes" 1ED5C801, PAL, 4, 0, 16, 16, 0, false, Horizontal, "Super Mario Bros. 3 (Europe).nes" 1ED7D6BE, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Yamamura Misa Suspense - Kyouto Hana no Misshitsu Satsujin Jiken (Japan).nes" 1EFE38EB, PAL, 7, 0, 1, 8, 0, false, Horizontal, "Captain Skyhawk (Europe).nes" 1F2D9DB7, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Baltron (Japan).nes" 1F6660E6, PAL, 1, 0, 16, 4, 0, false, Horizontal, "Barker Bill's Trick Shooting (Europe).nes" 1F6EA423, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Baseball Simulator 1.000 (USA).nes" 1F74EA6C, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Arctic (Japan).nes" 1FA8C4A4, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Superman (USA).nes" 1FF251AE, NTSC, 1, 0, 16, 8, 0, false, Vertical, "Teenage Mutant Ninja Turtles (USA) (Beta).nes" 20353E63, NTSC, 1, 0, 8, 8, 0, false, Horizontal, "Fox's Peter Pan & the Pirates - The Revenge of Captain Hook (USA).nes" 203583D5, NTSC, 23, 0, 0, 8, 0, false, Vertical, "TwinBee 3 - Poko Poko Daimaou (Japan) (Beta).nes" 2055971A, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Mario Is Missing! (USA).nes" 2061772A, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Tantei Jinguuji Saburou - Toki no Sugiyuku Mama ni... (Japan).nes" 209B4BED, NTSC, 26, 0, 16, 16, 0, true, Horizontal, "Esper Dream 2 - Aratanaru Tatakai (Japan).nes" 209F3587, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Untouchables, The (USA).nes" 20A5219B, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Conquest of the Crystal Palace (USA).nes" 20AF7E1A, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Chiki Chiki Machine Mou Race (Japan).nes" 20C5D187, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Utsurun Desu (Japan).nes" 20C795EB, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Untouchables, The (USA) (Rev B).nes" 20CC079D, NTSC, 4, 0, 16, 16, 0, true, Horizontal, "Mother (Japan).nes" 20F98977, NTSC, 87, 0, 0, 1, 0, false, Vertical, "City Connection (Japan).nes" 213CB3FB, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "U.S. Championship V'Ball (Japan).nes" 219DFABF, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Ninja-kun - Ashura no Shou (Japan).nes" 21DD2174, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Haja no Fuuin (Japan).nes" 21E28F50, NTSC, 1, 0, 16, 8, 0, false, Vertical, "Parody World - Monster Party (Japan) (Proto).nes" 21F2A1A6, PAL, 4, 0, 32, 8, 0, false, Vertical, "WWF Wrestlemania Steel Cage Challenge (Europe).nes" 21F85681, NTSC, 87, 0, 0, 2, 0, false, Vertical, "Hyper Olympic (Japan) (Genteiban!).nes" 21F8C4AB, NTSC, 89, 0, 0, 8, 0, false, Vertical, "Tenka no Goikenban - Mito Koumon (Japan).nes" 2220E14A, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Space Shuttle Project (USA).nes" 2225C20F, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Genghis Khan (USA).nes" 22276213, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Menace Beach (USA) (Unl).nes" 227CF577, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Adventures of Rocky and Bullwinkle and Friends, The (USA).nes" 22AB9694, PAL, 2, 0, 1, 8, 0, false, Vertical, "Rod Land (Europe).nes" 22D6D5BD, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Jikuu Yuuden - Debias (Japan).nes" 231BC76E, NTSC, 11, 0, 4, 2, 0, false, Horizontal, "Chiller (Australia) (Unl).nes" 2328046E, NTSC, 7, 0, 1, 16, 0, false, Horizontal, "IronSword - Wizards & Warriors II (USA).nes" 2337F45E, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Hokuto no Ken (Japan) (Beta).nes" 2370C0A9, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Rollerblade Racer (USA).nes" 2394AE1C, PAL, 243, 0, 0, 2, 0, false, Horizontal, "Happy Pairs (Asia) (PAL) (Unl).nes" 239971D1, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Zhuang Qiu Chuan Shuo Hua Zhuang II - Ball Story (China) (Unl).nes" 23BEFF5E, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Parasol Stars - The Story of Bubble Bobble III (Europe) (Beta).nes" 23BF0507, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Dengeki - Big Bang! (Japan).nes" 23C3FB2D, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Dungeon Magic - Sword of the Elements (USA).nes" 23D17F5E, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Lone Ranger, The (USA).nes" 23D7D48F, PAL, 7, 0, 1, 16, 0, false, Horizontal, "Battletoads-Double Dragon (Europe).nes" 23D91BC6, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Mahjong (Japan) (Rev B).nes" 23E03DC1, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Dong Dong Nao 1 (Asia) (Unl).nes" 23E9C736, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Shin Satomi Hakken-Den - Hikari to Yami no Tatakai (Japan).nes" 23F38647, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Zunou Senkan Galg (Japan) (En) (Beta).nes" 23F4B48F, NTSC, 4, 0, 1, 16, 0, false, Horizontal, "Wily & Right no Rockboard - That's Paradise (Japan).nes" 240863B9, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Airball (Unknown) (Proto 2).nes" 240C6DE8, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Elysion (Japan).nes" 240DE736, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "Star Wars - The Empire Strikes Back (USA).nes" 243A8735, NTSC, 32, 0, 0, 8, 0, false, Horizontal, "Major League (Japan).nes" 2447E03B, NTSC, 19, 0, 16, 8, 0, false, Vertical, "Top Striker (Japan).nes" 24598791, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Duck Hunt (World).nes" 2470402B, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Famicom Igo Nyuumon (Japan).nes" 2472C3EB, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Pyramid (USA) (Rev 1) (Unl).nes" 247CC73D, NTSC, 243, 0, 0, 2, 0, false, Horizontal, "Poker II (Asia) (Unl).nes" 248566A7, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Total Recall (USA).nes" 24BA12DD, NTSC, 71, 0, 1, 16, 0, false, Vertical, "Micro Machines (USA) (Aladdin Compact Cartridge) (Unl).nes" 24BA90CA, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Cobra Command (Japan).nes" 24EECC15, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Sesame Street ABC & 123 (USA).nes" 250F7913, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Ultima - Exodus (Japan).nes" 2526C943, NTSC, 66, 0, 4, 8, 0, false, Vertical, "Takahashi Meijin no Bugutte Honey (Japan).nes" 252FFD12, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Sweet Home (Japan).nes" 2538D860, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Pinball Quest (USA).nes" 2545214C, NTSC, 1, 0, 2, 4, 0, true, Horizontal, "Dragon Warrior (USA) (Rev A).nes" 25468546, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Gegege no Kitarou - Youkai Daimakyou (Japan).nes" 25519E6E, NTSC, 3, 0, 8, 2, 0, false, Vertical, "Pyramid (Japan) (Hacker inc.) (Unl).nes" 25551F3F, PAL, 9, 0, 16, 8, 0, false, Horizontal, "Mike Tyson's Punch-Out!! (Europe) (Rev A).nes" 256392F1, PAL, 4, 0, 16, 8, 0, true, Horizontal, "Formula 1 Sensation (Europe).nes" 25952141, NTSC, 4, 0, 16, 32, 0, true, Horizontal, "Advanced Dungeons & Dragons - Pool of Radiance (USA).nes" 25EDAF5C, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Okkotoshi Puzzle - Tonjan! (Japan).nes" 26049798, NTSC, 4, 0, 32, 16, 0, true, Horizontal, "My Life My Love - Boku no Yume - Watashi no Negai (Japan).nes" 262B5A1D, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Star Soldier (USA).nes" 262F31AC, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Zelda II - The Adventure of Link (USA) (GameCube Edition).nes" 263AC8A0, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Rampage (USA).nes" 264F26B1, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Oishinbo - Kyuukyoku no Menu Sanbon Shoubu (Japan).nes" 2651F227, NTSC, 4, 0, 32, 8, 0, true, Horizontal, "Tecmo NBA Basketball (USA).nes" 26535EF5, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Wizards & Warriors (USA) (Rev A).nes" 26796758, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Rampart (USA).nes" 267DE4CC, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Fisher-Price - I Can Remember (USA).nes" 267E592F, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Karnov (Japan).nes" 268E39D0, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Tantei Jinguuji Saburou - Yokohamakou Renzoku Satsujin Jiken (Japan).nes" 26BB1C8C, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Hello Kitty no Ohanabatake (Japan).nes" 26BD6EC6, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Star Luster (Japan).nes" 26BFED27, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Super Chinese 2 - Dragon Kid (Japan).nes" 26CEC726, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Kaguya Hime Densetsu (Japan).nes" 26D3082C, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Joe & Mac (USA).nes" 26E39935, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "Moon Crystal (Japan).nes" 26E82008, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Bucky O'Hare (Japan).nes" 2705EAEB, NTSC, 234, 0, 0, 32, 0, false, Horizontal, "Maxi 15 (USA) (Unl).nes" 270EAED5, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Onyanko Town (Japan).nes" 2746B39E, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Tetris Flash (Japan).nes" 276237B3, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Karnov (Japan) (Rev 1).nes" 276AC722, NTSC, 159, 0, 16, 16, 0, false, Horizontal, "SD Gundam Gaiden - Knight Gundam Monogatari (Japan) (Rev 1).nes" 27738241, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Popeye no Eigo Asobi (Japan).nes" 27777635, PAL, 0, 0, 1, 2, 0, false, Vertical, "Volleyball (USA, Europe).nes" 279710DC, NTSC, 7, 0, 1, 16, 0, false, Horizontal, "Battletoads (USA).nes" 27AA3933, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Seicross (USA).nes" 27C16011, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Baken Hisshou Gaku - Gate In (Japan).nes" 27CA0679, PAL, 7, 0, 1, 8, 0, false, Vertical, "Danny Sullivan's Indy Heat (Europe).nes" 27D14A54, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Jaws (USA).nes" 27D34A57, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Golgo 13 - Dainishou - Icarus no Nazo (Japan).nes" 27DDF227, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Athena (USA).nes" 27F8D0D2, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Punisher, The (USA).nes" 280AD3C5, PAL, 1, 0, 16, 4, 0, false, Vertical, "Knight Rider (Europe).nes" 282745C5, NTSC, 191, 0, 0, 16, 0, false, Horizontal, "Q Boy (Asia) (Unl).nes" 283AD224, NTSC, 32, 0, 0, 16, 0, false, Horizontal, "Ai Sensei no Oshiete - Watashi no Hoshi (Japan).nes" 28492586, PAL, 4, 0, 4, 2, 0, false, Horizontal, "Burai Fighter (Europe).nes" 2856111F, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Overlord (USA).nes" 2858933B, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Satsui no Kaisou - Soft House Renzoku Satsujin Jiken (Japan).nes" 286FCD20, NTSC, 21, 0, 0, 16, 0, true, Horizontal, "Ganbare Goemon Gaiden 2 - Tenka no Zaihou (Japan).nes" 28C1D3D5, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Kamen no Ninja - Akakage (Japan).nes" 28C2DFCE, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Die Hard (Japan).nes" 28F9B41F, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Bases Loaded 4 (USA).nes" 28FB71AE, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Defender of the Crown (USA).nes" 2915FAF0, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Incantation (Asia) (Unl).nes" 291BCD7D, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Pachio-kun 2 (Japan).nes" 29582CA1, NTSC, 243, 0, 0, 2, 0, false, Horizontal, "Honey Peach - Mei Nv Quan (Asia) (Unl).nes" 2969A5C1, NTSC, 79, 0, 1, 1, 0, false, Horizontal, "Pyramid (USA) (Unl).nes" 297198B9, NTSC, 4, 0, 4, 2, 0, false, Horizontal, "Adventures of Lolo (Japan).nes" 29DD37F4, NTSC, 69, 0, 32, 8, 0, false, Vertical, "Batman - Return of the Joker (USA) (Beta).nes" 29DE87AF, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Family Trainer 3 - Aerobics Studio (Japan).nes" 29E173FF, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Gyrodine (Japan).nes" 29EC0FD1, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "1999 - Hore, Mitakotoka! Seikimatsu (Japan).nes" 2A01F9D1, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Wagyan Land (Japan).nes" 2A1919FE, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Kamen Rider SD - Granshocker no Yabou (Japan).nes" 2A3CA509, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Downtown - Nekketsu Monogatari (Japan).nes" 2A46B57F, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Terra Cresta (USA).nes" 2A5F4C5A, NTSC, 132, 0, 0, 2, 0, false, Vertical, "Jin Gwok Sei Chuen Saang (Asia) (Unl).nes" 2A6559A1, NTSC, 33, 0, 0, 8, 0, false, Horizontal, "Operation Wolf (Japan).nes" 2A662AC7, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Jeopardy! (USA) (Rev A).nes" 2A7D3ADF, NTSC, 19, 0, 16, 8, 0, false, Vertical, "Dragon Ninja (Japan).nes" 2AAF0804, NTSC, 11, 0, 16, 8, 0, false, Vertical, "Spiritual Warfare (USA) (Beta) (Unl).nes" 2AC5233C, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Hinotori - Houou Hen - Gaou no Bouken (Japan).nes" 2AC87283, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Elevator Action (USA).nes" 2AE535CA, NTSC, 19, 0, 16, 8, 0, false, Vertical, "Dragon Ninja (Japan) (Rev A).nes" 2AE97660, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Ghostbusters II (USA).nes" 2B11E0B0, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Nobunaga no Yabou - Zenkoku Ban (Japan) (Rev A).nes" 2B1497DC, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Shadowgate (Sweden).nes" 2B160BF0, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Mighty Final Fight (Japan).nes" 2B20B022, PAL, 4, 0, 32, 8, 0, false, Horizontal, "Gremlins 2 - The New Batch (Europe).nes" 2B20ED9B, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Shui Guo Li (Asia) (Unl).nes" 2B378D11, NTSC, 7, 0, 1, 16, 0, false, Horizontal, "Double Dare (USA).nes" 2B462010, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Balloon Fight (Japan).nes" 2B4D80AE, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Battle Formula (Japan).nes" 2B750BF9, NTSC, 87, 0, 0, 2, 0, false, Vertical, "Urusei Yatsura - Lum no Wedding Bell (Japan) (Beta).nes" 2BA86F76, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Gran Aventura Submarina, La (Spain) (Gluk Video) (Unl).nes" 2BB3DABE, NTSC, 82, 0, 0, 8, 0, true, Horizontal, "Kyuukyoku Harikiri Stadium III (Japan).nes" 2BB6A0F8, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Sherlock Holmes - Hakushaku Reijou Yuukai Jiken (Japan).nes" 2BC25D5A, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Ghoul School (USA).nes" 2BC67AA8, NTSC, 4, 0, 1, 32, 0, false, Horizontal, "Mega Man 4 (USA).nes" 2BCF2132, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Heavy Barrel (Japan).nes" 2BE254E9, NTSC, 64, 0, 0, 2, 0, false, Vertical, "Dig Dug II (Japan).nes" 2BF0F9C5, PAL, 0, 0, 1, 1, 0, false, Horizontal, "Mario Bros. (Europe) (Rev A).nes" 2BF61C53, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Jetsons, The - Cogswell's Caper (USA).nes" 2BFB1186, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Be-Bop-Highschool - Koukousei Gokuraku Densetsu (Japan).nes" 2C043781, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Paperboy (Japan).nes" 2C088DC5, PAL, 4, 0, 32, 32, 0, true, Horizontal, "Kirby's Adventure (Europe).nes" 2C2DDFB4, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Battle Chess (USA).nes" 2C33161D, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Advanced Dungeons & Dragons - Hillsfar (Japan).nes" 2C4421B2, NTSC, 16, 0, 16, 8, 0, false, Horizontal, "Akuma-kun - Makai no Wana (Japan).nes" 2C5908A7, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "Advanced Dungeons & Dragons - DragonStrike (USA).nes" 2C5FAC1C, NTSC, 1, 0, 16, 8, 0, true, Vertical, "Famicom Shougi - Ryuuousen (Japan) (Beta).nes" 2C624B5F, NTSC, 64, 0, 0, 8, 0, false, Vertical, "Xybots (USA) (Proto) (Unl).nes" 2C7D68F3, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Pac-Man (Japan) (En) (Rev B).nes" 2C818014, NTSC, 9, 0, 16, 8, 0, false, Horizontal, "Mike Tyson's Punch-Out!! (Japan, USA) (Rev A).nes" 2CAAE01C, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Felix the Cat (USA).nes" 2CECD630, NTSC, 36, 0, 0, 4, 0, false, Vertical, "Policeman (Spain) (Gluk Video) (Unl).nes" 2CF5DB05, NTSC, 176, 0, 0, 8, 0, false, Horizontal, "Zhi Li Xiao Zhuang Yuan (China) (Unl).nes" 2D020965, PAL, 1, 0, 1, 16, 0, true, Horizontal, "NES Open Tournament Golf (Europe).nes" 2D1FEE70, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Magician (USA) (Beta 2).nes" 2D273AA4, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Ikari Warriors (USA) (Rev A).nes" 2D2F91B8, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Mahou no Princess Minky Momo - Remember Dream (Japan).nes" 2D41EF92, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Uncanny X-Men, The (USA).nes" 2D664D99, NTSC, 4, 0, 1, 32, 0, false, Horizontal, "Rockman 6 - Shijou Saidai no Tatakai!! (Japan).nes" 2D75C7A9, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Cobra Command (USA).nes" 2D8730E2, NTSC, 0, 0, 4, 2, 0, false, Horizontal, "Poker Mahjong - Pu Ke Mao Que (Asia) (Unl).nes" 2DB7C31E, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Hook (Japan).nes" 2DBB054D, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "HVC Kensa Cassette Controller Test (Japan).nes" 2DC05A6F, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Astro Robo Sasa (Japan).nes" 2DC331A2, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "New York Nyankies (Japan).nes" 2DD71ACB, NTSC, 1, 0, 1, 32, 0, true, Horizontal, "Dragon Quest IV - Michibikareshi Monotachi (Japan).nes" 2DDC2DC3, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Thunderbirds (USA).nes" 2DEB12B8, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Venice Beach Volleyball (Asia) (Unl).nes" 2DFF7FDC, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Great Waldo Search, The (USA).nes" 2E0741B6, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Home Alone 2 - Lost in New York (USA).nes" 2E0F51AF, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Nintendo - NTF2 Test Cartridge (NES Test) (USA) (Rev 1).nes" 2E1E7FD8, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Dead Fox (Japan).nes" 2E2ACAE9, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Gambler Jiko Chuushinha - Mahjong Game (Japan).nes" 2E326A1D, NTSC, 4, 0, 4, 4, 0, false, Vertical, "R.B.I. Baseball (USA) (Unl).nes" 2E4CCF46, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Game Genie (USA) (Unl).nes" 2E563C66, NTSC, 4, 0, 4, 8, 0, false, Horizontal, "Mappy-Land (Japan).nes" 2E6301ED, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Super Mario Bros. 3 (USA) (Rev A).nes" 2E68ACFC, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Tiger-Heli (Japan).nes" 2E6EE98D, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Harlem Globetrotters (USA).nes" 2EA8CC16, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Indiana Jones and the Last Crusade (USA) (UBI Soft).nes" 2F128512, NTSC, 3, 0, 16, 2, 0, false, Vertical, "Family Trainer 4 - Jogging Race (Japan).nes" 2F1686E5, NTSC, 0, 0, 8, 8, 0, false, Horizontal, "Super Cartridge Ver 2 - 10 in 1 (Asia) (Unl).nes" 2F2D1FA9, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Castle of Dragon (USA).nes" 2F2E30F7, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Ninja Ryuuken Den III - Yomi no Hakobune (Japan).nes" 2F52BBE0, NTSC, 148, 0, 0, 2, 0, false, Horizontal, "Mahjong Trap - Si Cuan Ma Que - Zhi Fu Pian (Asia) (Unl).nes" 2F55BE88, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Lunar Ball (Japan).nes" 2F66E302, NTSC, 2, 0, 1, 8, 0, false, Vertical, "California Games (USA).nes" 2F698C4D, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Monster Truck Rally (USA).nes" 2FA8CBB4, NTSC, 79, 0, 4, 2, 0, false, Horizontal, "Puzzle (USA) (Beta) (Unl).nes" 2FBEA66D, NTSC, 79, 0, 4, 2, 0, false, Horizontal, "F15 City War (Spain) (Gluk Video) (Unl).nes" 2FC1ABAE, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Hudson Hawk (Japan).nes" 2FD2E632, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Defender of the Crown (France).nes" 2FE20D79, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Flintstones, The - The Rescue of Dino & Hoppy (USA).nes" 2FFDE228, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Times of Lore (Japan).nes" 303D4371, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Jordan vs Bird - One On One (USA).nes" 304FA926, PAL, 0, 0, 1, 1, 0, false, Horizontal, "Tennis (Europe).nes" 3057B904, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Pennant League!! - Home Run Nighter (Japan).nes" 305B4E62, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Super C (USA).nes" 308DA987, PAL, 7, 0, 1, 16, 0, false, Horizontal, "R.C. Pro-Am II (Europe).nes" 30A225A8, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Columbus - Ougon no Yoake (Japan).nes" 30BF2DBA, NTSC, 86, 0, 0, 8, 0, false, Vertical, "Moero!! Pro Yakyuu (Japan).nes" 30C5E6CF, NTSC, 2, 0, 1, 8, 0, false, Vertical, "DuckTales 2 (France).nes" 31957AE4, NTSC, 1, 0, 4, 2, 0, false, Horizontal, "Palamedes II - Star Twinkle, Hoshi no Mabataki (Japan).nes" 31B44C65, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Captain Tsubasa Vol. II - Super Striker (Japan).nes" 31C7AD13, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Shounen Ashibe - Nepal Daibouken no Maki (Japan).nes" 32086826, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Paperboy (USA).nes" 322C9F6A, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Destructor, El (Spain) (Gluk Video) (Unl).nes" 3256114C, NTSC, 4, 0, 1, 32, 0, false, Horizontal, "America Oudan Ultra Quiz - Shijou Saidai no Tatakai (Japan).nes" 326AB3B6, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "8 Eyes (USA).nes" 3275FD7E, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Hottarman no Chitei Tanken (Japan).nes" 3293AFEA, NTSC, 66, 0, 4, 8, 0, false, Vertical, "Mississippi Satsujin Jiken (Japan) (Rev A).nes" 329C0349, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Nintendo - NTF2 Test Cartridge (USA).nes" 32CF4307, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Conflict (USA).nes" 32E02CB8, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Nintendo World Class Service - Port Test Cartridge (USA).nes" 32FA246F, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Tag Team Pro-Wrestling (Japan).nes" 32FB0583, NTSC, 3, 0, 2, 2, 0, false, Horizontal, "Arkanoid (USA).nes" 33007B67, NTSC, 177, 0, 0, 64, 0, true, Vertical, "Mei Guo Fu Hao - American Man (China) (Unl).nes" 330DE468, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Obake no Q Tarou - Wanwan Panic (Japan).nes" 3322105A, NTSC, 1, 0, 16, 2, 0, false, Horizontal, "Sky Kid (USA).nes" 332C47E0, NTSC, 4, 0, 16, 16, 0, true, Horizontal, "Radia Senki - Reimei Hen (Japan).nes" 333C48A0, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Werewolf - The Last Warrior (USA).nes" 336093EF, NTSC, 66, 0, 4, 8, 0, false, Vertical, "Doraemon (Japan) (Rev A).nes" 3368F7FB, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Wheel of Fortune (USA) (Rev A).nes" 339437F6, NTSC, 1, 0, 4, 2, 0, false, Horizontal, "Sesame Street 123 (USA).nes" 33B899C9, NTSC, 16, 0, 16, 8, 0, false, Horizontal, "Dragon Ball - Daimaou Fukkatsu (Japan).nes" 340713DD, NTSC, 4, 0, 16, 16, 0, true, Horizontal, "Crystalis (USA) (Beta).nes" 3417EC46, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Swords and Serpents (USA).nes" 342727B1, NTSC, 80, 0, 0, 8, 0, false, Horizontal, "Yamamura Misa Suspense - Kyouto Ryuu no Tera Satsujin Jiken (Japan).nes" 343C7BB0, NTSC, 3, 0, 2, 2, 0, false, Vertical, "Tetris (USA) (Unl).nes" 34540318, NTSC, 1, 0, 1, 8, 0, true, Horizontal, "Legend of Zelda, The (USA) (Rev B) (GameCube Edition).nes" 345D3A1A, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Castle of Deceit (USA) (Unl).nes" 34629104, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Teenage Mutant Hero Turtles - Tournament Fighters (Europe).nes" 348D3FF1, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Shatterhand (Europe).nes" 34BB757B, PAL, 1, 0, 1, 8, 0, false, Horizontal, "Dynablaster (Europe).nes" 34C1E893, PAL, 1, 0, 16, 8, 0, false, Vertical, "Bigfoot (Europe).nes" 34DDF806, NTSC, 243, 0, 0, 2, 0, false, Horizontal, "Strategist (Asia) (NTSC) (Unl).nes" 34DEBDFD, NTSC, 1, 0, 1, 8, 0, true, Horizontal, "Tamura Koushou Mahjong Seminar (Japan).nes" 34EAB034, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Heavy Barrel (USA).nes" 350D835E, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Gradius (USA).nes" 35476E87, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Wolverine (USA).nes" 358E29DD, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Chevaliers du Zodiaque, Les - La Legende d'Or (France).nes" 35B6FEBF, NTSC, 2, 0, 1, 8, 0, false, Vertical, "NFL (USA).nes" 35C41CD4, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Air Fortress (USA).nes" 35C6F574, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Young Indiana Jones Chronicles, The (USA).nes" 35D8C961, NTSC, 19, 0, 16, 8, 0, false, Horizontal, "Mappy Kids (Japan).nes" 35EFFD0E, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Rambo (Japan).nes" 360AA8B4, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Datsugoku (Japan).nes" 36584C96, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Robocco Wars (Japan).nes" 367566CE, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Strider Hiryu (Japan) (Proto).nes" 3691C120, NTSC, 18, 0, 16, 16, 0, false, Horizontal, "Mezase Top Pro - Green ni Kakeru Yume (Japan).nes" 369DA42D, NTSC, 19, 0, 16, 8, 0, true, Vertical, "King of Kings (Japan).nes" 36B35988, NTSC, 79, 0, 4, 2, 0, false, Vertical, "Double Strike - Aerial Attack Force (USA) (v1.0) (Unl).nes" 36C3B13A, PAL, 2, 0, 1, 8, 0, false, Vertical, "Rod Land featuring Rit and Tam (Europe) (Beta).nes" 36CA3102, NTSC, 4, 0, 32, 16, 0, false, FourScreen, "Rocman X (Asia) (Unl).nes" 37088EFF, NTSC, 4, 0, 32, 32, 0, true, Horizontal, "Kirby's Adventure (Canada).nes" 370CEB65, NTSC, 70, 0, 0, 8, 0, false, Horizontal, "Family Trainer 5 - Meiro Daisakusen (Japan).nes" 37138039, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "WWF Wrestlemania (USA).nes" 3719A26D, NTSC, 4, 0, 4, 2, 0, false, Horizontal, "Family Jockey (Japan).nes" 37397194, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Adventures of Lolo 3 (Europe).nes" 3747CD0B, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Nintendo World Class Service - Control Deck Test Cartridge (USA).nes" 37A5EB52, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Probotector II - Return of the Evil Forces (Europe).nes" 37B62D04, NTSC, 118, 0, 0, 16, 0, false, Vertical, "Ys III - Wanderers from Ys (Japan).nes" 37BA3261, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Back to the Future Part II & III (USA).nes" 37C474D5, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Rygar (USA) (Rev A).nes" 37CB1801, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Top Gun (Japan).nes" 37E24797, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Nekketsu Kakutou Densetsu (Japan).nes" 37F59450, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Aigina no Yogen - Balubalouk no Densetsu Yori (Japan).nes" 3824F7A5, PAL, 1, 0, 4, 2, 0, false, Horizontal, "Snake Rattle n Roll (Europe).nes" 3836EEAC, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Tanigawa Kouji no Shougi Shinan II (Japan).nes" 383CABBF, NTSC, 119, 0, 0, 8, 0, false, Horizontal, "High Speed (USA).nes" 3869E598, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Hollywood Squares (USA).nes" 38810A91, NTSC, 0, 0, 1, 2, 0, false, FourScreen, "Mach Rider (Japan, USA) (Rev A).nes" 38946C43, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Fisher-Price - Firehouse Rescue (USA).nes" 38B590E4, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Dusty Diamond's All-Star Softball (USA).nes" 38BFC03C, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Shanghai II (Japan).nes" 38DE7053, PAL, 2, 0, 1, 8, 0, false, Vertical, "Pro Wrestling (Europe).nes" 38EF66B5, NTSC, 87, 0, 0, 2, 0, false, Vertical, "Choplifter (Japan) (En) (Rev 1).nes" 38FBCC85, NTSC, 71, 0, 1, 16, 0, false, Vertical, "Fantastic Adventures of Dizzy, The (USA) (Unl).nes" 391AA1B8, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Bloody Warriors - Shan-Go no Gyakushuu (Japan).nes" 396F0D59, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Sekiryuuou (Japan).nes" 398B8182, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Darkman (USA).nes" 39B68AA3, NTSC, 23, 0, 0, 8, 0, false, Horizontal, "Jarinko Chie - Bakudan Musume no Shiawase Sagashi (Japan).nes" 39BB6616, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Monopoly (Germany).nes" 39D43261, NTSC, 4, 0, 32, 8, 0, true, Horizontal, "Deja Vu (Sweden).nes" 39F2CE4B, NTSC, 5, 0, 16, 16, 0, true, Horizontal, "Suikoden - Tenmei no Chikai (Japan).nes" 3A0965B1, NTSC, 2, 0, 1, 16, 0, false, Horizontal, "Paperboy 2 (USA).nes" 3A4D4D10, PAL, 9, 0, 16, 8, 0, false, Horizontal, "Mike Tyson's Punch-Out!! (Europe).nes" 3A8723B9, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Wrath of the Black Manta (USA) (Rev A).nes" 3A8F81B0, NTSC, 184, 0, 0, 2, 0, false, Vertical, "Madoola no Tsubasa (Japan).nes" 3A990EE0, NTSC, 71, 0, 1, 8, 0, false, Vertical, "Stunt Kids (USA) (Unl).nes" 3AC0830A, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Action in New York (Europe).nes" 3B0F4DB2, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Adventures of Dr. Franken, The (USA) (Proto).nes" 3B1A7EEF, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Murder Club - Honkaku Mystery Adventure (Japan).nes" 3B3F88F0, NTSC, 1, 0, 2, 4, 0, true, Horizontal, "Dragon Warrior (USA).nes" 3B7F5B3B, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Jurassic Park (USA).nes" 3B90D11E, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Universe Soldiers, The (Unknown) (Unl).nes" 3BB31E38, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Little Ninja Brothers (Europe).nes" 3BBFF3A6, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Best Play Pro Yakyuu (Japan).nes" 3BE244EF, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Little Mermaid, The (USA).nes" 3BE91A23, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Pajama Hero - Nemo (Japan).nes" 3BF55966, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Ganso Saiyuuki - Super Monkey Daibouken (Japan).nes" 3C5C81D4, NTSC, 4, 0, 4, 4, 0, false, Vertical, "R.B.I. Baseball (USA).nes" 3C7E38F5, NTSC, 11, 0, 4, 2, 0, false, Vertical, "Master Chu and the Drunkard Hu (USA) (Unl).nes" 3CCB5D57, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Klax (Japan).nes" 3CD4B420, NTSC, 33, 0, 0, 8, 0, false, Vertical, "Takeshi no Sengoku Fuuunji (Japan) (Beta).nes" 3CD6BB0E, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Lost Word of Jenny - Ushinawareta Message (Japan).nes" 3CF67AEC, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Twin Eagle (Japan).nes" 3CF749DE, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Adventures in the Magic Kingdom (USA) (Beta 1).nes" 3D0996B2, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Pirates! (USA).nes" 3D1C3137, NTSC, 78, 0, 0, 8, 0, false, FourScreen, "Uchuusen Cosmo Carrier (Japan).nes" 3D1C4894, NTSC, 4, 0, 1, 8, 0, false, Horizontal, "Ninja Crusaders (USA).nes" 3D3FF543, NTSC, 79, 0, 8, 4, 0, false, Horizontal, "AV Dragon Mahjang (Japan) (Unl).nes" 3D4B64F1, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "J.League Winning Goal (Japan).nes" 3D564757, PAL, 0, 0, 1, 2, 0, false, Horizontal, "10-Yard Fight (USA, Europe).nes" 3D95D866, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Sanrio Carnival 2 (Japan).nes" 3DA2085E, NTSC, 2, 0, 1, 16, 0, false, Vertical, "Maniac Mansion (Japan).nes" 3DBD6DAF, PAL, 1, 0, 16, 8, 0, false, Vertical, "Hoops (Europe).nes" 3DCADA42, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Hoshi o Miru Hito (Japan).nes" 3E00A373, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Meikyuu Kumikyoku - Milon no Daibouken (Japan).nes" 3E1271D5, NTSC, 79, 0, 4, 2, 0, false, Vertical, "Tiles of Fate (USA) (Unl).nes" 3E170708, NTSC, 0, 0, 2, 4, 0, false, Horizontal, "Final Combat (Asia) (NTSC) (Unl).nes" 3E470FE0, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Downtown - Nekketsu Koushinkyoku - Soreyuke Daiundoukai (Japan).nes" 3E58A87E, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Freedom Force (USA).nes" 3E59E951, NTSC, 168, 0, 0, 4, 0, true, Horizontal, "Racermate Challenge II (USA) (v6.02.002) (Unl).nes" 3E785DC3, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Air Fortress (Japan).nes" 3E95BA25, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Pac-Man (Japan).nes" 3ECA3DDA, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Bases Loaded 3 (USA).nes" 3ECDB1F7, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Rampart (Japan).nes" 3EDCF7E8, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "Mega Man 5 (USA).nes" 3EEA372E, NTSC, 189, 0, 0, 8, 0, false, Vertical, "Thunder Warrior (Asia) (Unl).nes" 3EFF62E4, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Dark Lord (Japan).nes" 3F0C8136, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Golf Grand Slam (Japan).nes" 3F0FD764, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Blaster Master (USA).nes" 3F15D20D, NTSC, 16, 0, 1, 32, 0, true, Horizontal, "Famicom Jump II - Saikyou no 7 Nin (Japan).nes" 3F2450EA, NTSC, 0, 0, 4, 2, 0, false, Horizontal, "Galactic Crusader (Asia) (Unl).nes" 3F2BDA65, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Maniac Mansion (Sweden).nes" 3F56A392, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Captain ED (Japan).nes" 3F57E040, NTSC, 1, 0, 1, 8, 0, false, Vertical, "Square Deal (Japan) (Beta).nes" 3F78037C, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Mighty Final Fight (USA).nes" 3F7AD415, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Nobunaga no Yabou - Zenkoku Ban (Japan).nes" 3F8D6889, NTSC, 18, 0, 16, 16, 0, false, Horizontal, "Moe Pro! - Saikyou Hen (Japan).nes" 3FA96277, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Super Star Force (Japan).nes" 3FC1DC19, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Deblock (Japan).nes" 3FE272FB, NTSC, 1, 0, 1, 8, 0, true, Horizontal, "Legend of Zelda, The (USA).nes" 3FEA656A, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Quarter Back Scramble (Japan).nes" 3FF10E3D, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "1943 - The Battle of Midway (Japan) (Beta).nes" 3FF44F87, NTSC, 1, 0, 1, 8, 0, true, Horizontal, "Tetris 2 + Bombliss (Japan).nes" 3FFA5762, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "American Dream (Japan).nes" 401349A8, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Balloon Fight (USA).nes" 401521F7, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Wacky Races (USA).nes" 4022C94E, PAL, 2, 0, 1, 8, 0, false, Vertical, "Smurfs, The (Europe) (En,Fr,De,Es).nes" 404B2E8B, NTSC, 4, 0, 8, 4, 0, false, FourScreen, "Rad Racer II (USA).nes" 4057C51B, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Ghostbusters (Japan).nes" 40684E95, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Solomon's Key (USA).nes" 407D6FFD, NTSC, 47, 0, 0, 16, 0, false, Vertical, "Super Spike V'Ball + Nintendo World Cup (USA).nes" 40A5E676, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Bible Adventures (USA) (Unl).nes" 40BFA660, PAL, 3, 0, 4, 2, 0, false, Horizontal, "Tiger-Heli (Europe).nes" 40C0AD47, NTSC, 33, 0, 0, 8, 0, false, Horizontal, "Flintstones, The - The Rescue of Dino & Hoppy (Japan).nes" 40D159B6, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Baseball Stars (USA).nes" 40DAFCBA, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Bad News Baseball (USA).nes" 40DBF7A2, PAL, 243, 0, 0, 4, 0, false, Horizontal, "Olympic I.Q. (Asia) (PAL) (Unl).nes" 40ED2A9D, NTSC, 1, 0, 4, 2, 0, false, Horizontal, "Taboo - The Sixth Sense (USA).nes" 41462D21, NTSC, 1, 0, 16, 8, 0, false, Vertical, "Sou Setsu Ryuu (Japan) (Beta).nes" 4156A3CD, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Keroppi to Keroriinu no Splash Bomb! (Japan).nes" 415E5109, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Meikyuu no Tatsujin - Daimeiro (Japan).nes" 41632CB6, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Zombie Hunter (Japan).nes" 4178497A, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Booby Kids (Japan).nes" 4185ADA1, PAL, 0, 0, 4, 2, 0, false, Horizontal, "Super Pang (Asia) (PAL) (Unl).nes" 419461D0, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Super Cars (USA).nes" 41CC30A7, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "World Super Tennis (Japan).nes" 41CF5B6A, NTSC, 246, 0, 0, 32, 0, true, Horizontal, "Feng Shen Bang (Asia) (Unl).nes" 41D32FD7, PAL, 7, 0, 1, 16, 0, false, Horizontal, "Aladdin (Europe).nes" 41F5D38D, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Kero Kero Keroppi no Daibouken 2 - Donuts Ike wa Oosawagi! (Japan).nes" 41F9E0AA, NTSC, 118, 0, 0, 8, 0, false, Horizontal, "Pro Sport Hockey (USA).nes" 4220C170, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Wheel of Fortune Starring Vanna White (USA).nes" 4232C609, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Drac's Night Out (USA) (Proto).nes" 423ADA8E, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Skate or Die (USA).nes" 42749A95, NTSC, 11, 0, 8, 4, 0, false, Vertical, "P'Radikus Conflict (USA) (Unl).nes" 429103C9, NTSC, 210, 2, 16, 8, 0, false, Vertical, "Famista '94 (Japan).nes" 4318A2F8, NTSC, 1, 0, 16, 4, 0, false, Horizontal, "Barker Bill's Trick Shooting (USA).nes" 4339865C, NTSC, 69, 0, 16, 8, 0, false, Horizontal, "Pyokotan no Daimeiro (Japan).nes" 43539A3C, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Space Harrier (Japan).nes" 435AEEC6, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Panic Restaurant (USA).nes" 437E7B69, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Double Dribble (USA).nes" 43B0944B, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Shin Jinrui - The New Type (Japan).nes" 43D01C10, NTSC, 4, 0, 32, 8, 0, true, Horizontal, "Deja Vu (USA).nes" 43D30C2F, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Ms. Pac-Man (USA) (Unl).nes" 441AEAE6, NTSC, 94, 0, 0, 8, 0, false, Horizontal, "Senjou no Ookami (Japan).nes" 441DE6D8, PAL, 1, 0, 16, 8, 0, true, Horizontal, "Pirates! (Europe).nes" 443FC6CD, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Darkwing Duck (Germany).nes" 44B060DA, NTSC, 1, 0, 1, 8, 0, true, Horizontal, "Shogun (Japan).nes" 44D21F83, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "10-Yard Fight (Japan) (Rev 1).nes" 44F34172, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Sqoon (USA).nes" 44F92026, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Pachinko Daisakusen 2 (Japan).nes" 4536FE1C, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Majou Densetsu II - Daimashikyou Galious (Japan).nes" 455CA7DE, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Igo Meikan (Japan).nes" 4582F22E, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Otaku no Seiza - An Adventure in the Otaku Galaxy (Japan).nes" 45878D7F, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Cat Ninden Teyandee (Japan).nes" 459D0C2A, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Dungeon & Magic - Swords of Element (Japan).nes" 45A41784, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Jackie Chan's Action Kung Fu (USA).nes" 45A9DB6F, PAL, 2, 0, 1, 8, 0, false, Vertical, "Section-Z (Europe).nes" 45F03D2E, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Faria - A World of Mystery & Danger! (USA).nes" 46135141, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Swords and Serpents (France).nes" 4640EBE0, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "President no Sentaku (Japan).nes" 4642DDA6, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Nobunaga's Ambition (USA).nes" 46480432, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Godzilla - Monster of Monsters! (Europe).nes" 464A67AB, PAL, 0, 0, 1, 2, 0, false, Vertical, "Kung Fu (Europe).nes" 465E5483, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Sword Master (USA).nes" 466EFDC2, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Final Fantasy (Japan) (Rev 0A).nes" 4681691A, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Demon Sword (USA).nes" 4686C5DD, NTSC, 41, 0, 0, 16, 0, false, Horizontal, "Caltron - 6 in 1 (USA) (Unl).nes" 46931EA0, PAL, 1, 0, 4, 2, 0, false, Horizontal, "R.C. Pro-Am (Europe) (Rev A).nes" 46B5751B, NTSC, 244, 0, 0, 8, 0, false, Vertical, "Decathlon (Asia) (Unl).nes" 46E0D37D, NTSC, 1, 0, 1, 8, 0, true, Horizontal, "Legend of Zelda, The (USA) (Rev A) (GameCube Edition).nes" 46F30F2D, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Maison Ikkoku (Japan).nes" 46FD7843, NTSC, 210, 2, 16, 8, 0, false, Vertical, "Splatter House - Wanpaku Graffiti (Japan).nes" 471173E7, PAL, 243, 0, 0, 2, 0, false, Horizontal, "Chinese Checkers (Asia) (PAL) (Unl).nes" 47232739, NTSC, 210, 2, 4, 8, 0, false, Horizontal, "Top Rider (Japan).nes" 4751A751, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Nigel Mansell's World Championship Challenge (USA).nes" 475CDBFE, NTSC, 72, 0, 0, 8, 0, false, Horizontal, "Pinball Quest (Japan).nes" 476E022B, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Rock 'n' Ball (USA).nes" 477A478D, NTSC, 3, 0, 8, 2, 0, false, Horizontal, "AV Poker (Japan) (Unl).nes" 47918D84, PAL, 243, 0, 0, 2, 0, false, Horizontal, "Auto-Upturn (Asia) (PAL) (Unl).nes" 47B6A39F, PAL, 1, 0, 16, 8, 0, true, Horizontal, "Zelda II - The Adventure of Link (Europe) (Rev B).nes" 47C2020B, NTSC, 19, 0, 16, 16, 0, false, Horizontal, "Hydlide 3 - Yami kara no Houmonsha (Japan).nes" 47EA8047, PAL, 0, 0, 8, 16, 0, false, Horizontal, "Hell Fighter (Asia) (PAL) (Unl).nes" 47F7F860, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Superman (Sunsoft) (USA) (Proto).nes" 47FD88CF, PAL, 1, 0, 16, 8, 0, true, Horizontal, "Zelda II - The Adventure of Link (Europe).nes" 481519B1, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Gekitou Stadium!! (Japan).nes" 48239B42, NTSC, 113, 0, 8, 4, 0, false, Horizontal, "Mahjang Companion (Asia) (Hacker) (Unl).nes" 4823EEFE, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Ultima - Warriors of Destiny (USA).nes" 482C79AF, NTSC, 1, 0, 4, 2, 0, false, Horizontal, "Taboo - The Sixth Sense (USA) (Rev A).nes" 48349B0B, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Dragon Quest II - Akuryou no Kamigami (Japan).nes" 484A60DB, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Ballblazer (Japan).nes" 485AC098, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Sanrio Carnival (Japan).nes" 4864C304, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Donkey Kong Jr. (World) (Rev A).nes" 489D19AB, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Kame no Ongaeshi - Urashima Densetsu (Japan).nes" 489EF6A2, NTSC, 1, 0, 16, 2, 0, false, Horizontal, "Airwolf (USA).nes" 48B8EE58, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Four Players' Tennis (Europe).nes" 48CA0EE1, NTSC, 69, 0, 32, 8, 0, false, Horizontal, "Barcode World (Japan).nes" 48E904D0, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Snake's Revenge (USA).nes" 48F68D40, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Clu Clu Land (World).nes" 490E8A4C, NTSC, 25, 0, 0, 16, 0, false, Horizontal, "Teenage Mutant Ninja Turtles II - The Manhattan Project (Japan).nes" 49123146, NTSC, 23, 0, 0, 8, 0, false, Horizontal, "Getsu Fuuma Den (Japan).nes" 491CD95E, NTSC, 0, 0, 16, 16, 0, false, Horizontal, "Jurassic Boy (Asia) (Unl).nes" 491D8CDB, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Family Pinball (Japan).nes" 493BD2FF, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Star Gate (Japan).nes" 4942BDA8, NTSC, 4, 0, 16, 16, 0, true, Horizontal, "Magic Candle, The (Japan).nes" 498187B6, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Wizardry - Proving Grounds of the Mad Overlord (Japan).nes" 49AEB3A6, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Excitebike (Japan, USA).nes" 49DA2F76, NTSC, 18, 0, 16, 8, 0, false, Horizontal, "Pizza Pop! (Japan).nes" 49F745E0, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "720 Degrees (USA).nes" 4A601A2C, NTSC, 25, 0, 0, 16, 0, false, Horizontal, "Teenage Mutant Ninja Turtles (Japan).nes" 4A99B47E, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Gomoku Narabe (Japan).nes" 4AE58F5D, NTSC, 18, 0, 16, 16, 0, false, Horizontal, "Shin Moero!! Pro Yakyuu (Japan).nes" 4AEA40F7, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Tom & Jerry (Japan).nes" 4B041B6B, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Ivan 'Ironman' Stewart's Super Off Road (USA).nes" 4B0DACCE, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Dragon Fighter (Japan).nes" 4B40CBD9, NTSC, 232, 0, 0, 16, 0, false, Vertical, "Pegasus 4 in 1 (Unknown) (Unl).nes" 4B5177E9, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Kunio-kun no Nekketsu Soccer League (Japan).nes" 4B6EF399, NTSC, 188, 0, 0, 16, 0, false, Horizontal, "Karaoke Studio Senyou Cassette Vol. 1 (Japan).nes" 4B750880, PAL, 4, 0, 32, 8, 0, false, Vertical, "Flintstones, The - The Surprise at Dinosaur Peak (Europe).nes" 4BB6B430, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Tetsudou Ou - Famicom Boardgame (Japan).nes" 4BB9B840, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Isolated Warrior (Europe).nes" 4BC75F16, NTSC, 11, 0, 8, 4, 0, false, Vertical, "King Neptune's Adventure (USA) (Beta) (Unl).nes" 4BF80AF8, NTSC, 0, 0, 32, 16, 0, false, Horizontal, "Super Cartridge Ver 3 - 8 in 1 (Asia) (Unl).nes" 4C049CFE, NTSC, 69, 0, 32, 8, 0, false, Horizontal, "Honoo no Toukyuuji - Dodge Danpei (Japan).nes" 4C0E8BBB, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Satomi Hakkenden (Japan).nes" 4C5836BD, NTSC, 19, 0, 32, 16, 0, false, Horizontal, "Namco Classic (Japan).nes" 4D1AC58C, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "David Crane's A Boy and His Blob - Trouble on Blobolonia (USA).nes" 4D1DF589, PAL, 1, 0, 16, 8, 0, true, Horizontal, "Turbo Racing (Europe).nes" 4D345422, PAL, 1, 0, 16, 2, 0, false, Horizontal, "Airwolf (Europe).nes" 4D3FBA78, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Dr. Jekyll and Mr. Hyde (USA).nes" 4D527D4A, NTSC, 11, 0, 2, 2, 0, false, Vertical, "Tagin' Dragon (USA) (Unl).nes" 4D68CFB1, NTSC, 38, 0, 0, 8, 0, false, Vertical, "Crime Busters (Unknown) (Unl).nes" 4D7859A9, NTSC, 69, 0, 16, 8, 0, false, Horizontal, "Batman (Japan).nes" 4D7D896C, PAL, 0, 0, 1, 2, 0, false, Vertical, "Pro Action Replay (Europe) (v1.2) (No Cart Present) (Unl).nes" 4DCD15EE, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "World Boxing (Japan).nes" 4DFD949E, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Sqoon (Japan).nes" 4E22368D, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Top Gun (USA).nes" 4E42F13A, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "1943 - The Battle of Valhalla (Japan).nes" 4E44FF44, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Bonk's Adventure (USA).nes" 4E5257D7, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Osomatsu-kun (Japan).nes" 4E77733A, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Hunt for Red October, The (USA) (Rev A).nes" 4E959173, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Gotcha! - The Sport! (USA).nes" 4E99CEA4, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Bikkuri Nekketsu Shin Kiroku! - Harukanaru Kin Medal (Japan).nes" 4EC0FECC, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Ufouria - The Saga (Europe).nes" 4ECD4624, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Mission Impossible (France).nes" 4ED3C6F1, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Predator (Japan).nes" 4ED5AA56, NTSC, 1, 0, 1, 8, 0, true, Vertical, "Bard's Tale, The - Tales of the Unknown (USA) (Beta 1).nes" 4F032933, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Ike Ike! Nekketsu Hockey-bu - Subette Koronde Dairantou (Japan).nes" 4F089E8A, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Galaxy 5000 (Europe).nes" 4F16C504, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Taito Basketball (Japan).nes" 4F2F1846, NTSC, 206, 0, 8, 8, 0, false, Vertical, "Famista '89 - Kaimaku Ban!! (Japan).nes" 4F3B2E57, NTSC, 66, 0, 4, 8, 0, false, Vertical, "Dragon Ball - Le Secret du Dragon (France).nes" 4F467410, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Ikari Warriors II - Victory Road (USA).nes" 4F48B240, PAL, 2, 0, 1, 8, 0, false, Vertical, "Trojan (Europe).nes" 4F74E236, NTSC, 2, 0, 1, 16, 0, false, Vertical, "Wonderland Dizzy (Unknown) (Proto) (1993-09-24) (Unl).nes" 4F9DBBE5, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Rambo (USA) (Rev A).nes" 4FBBE319, NTSC, 1, 0, 16, 8, 0, false, Vertical, "Adventures in the Magic Kingdom (USA) (Beta 2).nes" 4FC2F673, NTSC, 75, 0, 0, 8, 0, false, Vertical, "Ganbare Goemon! - Karakuri Douchuu (Japan).nes" 505F9715, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Wizards & Warriors (USA).nes" 5062A34B, NTSC, 184, 0, 0, 2, 0, false, Vertical, "Atlantis no Nazo (Japan) (Sample).nes" 506E259D, NTSC, 1, 0, 1, 32, 0, true, Horizontal, "Dragon Warrior IV (USA).nes" 50893B58, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Gekitou Pro Wrestling!! - Toukon Densetsu (Japan).nes" 509E6032, PAL, 2, 0, 1, 16, 0, false, Horizontal, "Paperboy 2 (Europe).nes" 50A1B3FE, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Donkey Kong Jr. + Jr. Lesson (Japan).nes" 50CCC8ED, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Battleship (USA).nes" 50CCDA33, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Taiyou no Shinden (Japan).nes" 50D141FC, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Yo! Noid (USA).nes" 50D296B3, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Tombs & Treasure (USA).nes" 50DA4867, NTSC, 4, 0, 32, 8, 0, true, Horizontal, "Shadow Brain (Japan).nes" 50F3E338, NTSC, 188, 0, 0, 16, 0, false, Horizontal, "Karaoke Studio Senyou Cassette Vol. 2 (Japan).nes" 50FD0CC6, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Double Dragon III - The Sacred Stones (USA).nes" 5104833E, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Kick Master (USA).nes" 5112DC21, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Wild Gunman (World) (Rev A).nes" 516B2412, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Kyouryuu Sentai Zyuranger (Japan).nes" 517611FE, NTSC, 0, 0, 16, 16, 0, false, Horizontal, "Super Cartridge Ver 8 - 4 in 1 (Asia) (Unl).nes" 51BD8336, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Foton - The Ultimate Game on Planet Earth (Japan).nes" 51BEE3EA, NTSC, 1, 0, 16, 2, 0, false, Horizontal, "Family Feud (USA).nes" 51BF28AF, PAL, 7, 0, 1, 8, 0, false, Horizontal, "Marble Madness (Europe).nes" 51C51C35, PAL, 3, 0, 4, 2, 0, false, Vertical, "Gradius (Europe).nes" 51C70247, NTSC, 11, 0, 16, 8, 0, false, Vertical, "Joshua & the Battle of Jericho (USA) (v5.0) (Unl).nes" 5229FCDD, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Kyoro-chan Land (Japan).nes" 52387646, NTSC, 1, 0, 1, 8, 0, true, Vertical, "Super Mario Bros. 2 (USA) (Beta).nes" 5248CAF3, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Simpsons, The - Bart vs. the Space Mutants (USA) (Rev A).nes" 524A5A32, PAL, 7, 0, 1, 16, 0, false, Horizontal, "Battletoads (Europe).nes" 52880295, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Winter Games (USA).nes" 529B621F, NTSC, 1, 0, 8, 8, 0, false, Horizontal, "Super Mario Bros. + Duck Hunt + World Class Track Meet (USA).nes" 52B58732, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Yoshi's Cookie (USA).nes" 52E2B5E0, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Super Mario Bros. 3 (Japan) (Rev A).nes" 530BCCB4, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Red Arremer II (Japan).nes" 5318CDB9, NTSC, 4, 0, 32, 8, 0, true, Horizontal, "Kouryuu Densetsu Villgust Gaiden (Japan).nes" 532A27E6, NTSC, 4, 0, 32, 16, 0, true, Horizontal, "Might & Magic - Secret of the Inner Sanctum (USA).nes" 53328FC4, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Chester Field - Ankoku Shin e no Chousen (Japan) (Beta).nes" 5337F73C, NTSC, 4, 0, 16, 16, 0, true, Horizontal, "Niji no Silk Road (Japan).nes" 535C5446, NTSC, 3, 0, 8, 2, 0, false, Vertical, "Idol Shisen Mahjong (Japan) (Unl).nes" 536E5200, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Kitty's Catch (USA) (Proto) (Unl).nes" 538218B2, PAL, 2, 0, 1, 8, 0, false, Horizontal, "Ikari Warriors (Europe).nes" 538CD2EA, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Three Stooges, The (USA).nes" 5393D949, NTSC, 76, 0, 16, 8, 0, false, Vertical, "Digital Devil Story - Megami Tensei (Japan).nes" 5397E80B, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Tenkaichi Bushi - Keru Naguuru (Japan).nes" 53A9B53A, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Ferrari Grand Prix Challenge (Europe).nes" 53A9E2BA, NTSC, 4, 0, 32, 16, 0, true, Horizontal, "Earth Bound (USA) (Proto).nes" 5440811C, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Stick Hunter - Exciting Ice Hockey (Japan).nes" 548A2C3C, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Karnov (USA).nes" 54E43C57, PAL, 4, 0, 32, 16, 0, false, Horizontal, "Star Wars - The Empire Strikes Back (Europe).nes" 5529431F, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Operation Wolf (Europe).nes" 55397DB3, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Wizardry II - Llylgamyn no Isan (Japan).nes" 555042B3, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Minna no Taabou no Nakayoshi Daisakusen (Japan).nes" 55761931, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Ninja Hattori-kun - Ninja wa Syugyou de Gozaru (Japan).nes" 55773880, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Adventures of Gilligan's Island, The (USA).nes" 5581E835, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Popeye (Japan).nes" 55B4052B, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Makai Island (USA) (Proto).nes" 55DB7E2A, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Mario's Time Machine (USA).nes" 560BF5A6, NTSC, 11, 0, 16, 8, 0, false, Vertical, "King of Kings, The (USA) (v1.2) (Unl).nes" 563C2CC0, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Kiwi Kraze - A Bird-Brained Adventure! (USA).nes" 563E394A, NTSC, 150, 0, 0, 4, 0, false, Horizontal, "Mahjong Academy (Asia) (Unl).nes" 565A4681, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Hissatsu Doujou Yaburi (Japan).nes" 565B1BDB, PAL, 0, 0, 1, 1, 0, false, Horizontal, "Golf (Europe).nes" 56756615, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Princess Tomato in the Salad Kingdom (USA).nes" 567E1620, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Ikari III - The Rescue (USA).nes" 56B9F640, NTSC, 216, 0, 0, 4, 0, false, Vertical, "Magic Jewelry 2 (Asia) (Unl).nes" 56F05853, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Ufouria - The Saga (Europe) (Beta).nes" 5734EB9E, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "World Class Track Meet (USA).nes" 5746A461, NTSC, 19, 0, 16, 8, 0, false, Vertical, "Final Lap (Japan).nes" 574E5F8B, NTSC, 1, 0, 16, 8, 0, false, Vertical, "Pirates! (Germany).nes" 576A0DE8, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Fun House (USA).nes" 57AC67AF, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Super Mario Bros. 2 (USA).nes" 57C2AE4E, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Cliffhanger (USA).nes" 57D162F1, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Mickey Mouse III - Yume Fuusen (Japan).nes" 57DD23D1, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Faxanadu (USA).nes" 57E220D0, NTSC, 4, 0, 1, 32, 0, true, Horizontal, "Final Fantasy III (Japan).nes" 57E9B21C, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Vegas Connection - Casino kara Ai o Komete (Japan).nes" 5800BE2D, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Toobin' (USA) (Unl).nes" 58152B42, NTSC, 142, 0, 0, 4, 0, false, Vertical, "Pipe 5 (Asia) (Unl).nes" 58507BC9, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Famicom Top Management (Japan).nes" 585BA83D, PAL, 4, 0, 16, 16, 0, false, Horizontal, "Krusty's Fun House (Europe).nes" 586A3277, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Milon's Secret Castle (USA).nes" 588A31FE, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Untouchables, The (USA) (Rev A).nes" 588E7492, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Tecmo Bowl (Japan).nes" 58C7DDAF, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Captain America and the Avengers (USA).nes" 58D1F46A, PAL, 0, 0, 2, 4, 0, false, Horizontal, "Final Combat (Asia) (PAL) (Unl).nes" 58E63E82, NTSC, 112, 0, 0, 8, 0, false, Horizontal, "Chik Bik Ji Jin - Saam Gwok Ji (Asia) (Unl).nes" 59280BEC, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Jackie Chan (Japan).nes" 5931BE01, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "New Ghostbusters II (Japan).nes" 59449E3B, NTSC, 4, 0, 16, 16, 0, true, Horizontal, "Mahjong Taisen (Japan).nes" 5991B9D0, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Simpsons, The - Bartman Meets Radioactive Man (USA).nes" 59977A46, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Mach Rider (Japan, USA).nes" 5A0454F3, NTSC, 4, 0, 16, 16, 0, true, Horizontal, "Ys II - Ancient Ys Vanished - The Final Chapter (Japan).nes" 5A18F611, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Tsuri Kichi Sanpei - Blue Marlin Hen (Japan).nes" 5A4F156D, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Hudson Hawk (USA).nes" 5A5A0CD9, NTSC, 1, 0, 4, 8, 0, true, Horizontal, "Daisenryaku (Japan).nes" 5A62F17F, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Captain America and the Avengers (Australia).nes" 5A6860F1, NTSC, 4, 0, 1, 16, 0, false, Horizontal, "Shougi Meikan '92 (Japan).nes" 5A8B4DA8, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Formula One - Built to Win (USA).nes" 5AB54795, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Chuugoku Janshi Story - Tonfuu (Japan).nes" 5ABBF861, NTSC, 1, 0, 16, 8, 0, false, Vertical, "New Ghostbusters II (USA) (Proto).nes" 5ADBF660, NTSC, 25, 0, 0, 8, 0, false, Horizontal, "Gradius II (Japan).nes" 5AEFBC94, PAL, 150, 0, 0, 4, 0, false, Horizontal, "Jovial Race (Asia) (PAL) (Unl).nes" 5B16A3C8, NTSC, 11, 0, 16, 8, 0, false, Vertical, "Sunday Funday - The Ride (USA) (Unl).nes" 5B457641, NTSC, 16, 0, 1, 16, 0, false, Horizontal, "Datach - Ultraman Club - Supokon Fight! (Japan).nes" 5B4B6056, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Little Nemo - The Dream Master (USA).nes" 5B4C6146, NTSC, 4, 0, 8, 4, 0, false, Horizontal, "Family Boxing (Japan).nes" 5B5AB1F8, PAL, 4, 0, 16, 16, 0, false, Horizontal, "Little Samson (Europe).nes" 5B6CA654, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Barbie (USA).nes" 5B7AC91F, NTSC, 18, 0, 16, 8, 0, false, Horizontal, "Goal!! (Japan).nes" 5B837E8D, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Alien Syndrome (Japan).nes" 5BB62688, NTSC, 4, 0, 8, 4, 0, false, Vertical, "Ring King (USA).nes" 5BC9D7A1, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Al Unser Jr. Turbo Racing (USA).nes" 5C123EF7, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Wizardry III - Diamond no Kishi (Japan).nes" 5C5A1AB8, NTSC, 3, 0, 2, 2, 0, false, Horizontal, "Tetris (Bulletproof) (Japan) (Rev B).nes" 5C9063E0, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Golf (Japan).nes" 5CAA3E61, NTSC, 144, 0, 8, 4, 0, false, Vertical, "Death Race (USA) (Unl).nes" 5CD5FDA4, NTSC, 66, 0, 4, 8, 0, false, Vertical, "Family Trainer 9 - Fuuun Takeshi-jou 2 (Japan).nes" 5CDB2823, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Pac-Man (Japan) (Rev A).nes" 5CE55F5B, PAL, 3, 0, 4, 2, 0, false, Vertical, "Star Force (Europe).nes" 5CF536F4, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Power Blade (USA).nes" 5CF6A82E, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Sesame Street Countdown (USA).nes" 5D0D3047, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Werewolf - The Last Warrior (Europe).nes" 5D105C10, NTSC, 4, 0, 16, 16, 0, true, Horizontal, "Hissatsu Shigoto Nin (Japan).nes" 5D1301C5, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Mappy (Japan).nes" 5D2444D7, NTSC, 86, 0, 0, 8, 0, false, Vertical, "Moero!! Pro Yakyuu (Japan) (Rev 2).nes" 5D2B1962, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Tetris 2 (Europe).nes" 5D40C08A, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Pescatore (Japan) (Proto).nes" 5D99053D, PAL, 3, 0, 4, 2, 0, false, Vertical, "Track & Field in Barcelona (Europe).nes" 5DA9CEC8, NTSC, 11, 0, 2, 2, 0, false, Horizontal, "Mission Cobra (USA) (Unl).nes" 5DBD6099, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Adventures in the Magic Kingdom (USA).nes" 5DC9BC41, NTSC, 7, 0, 1, 8, 0, false, Vertical, "Solstice - The Quest for the Staff of Demnos (USA) (Beta).nes" 5DCE2EEA, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Darkwing Duck (USA).nes" 5DE61639, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Advanced Dungeons & Dragons - Hillsfar (USA).nes" 5DEC84F8, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Chiisana Obake - Acchi Socchi Kocchi (Japan).nes" 5E24EEDA, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Fuzzical Fighter (Japan).nes" 5E345B6D, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Magmax (Japan).nes" 5E36D3BE, NTSC, 113, 0, 16, 16, 0, false, Vertical, "Real Player's Pak (Australia) (Unl).nes" 5E66EAEA, NTSC, 13, 0, 0, 2, 0, false, Vertical, "Videomation (USA).nes" 5E6D9975, PAL, 7, 0, 1, 8, 0, false, Horizontal, "Wizards & Warriors (Europe).nes" 5E767671, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Silent Service (USA).nes" 5E900522, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Bubble Bobble (USA).nes" 5EA7D410, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "WCW World Championship Wrestling (USA).nes" 5EB8E707, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Magic Darts (USA).nes" 5ED6F221, NTSC, 4, 0, 32, 32, 0, true, Horizontal, "Kirby's Adventure (USA) (Rev A).nes" 5EDEC8CD, NTSC, 1, 0, 2, 2, 0, false, Vertical, "Virus (USA) (Beta) (1990-02-02).nes" 5EE6008E, NTSC, 1, 0, 16, 4, 0, false, Horizontal, "Mechanized Attack (USA).nes" 5F0BCE2A, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Break Time - The National Pool Tour (USA).nes" 5F14DC48, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "FC Genjin - Freakthoropus Computerus (Japan).nes" 5F2C3195, NTSC, 4, 0, 8, 4, 0, false, Vertical, "Super Sprint (USA) (Unl).nes" 5F5BFA54, NTSC, 11, 0, 16, 8, 0, false, Vertical, "Exodus - Journey to the Promised Land (USA) (v5.0) (Unl).nes" 5F6E8A07, NTSC, 66, 0, 4, 8, 0, false, Vertical, "Paris-Dakar Rally Special (Japan).nes" 5FAB6BCE, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Devil World (Japan).nes" 5FD2AAB1, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Bo Jackson Baseball (USA).nes" 6025C660, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Wally Bear and the No! Gang (USA) (Beta) (Unl).nes" 603AAA57, NTSC, 4, 0, 16, 16, 0, false, Vertical, "Mega Man 3 (USA).nes" 6058C65D, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Spartan X (Japan).nes" 607BD020, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Die Hard (Europe).nes" 60925D08, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Tecmo Cup - Football Game (Europe).nes" 60A3B803, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Hokuto no Ken (Japan).nes" 60A59624, NTSC, 113, 0, 16, 8, 0, false, Horizontal, "Mind Blower Pak (Australia) (Unl).nes" 60AA9AE0, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Prince of Persia (Germany).nes" 60AD090A, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Family Trainer 1 - Athletic World (Japan) (Rev 1).nes" 60E563F1, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Tatakai no Banka (Japan) (Rev A).nes" 60E63537, NTSC, 1, 0, 8, 8, 0, false, Horizontal, "Super Mario Bros. + Duck Hunt + World Class Track Meet (USA) (Rev A).nes" 60EA98A0, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Maniac Mansion (Germany).nes" 61061352, NTSC, 112, 0, 0, 2, 0, false, Horizontal, "Master Shooter (Asia) (Unl).nes" 61179BFA, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Jungle Book, The (USA).nes" 61253D1C, NTSC, 11, 0, 4, 4, 0, false, Vertical, "Raid 2020 (USA) (Unl).nes" 6150517C, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Rockman 2 - Dr. Wily no Nazo (Japan).nes" 619BEA12, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Gun-Dec (Japan).nes" 61A852EA, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Battle Stadium - Senbatsu Pro Yakyuu (Japan).nes" 61B4295A, NTSC, 87, 0, 0, 2, 0, false, Vertical, "Jajamaru no Daibouken (Japan).nes" 61D86167, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Street Cop (USA).nes" 62217BA7, NTSC, 184, 0, 0, 2, 0, false, Vertical, "Wing of Madoola, The (Japan) (Sample).nes" 622E054A, PAL, 1, 0, 16, 8, 0, false, Vertical, "Guerrilla War (Europe).nes" 622F059D, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Chack'n Pop (Japan).nes" 623020FB, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Kiddy Sun in Fantasia (Taiwan).nes" 626ABD49, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Takahashi Meijin no Bouken-jima III (Japan).nes" 6272C549, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Terminator, The (USA, Europe).nes" 62C67984, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Nekketsu Koukou Dodgeball-bu (Japan).nes" 62E2E7FC, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Stanley - The Search for Dr. Livingston (USA).nes" 6328B44D, PAL, 4, 0, 16, 8, 0, false, Vertical, "Parodius (Europe) (Beta).nes" 63338C3C, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Formation Z (Japan).nes" 63469396, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Hokuto no Ken 4 - Shichisei Haken Den - Hokuto Shinken no Kanata e (Japan).nes" 636923BB, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Spelunker (Japan).nes" 637134E8, NTSC, 193, 0, 0, 8, 0, false, Horizontal, "Fighting Hero (Asia) (Unl).nes" 6377CB75, NTSC, 1, 0, 2, 8, 0, true, Horizontal, "A Ressha de Ikou (Japan).nes" 637A7ACB, NTSC, 1, 0, 1, 16, 0, true, Vertical, "Tenchi o Kurau (Japan) (Rev A).nes" 637FE65C, NTSC, 79, 0, 4, 2, 0, false, Vertical, "Krazy Kreatures (USA) (Unl).nes" 638DBC52, NTSC, 193, 0, 0, 8, 0, false, Vertical, "War in the Gulf (Brazil) (CCE, Gluk Video) (Unl).nes" 6396B988, NTSC, 5, 0, 16, 16, 0, true, Horizontal, "Empereur, L' (Japan).nes" 63AEA200, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Donkey Kong Jr. no Sansuu Asobi (Japan).nes" 63C4E122, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Guevara (Japan).nes" 63D38B86, NTSC, 71, 0, 1, 8, 0, false, Vertical, "Dreamworld Pogie (Unknown) (Proto).nes" 63D3AFF4, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Rockin' Kats (USA) (Beta).nes" 63E992AC, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Alfred Chicken (USA).nes" 63FCC0DD, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Rescue - The Embassy Mission (USA).nes" 6435C095, NTSC, 1, 0, 4, 4, 0, false, Horizontal, "Short Order + Egg-Splode! (USA).nes" 6439F53A, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Mini Putt (Japan).nes" 644E312B, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Ankoku Shinwa - Yamato Takeru Densetsu (Japan).nes" 6479E76A, NTSC, 18, 0, 16, 8, 0, false, Horizontal, "Terao no Dosukoi Oozumou (Japan).nes" 64A02715, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Gekikame Ninja Den (Japan).nes" 64B710D2, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Pro Wrestling (USA) (Rev A).nes" 64B8CDE2, NTSC, 64, 0, 0, 4, 0, false, Vertical, "Klax (USA) (Beta) (Unl).nes" 64BBCB77, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Warpman (Japan).nes" 64BD6CDB, NTSC, 1, 0, 4, 2, 0, false, Horizontal, "Hirake! Ponkikki (Japan).nes" 64C0FA3B, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Little Mermaid - Ningyo Hime (Japan).nes" 64C96F53, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Raid on Bungeling Bay (Japan).nes" 64FD3BA6, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Nekketsu Koukou Dodgeball-bu - Soccer Hen (Japan).nes" 652F3324, NTSC, 18, 0, 16, 8, 0, false, Horizontal, "Tsuru Pika Hagemaru - Mezase! Tsuru Seko no Akashi (Japan).nes" 654F4E90, PAL, 1, 0, 1, 8, 0, false, Horizontal, "Rad Racer (Europe).nes" 65518EAE, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Addams Family, The (USA).nes" 655EFEED, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Ikari Warriors (USA).nes" 656D4265, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Urban Champion (World).nes" 656FA3B5, NTSC, 87, 0, 0, 2, 0, false, Vertical, "Argus (Japan).nes" 657F7875, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Soccer (World).nes" 65B6AF68, NTSC, 0, 0, 1, 1, 0, false, Vertical, "World of Card Games, The (Asia) (Unl).nes" 65D1AB64, PAL, 4, 0, 32, 8, 0, false, Horizontal, "Jetsons, The - Cogswell's Caper (Europe).nes" 66066326, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Sword Master (Europe).nes" 662B8C9C, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Excitebike (USA) (GameCube Edition).nes" 666BE5EC, PAL, 4, 0, 16, 8, 0, false, Vertical, "New Zealand Story, The (Europe).nes" 668D1715, PAL, 4, 0, 32, 16, 0, true, Horizontal, "Wario's Woods (Europe).nes" 66DD04E1, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Meiji Ishin (Japan).nes" 66EBDB64, PAL, 2, 0, 1, 8, 0, false, Vertical, "Skate or Die (Europe).nes" 66ED9C00, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Bananan Ouji no Daibouken (Japan).nes" 66F4D9F5, NTSC, 1, 0, 16, 4, 0, false, Horizontal, "Knight Rider (Japan).nes" 66F6A39E, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Darkwing Duck (Europe).nes" 671F23A8, PAL, 5, 0, 16, 16, 0, false, Horizontal, "Castlevania III - Dracula's Curse (Europe).nes" 6720ABAC, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Bucky O'Hare (Europe).nes" 67555417, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "SD Gundam - Gachapon Senshi 4 - NewType Story (Japan).nes" 6772CA86, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Juuouki (Japan).nes" 67751094, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Adventures of Bayou Billy, The (USA).nes" 6776A977, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Tokoro-san no Mamoru mo Semeru mo (Japan).nes" 67811DA6, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Dash Galaxy in the Alien Asylum (USA).nes" 67861A27, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Baseball (USA) (GameCube Edition).nes" 67A3C362, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Touch Down Fever (Japan).nes" 67CBC0A0, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Hammerin' Harry (Europe).nes" 67D5C3F9, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Hello Kitty World (Japan).nes" 67F77118, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Rocket Ranger (USA).nes" 67FC2E40, NTSC, 69, 0, 16, 16, 0, false, Vertical, "Mr. Gimmick (USA) (Proto).nes" 6800C5B3, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Tom Sawyer no Bouken (Japan).nes" 680D2EDA, NTSC, 150, 0, 0, 2, 0, false, Horizontal, "Chess Academy (Asia) (NTSC) (Unl).nes" 680DA78D, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Bible Adventures (USA) (v1.4) (Unl).nes" 681798A8, PAL, 3, 0, 4, 2, 0, false, Vertical, "City Connection (Europe).nes" 68379FDB, NTSC, 79, 0, 2, 2, 0, true, Vertical, "Pipemania (Australia) (HES) (Unl).nes" 68383607, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Wheel of Fortune - Junior Edition (USA).nes" 684AFCCD, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Space Hunter (Japan).nes" 684B292F, NTSC, 19, 0, 32, 16, 0, false, Horizontal, "Namco Classic II (Japan).nes" 6866A989, NTSC, 1, 0, 16, 8, 0, false, Vertical, "Scarabeus (USA) (Sample).nes" 689971F9, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Super Dodge Ball (USA).nes" 68AFEF5F, NTSC, 3, 0, 8, 2, 0, false, Vertical, "Bubble Bath Babes (USA) (Unl).nes" 68C62E50, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Home Alone 2 - Lost in New York (Europe).nes" 68CF9B78, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Fester's Quest (Europe).nes" 68EC97CB, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Desert Commander (USA).nes" 68F9B5F5, PAL, 1, 0, 1, 16, 0, false, Horizontal, "Defender of the Crown (Europe).nes" 690AFE9F, PAL, 4, 0, 32, 16, 0, false, Vertical, "Ultimate Air Combat (Europe) (En,Fr,De) (Beta).nes" 692F2096, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Secret Scout in the Temple of Demise (USA) (Beta) (Unl).nes" 6944A01A, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Isolated Warrior (USA).nes" 694C801F, PAL, 7, 0, 1, 16, 0, false, Horizontal, "IronSword - Wizards & Warriors II (Europe).nes" 695515A2, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Platoon (USA) (Rev A).nes" 69565F13, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "TM Network - Live in Power Bowl (Japan).nes" 69635A6E, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Rollerball (USA).nes" 696D7839, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Tecmo Cup - Soccer Game (USA).nes" 6997F5E1, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Last Starfighter, The (USA).nes" 699FA085, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Othello (USA).nes" 69BCDB8B, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Navy Blue (Japan).nes" 69D07DDB, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Saiyuuki World (Japan).nes" 69FEECB2, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Flipull - An Exciting Cube Game (Japan) (En) (Rev 1).nes" 6A154B68, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Ghostbusters (USA).nes" 6A1F628A, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Shadowgate (USA).nes" 6A457A43, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Takahashi Meijin no Bouken-jima (Japan).nes" 6A483073, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Pesterminator (USA) (Unl).nes" 6A6B7239, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Hana no Star Kaidou (Japan).nes" 6A88579F, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Jeopardy! Junior Edition (USA).nes" 6ABAD366, NTSC, 1, 0, 8, 16, 0, false, Horizontal, "Bases Loaded (USA) (Rev B).nes" 6AE69227, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Sou Setsu Ryuu III - The Rosetta Stone (Japan).nes" 6AE762AE, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Hyper Sports (Japan) (Rev 1).nes" 6B53006A, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Battle of Olympus, The (USA).nes" 6B761858, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Adventures in the Magic Kingdom (Europe).nes" 6BB6A0CE, NTSC, 1, 0, 4, 2, 0, false, Horizontal, "Palamedes (USA).nes" 6BC33D2F, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Wizardry - Knight of Diamonds - The Second Scenario (USA).nes" 6BC65D7E, NTSC, 66, 0, 4, 8, 0, false, Vertical, "Youkai Club (Japan).nes" 6BD7047A, NTSC, 79, 0, 4, 4, 0, false, Vertical, "Robert Byrne's Pool Challenge (USA) (Proto) (Unl).nes" 6C1AB645, PAL, 4, 0, 16, 8, 0, true, Vertical, "Jurassic Park (Europe).nes" 6C4A9735, PAL, 1, 0, 1, 8, 0, true, Horizontal, "WWF Wrestlemania (Europe).nes" 6C61B622, NTSC, 72, 0, 0, 8, 0, false, Vertical, "Moero!! Pro Tennis (Japan).nes" 6C70A17B, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Chester Field - Ankoku Shin e no Chousen (Japan).nes" 6C93377C, NTSC, 71, 0, 1, 4, 0, false, Vertical, "Bee 52 (USA) (Unl).nes" 6C940A59, NTSC, 4, 0, 16, 8, 0, false, FourScreen, "SD Gundam - Gachapon Senshi 5 - Battle of Universal Century (Japan).nes" 6CCA1C1F, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Family Trainer 7 - Daiundoukai (Japan).nes" 6CD46979, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Bandai Golf - Challenge Pebble Beach (USA).nes" 6CD9CC23, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Bakushou! Star Monomane Shitennou (Japan).nes" 6CDC0CD9, NTSC, 33, 0, 0, 8, 0, false, Horizontal, "Bubble Bobble 2 (Japan).nes" 6D65CAC6, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Terra Cresta (Japan).nes" 6DC28B5A, NTSC, 25, 0, 0, 8, 0, false, Horizontal, "Bio Miracle Bokutte Upa (Japan).nes" 6DCBAAFD, PAL, 4, 0, 16, 8, 0, false, Horizontal, "RoboCop (Europe).nes" 6DCE4B23, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Double Dragon - Sou Setsu Ryuu (Japan).nes" 6DECD886, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Guerrilla War (USA).nes" 6E0EB43E, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Puss n Boots - Pero's Great Adventure (USA).nes" 6E4DCFD2, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Roundball - 2-on-2 Challenge (USA).nes" 6E68E31A, NTSC, 16, 0, 32, 8, 0, false, Horizontal, "Dragon Ball 3 - Gokuu Den (Japan).nes" 6E85D8DD, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Adventures of Tom Sawyer (USA).nes" 6EC51DE5, NTSC, 210, 2, 16, 8, 0, false, Vertical, "Famista '92 (Japan).nes" 6ED31CCD, NTSC, 1, 0, 1, 8, 0, false, Vertical, "Chip's Challenge (USA) (v0.924B) (Proto).nes" 6EE4BB0A, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Mega Man (USA).nes" 6EE94D32, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Smash T.V. (USA).nes" 6EEA1B10, NTSC, 1, 0, 16, 8, 0, false, Vertical, "Ninja Gaiden (USA) (Beta).nes" 6F10097D, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Simpsons, The - Bart vs. the Space Mutants (USA).nes" 6F27300B, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Teenage Mutant Ninja Turtles (Italy).nes" 6F4E4312, NTSC, 5, 0, 32, 32, 0, true, Horizontal, "Aoki Ookami to Shiroki Mejika - Genchou Hishi (Japan).nes" 6F5D9B2A, NTSC, 32, 0, 0, 8, 0, false, Horizontal, "Meikyuu-jima (Japan).nes" 6F6686B0, NTSC, 65, 0, 0, 8, 0, false, Horizontal, "Spartan X 2 (Japan).nes" 6F790F9B, NTSC, 2, 0, 1, 8, 0, true, Horizontal, "Rainbow Islands - The Story of Bubble Bobble 2 (Japan) (Sample).nes" 6F8AF3E8, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Top Gun - The Second Mission (USA).nes" 6F97C721, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Donkey Kong (World) (Rev A).nes" 6FB349E2, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Mickey's Adventure in Numberland (USA).nes" 6FD5A271, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Whomp 'Em (USA).nes" 6FD69F34, NTSC, 1, 0, 4, 2, 0, false, Horizontal, "Dr. Mario (USA) (Beta) (1990-04-27).nes" 7002FE8D, PAL, 2, 0, 1, 8, 0, false, Vertical, "Life Force - Salamander (Europe).nes" 70080810, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Metroid (USA).nes" 701B1ADF, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Takahashi Meijin no Bouken-jima II (Japan).nes" 705BD7C3, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Superstar Pro Wrestling (Japan).nes" 7077B075, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Lethal Weapon (USA).nes" 7080D1F8, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Power Soccer (Japan).nes" 70860FCA, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Popeye (World) (Rev A).nes" 708EA2BE, NTSC, 4, 0, 32, 16, 0, true, Horizontal, "Joy Mech Fight (Japan).nes" 709C9399, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Shadow Warriors (Europe).nes" 70CE3771, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Prince of Persia (USA).nes" 70F31D2C, PAL, 71, 0, 1, 16, 0, false, Vertical, "Cosmic Spacehead (Europe) (En,Fr,De,Es) (Unl).nes" 70F67AB7, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Musashi no Bouken (Japan).nes" 711896B8, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Xenophobe (USA).nes" 711C2B0E, NTSC, 4, 0, 2, 2, 0, false, Horizontal, "Super Chinese (Japan).nes" 7156CB4D, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Muppet Adventure - Chaos at the Carnival (USA).nes" 716DAEA5, NTSC, 19, 0, 32, 16, 0, true, Horizontal, "Juvei Quest (Japan) (Rev 1).nes" 7172F3D4, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Kabushiki Doujou (Japan).nes" 719571B3, PAL, 0, 0, 1, 1, 0, false, Horizontal, "Road Fighter (Europe).nes" 71BF075F, NTSC, 1, 0, 4, 2, 0, false, Horizontal, "Adventures of Lolo (USA).nes" 71C01B19, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Little Red Hood (Australia) (Unl).nes" 71C9ED1E, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Kujaku Ou (Japan).nes" 71CAF097, NTSC, 0, 0, 1, 2, 0, false, Vertical, "3-D Block (Asia) (RCM Group) (Unl).nes" 71D8C6E9, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Ultima - Seija e no Michi (Japan).nes" 721B5217, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Daiva - Imperial of Nirsartia (Japan).nes" 728BFA8D, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Route-16 Turbo (Japan).nes" 72928698, NTSC, 69, 0, 16, 8, 0, false, Horizontal, "Hebereke (Japan).nes" 72E66392, NTSC, 11, 0, 2, 4, 0, false, Vertical, "Crystal Mines (USA) (Unl).nes" 72FA78C3, NTSC, 139, 0, 0, 4, 0, false, Horizontal, "Mahjang Companion (Asia) (Sachen) (Unl).nes" 73140EEF, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Bible Adventures (USA) (v1.3) (Unl).nes" 7329118D, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Casino Kid II (USA).nes" 73298C87, PAL, 4, 0, 32, 16, 0, false, Horizontal, "Super Mario Bros. + Tetris + Nintendo World Cup (Europe).nes" 73418721, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Challenger (Japan).nes" 73620901, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Dr. Chaos (USA).nes" 736FEBC4, NTSC, 4, 0, 32, 16, 0, true, Vertical, "Meng Huan - Xiang Shuai Chuan Qi Zhi Xue Hai Piao Ling (China) (Unl).nes" 739A1027, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Nuts & Milk (Japan).nes" 73C09BCB, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Hanafuda Yuukyou Den - Nagarebana Oryuu (Japan) (Unl) [b].nes" 73C246D4, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Bible Adventures (USA) (v1.1) (Unl).nes" 73C7FCF4, NTSC, 2, 0, 1, 8, 0, false, Vertical, "DuckTales 2 (USA).nes" 73D5F7D3, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Sqoon (Japan) (Rev A).nes" 73E41AC7, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "100 Man Dollar Kid - Maboroshi no Teiou Hen (Japan).nes" 73F7E5D8, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Deep Dungeon III - Yuushi e no Tabi (Japan).nes" 73FB55AC, NTSC, 243, 0, 0, 4, 0, false, Vertical, "Lightgun Game 2 in 1 - Cosmocop + Cyber Monster (Asia) (Unl).nes" 7416903F, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Simpsons, The - Bart vs. the World (USA).nes" 74189E12, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "S.C.A.T. - Special Cybernetic Attack Team (USA).nes" 743387FF, NTSC, 85, 0, 0, 32, 0, true, Horizontal, "Lagrange Point (Japan).nes" 74386F15, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Godzilla - Monster of Monsters! (USA).nes" 74663267, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Hiryuu no Ken II - Dragon no Tsubasa (Japan).nes" 7474AC92, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Kabuki - Quantum Fighter (USA).nes" 74920C13, NTSC, 168, 0, 0, 4, 0, true, Horizontal, "Racermate Challenge II (USA) (v3.11.088) (Unl).nes" 74BEA652, NTSC, 3, 0, 4, 2, 0, false, Vertical, "3 in 1 Supergun (Asia) (Unl).nes" 74EE0FFC, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Radac Tailor-Made (Japan).nes" 74F0A89F, NTSC, 185, 0, 0, 2, 0, false, Horizontal, "B-Wings (Japan).nes" 75255F88, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "P.O.W. - Prisoners of War (USA).nes" 752743EC, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Willow (Japan).nes" 753768A6, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Vice - Project Doom (USA).nes" 757EFB63, NTSC, 0, 0, 1, 2, 0, true, Horizontal, "Skate Boy (Spain) (Gluk Video) (Unl).nes" 75901B18, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Titan (Japan).nes" 759418D2, PAL, 2, 0, 1, 8, 0, false, Horizontal, "Alfred Chicken (Europe).nes" 75A7E399, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Lot Lot (Japan).nes" 75B3EB37, NTSC, 18, 0, 16, 8, 0, false, Horizontal, "Saiyuuki World 2 - Tenjoukai no Majin (Japan).nes" 75C3E7D4, PAL, 3, 0, 4, 2, 0, false, Horizontal, "Solomon's Key (Europe).nes" 7653103A, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Mighty Final Fight (Europe).nes" 7678F1D5, NTSC, 80, 0, 0, 16, 0, false, Horizontal, "Fudou Myouou Den (Japan).nes" 768A1B6A, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Nakajima Satoru - F-1 Hero (Japan).nes" 76C161E3, PAL, 1, 0, 1, 16, 0, false, Horizontal, "Faxanadu (Europe).nes" 771C8855, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Adventure Island II (USA).nes" 771CE357, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Hokuto no Ken 3 - Shin Seiki Souzou Seiken Restuden (Japan).nes" 7739672E, NTSC, 0, 0, 4, 2, 0, false, Vertical, "Metal Fighter (Asia) (Sachen) (Unl).nes" 77512388, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Millipede - Kyodai Konchuu no Gyakushuu (Japan).nes" 7751588D, PAL, 1, 0, 1, 8, 0, false, Horizontal, "Metroid (Europe).nes" 77833016, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Chubby Cherub (USA).nes" 77BF8B23, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Hydlide (USA).nes" 77D59400, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Nintendo World Class Service - Power Pad Test Cartridge (USA).nes" 77DA06CF, NTSC, 115, 0, 0, 8, 0, false, Horizontal, "AV Kyuukyoku Mahjong 2 (Asia) (Unl).nes" 77DCBBA3, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Eggerland - Meikyuu no Fukkatsu (Japan).nes" 77F0F71D, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Simpsons, The - Bartman Meets Radioactive Man (USA) (Beta).nes" 78211EBF, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Daiku no Gen-san (Japan).nes" 7840B18D, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Chitei Senkuu Vazolder (Japan).nes" 784272F2, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Hector '87 (Japan).nes" 78A48B23, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Baseball (Japan).nes" 78B657AC, NTSC, 118, 0, 0, 16, 0, false, Vertical, "Armadillo (Japan).nes" 78C4460D, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Cocoron (Japan).nes" 78C72C75, NTSC, 1, 0, 16, 8, 0, false, Vertical, "S.C.A.T. - Special Cybernetic Attack Team (USA) (Beta).nes" 78CC796B, NTSC, 4, 0, 32, 8, 0, false, Vertical, "Batman Returns (Unknown) (Beta).nes" 790B295B, PAL, 4, 0, 4, 2, 0, false, Horizontal, "To the Earth (Europe).nes" 792070A9, NTSC, 232, 0, 0, 16, 0, false, Vertical, "Quattro Arcade (USA) (Unl).nes" 794CAAB6, NTSC, 75, 0, 0, 8, 0, false, Vertical, "Tetsuwan Atom (Japan).nes" 795BC424, PAL, 137, 0, 0, 2, 0, false, FourScreen, "Great Wall, The (Asia) (PAL) (Unl).nes" 79698B98, NTSC, 4, 0, 16, 16, 0, true, Horizontal, "God Slayer - Haruka Tenkuu no Sonata (Japan).nes" 7980C4F7, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Ultraman Club 2 - Kaettekita Ultraman Club (Japan).nes" 7984AE6D, NTSC, 79, 0, 4, 2, 0, false, Horizontal, "Puzzle (Spain) (Gluk Video) (Unl).nes" 798EEB98, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "TaleSpin (USA).nes" 79D48F34, PAL, 4, 0, 32, 8, 0, false, Horizontal, "Batman Returns (Europe).nes" 79F688BC, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Gauntlet II (Europe).nes" 7A11D2C9, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Igo Shinan '92 (Japan).nes" 7A3A49ED, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Elevator Action (Japan) (Rev A).nes" 7A497AE3, NTSC, 33, 0, 0, 8, 0, false, Horizontal, "Don Doko Don (Japan).nes" 7AA02377, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Outlanders (Japan).nes" 7AC3E8A1, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "RoboCop (USA) (Beta).nes" 7AE0BF3C, NTSC, 1, 0, 1, 8, 0, true, Horizontal, "Zelda no Densetsu 1 - The Hyrule Fantasy (Japan).nes" 7AE5C002, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Jackie Chan's Action Kung Fu (Europe).nes" 7B0A41B9, NTSC, 2, 0, 1, 16, 0, false, Vertical, "Esper Bouken Tai (Japan).nes" 7B44FB2A, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Ide Yousuke Meijin no Jissen Mahjong II (Japan).nes" 7B4ED0BB, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "WWF King of the Ring (USA).nes" 7B5206AF, NTSC, 16, 0, 16, 8, 0, false, Horizontal, "Meimon! Daisan Yakyuubu (Japan).nes" 7B55D481, PAL, 1, 0, 16, 8, 0, false, Vertical, "Ghostbusters II (Europe).nes" 7B6DC772, NTSC, 95, 0, 4, 8, 0, false, Vertical, "Dragon Buster (Japan).nes" 7B72FBA4, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Thunderbirds (Japan).nes" 7BA3F8AE, PAL, 4, 0, 16, 8, 0, false, Horizontal, "North & South (Europe).nes" 7BB5664F, NTSC, 4, 0, 4, 8, 0, false, Horizontal, "Super Xevious - Gump no Nazo (Japan).nes" 7BCCAFBB, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "Teenage Mutant Ninja Turtles II - The Arcade Game (Australia).nes" 7BD8F902, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Takahashi Meijin no Bouken-jima IV (Japan).nes" 7BF8A890, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Ninja Ryuuken Den II - Ankoku no Jashin Ken (Japan).nes" 7C108923, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Saint Seiya - Ougon Densetsu Kanketsu Hen (Japan) (Beta).nes" 7C16F819, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Nintendo World Cup (Europe) (Rev A).nes" 7C27AB86, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "Aces - Iron Eagle 3 (Japan).nes" 7C3D2EA3, NTSC, 2, 0, 1, 8, 0, false, Vertical, "SWAT - Special Weapons and Tactics (Japan).nes" 7C42CB7B, NTSC, 3, 0, 2, 2, 0, false, Vertical, "Banana (Japan) (Beta) (1986-06-30).nes" 7C4A72D8, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Ninja Gaiden (USA).nes" 7C596E45, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Portopia Renzoku Satsujin Jiken (Japan).nes" 7C6A3D51, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Mickey Mousecapade (USA).nes" 7C6F615F, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Attack of the Killer Tomatoes (USA).nes" 7C7A0A73, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Bashi Bazook - Morphoid Masher (USA) (Proto).nes" 7CB0D70D, PAL, 7, 0, 1, 8, 0, false, Horizontal, "Solstice - The Quest for the Staff of Demnos (Europe).nes" 7CF6B30A, PAL, 0, 0, 1, 2, 0, false, Vertical, "Locksmith (Asia) (PAL) (Unl).nes" 7D55CF29, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Attack Animal Gakuen (Japan).nes" 7D5F149B, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Ninja Gaiden - Episode II - The Dark Sword of Chaos (USA) (Beta) (1990-01-18).nes" 7D6C2065, NTSC, 1, 0, 1, 16, 0, true, Vertical, "Legend of Robin Hood, The (USA) (Proto).nes" 7DA77F11, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Jungle Book, The (Europe).nes" 7DCB4C18, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Mutant Virus, The - Crisis in a Computer World (USA).nes" 7DD0AFC8, PAL, 0, 0, 16, 4, 0, false, Vertical, "Challenge of the Dragon (Asia) (PAL) (Unl).nes" 7E053E64, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "BattleCity (Japan).nes" 7E4BA78F, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Adventure Island Part II, The (Europe).nes" 7E57FBEC, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Town & Country Surf Designs - Thrilla's Surfari (USA).nes" 7E9BCA05, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Crackout (USA) (Proto).nes" 7EABDA5C, NTSC, 11, 0, 16, 8, 0, false, Vertical, "King of Kings, The (USA) (v5.0) (Unl).nes" 7EAE9A13, NTSC, 234, 0, 0, 32, 0, false, Horizontal, "Maxi 15 (USA) (Rev 1) (Unl).nes" 7EC6F75B, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Magical Kid's Doropie (Japan).nes" 7EE02CA2, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Bard's Tale, The (Japan).nes" 7EE625EB, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Road Fighter (Japan).nes" 7F08D0D9, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Nintendo World Cup (Europe) (Rev B).nes" 7F24EFC0, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Bible Adventures (USA) (v1.2) (Unl).nes" 7F45CFF5, NTSC, 92, 0, 0, 16, 0, false, Vertical, "Moero!! Pro Soccer (Japan).nes" 7F495283, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Pooyan (Japan).nes" 7F4CB1B4, PAL, 2, 0, 1, 8, 0, false, Vertical, "Double Dribble (Europe).nes" 7F801368, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Tecmo Cup - Football Game (Spain).nes" 7F9C1DEC, NTSC, 7, 0, 1, 2, 0, false, Vertical, "BB Car (Unknown) (Unl).nes" 7FA191E7, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Track & Field II (USA) (Rev A).nes" 7FA2CC55, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Castle Excellent (Japan).nes" 7FB74A43, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Toki (USA).nes" 7FF76219, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Tecmo World Wrestling (USA).nes" 80250D64, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Monster in My Pocket (Europe).nes" 803B9979, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "J.League Fighting Soccer - The King of Ace Strikers (Japan).nes" 804F898A, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Dragon Unit (Japan).nes" 805F81BC, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Metal Gear (Japan).nes" 806DE21E, PAL, 7, 0, 1, 16, 0, false, Horizontal, "Wizards & Warriors III - Kuros...Visions of Power (Europe).nes" 808606F0, NTSC, 210, 1, 16, 8, 0, false, Vertical, "Famista '91 (Japan).nes" 80D63472, PAL, 0, 0, 2, 1, 0, false, Horizontal, "Sidewinder (Asia) (PAL) (Unl).nes" 80F39D59, NTSC, 79, 0, 4, 2, 0, false, Horizontal, "Poke Block (Asia) (Unl).nes" 80FB7E6B, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Super Mario USA (Japan).nes" 8106E694, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Blaster Master (Europe).nes" 810B7AB9, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Robo Warrior (USA).nes" 8111BA08, NTSC, 7, 0, 1, 16, 0, false, Horizontal, "Solar Jetman - Hunt for the Golden Warpship (USA).nes" 811F06D9, NTSC, 66, 0, 4, 8, 0, false, Vertical, "Dragon Power (USA).nes" 81210F63, PAL, 0, 0, 1, 1, 0, false, Horizontal, "Pac-Man (Europe).nes" 81241DEC, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "F-1 Sensation (Japan).nes" 81389607, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Magmax (USA).nes" 816AD178, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Batman - The Video Game (USA) (Beta 1).nes" 817431EC, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Metal Gear (USA).nes" 8192D2E7, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Kid Niki - Radical Ninja (USA).nes" 81A5EB65, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Tiny Toon Adventures 2 - Trouble in Wackyland (USA).nes" 81AF4AF9, PAL, 2, 0, 1, 8, 0, false, Horizontal, "Crackout (Europe).nes" 81B2A3CD, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Noah's Ark (Europe).nes" 81B7F1A8, NTSC, 210, 1, 16, 8, 0, false, Vertical, "Heisei Tensai Bakabon (Japan).nes" 821F2F9F, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Famicom Meijin Sen (Japan) (Rev A).nes" 821FEB7A, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Ikki (Japan).nes" 822F17EB, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Softball Tengoku (Japan).nes" 828F8F1F, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Sensha Senryaku - Sabaku no Kitsune (Japan).nes" 8293803A, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Prince of Persia (France).nes" 82AFA828, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Clash at Demonhead (USA).nes" 82BE4724, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Commando (USA).nes" 83000991, PAL, 2, 0, 1, 8, 0, false, Horizontal, "Side Pocket (Europe).nes" 8308FED7, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Fighting Golf (Japan).nes" 831F9C1A, NTSC, 79, 0, 8, 2, 0, false, Horizontal, "Ultimate League Soccer (USA) (Unl).nes" 8324A464, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Kakefu-kun no Jump Tengoku - Speed Jigoku (Japan).nes" 836685C4, PAL, 1, 0, 4, 8, 0, false, Horizontal, "Mario & Yoshi (Europe).nes" 8366CF72, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Titan Warriors (USA) (Proto).nes" 836C4FA7, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "10-Yard Fight (Japan).nes" 836FE2C2, PAL, 2, 0, 1, 8, 0, false, Vertical, "Jack Nicklaus' Greatest 18 Holes of Major Championship Golf (Europe).nes" 837A3D8A, PAL, 4, 0, 16, 16, 0, false, Vertical, "Mega Man 3 (Europe).nes" 83CB743F, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Koushien (Japan).nes" 83EA7B04, NTSC, 4, 0, 1, 32, 0, false, Horizontal, "Battle Baseball (Japan).nes" 83EAF3B1, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Best Keiba - Derby Stallion (Japan) (Rev A).nes" 83FC38F8, NTSC, 4, 0, 4, 8, 0, false, Horizontal, "Mappy-Land (USA).nes" 84148F73, NTSC, 1, 0, 16, 16, 0, false, Horizontal, "Goal! (USA).nes" 841B69B6, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Hatris (USA).nes" 84382231, NTSC, 9, 0, 16, 8, 0, false, Horizontal, "Punch-Out!! (Japan) (Gold Edition).nes" 846C9304, NTSC, 4, 0, 32, 16, 0, true, Vertical, "Lin Ze Xu Jin Yan (China) (Unl).nes" 847D672D, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Bill Elliott's NASCAR Challenge (USA).nes" 84BE00E9, NTSC, 0, 0, 1, 1, 0, false, Vertical, "4 Nin Uchi Mahjong (Japan) (Rev A).nes" 84C4A12E, PAL, 2, 0, 1, 8, 0, false, Vertical, "Metal Gear (Europe).nes" 84F7FC31, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Chip 'n Dale - Rescue Rangers (Europe).nes" 850090BC, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "White Lion Densetsu (Japan).nes" 851EB9BE, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Shooting Range (USA).nes" 8531C166, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Majaventure - Mahjong Senki (Japan).nes" 85323FD6, NTSC, 79, 0, 4, 2, 0, false, Vertical, "Krazy Kreatures (USA) (Beta) (Unl).nes" 853FEEA4, PAL, 4, 0, 4, 2, 0, false, Horizontal, "Adventures of Lolo 2 (Europe).nes" 856E7600, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Superman (Japan).nes" 8575A0CB, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Hydlide Special (Japan).nes" 8593E5AD, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "World Champ - Super Boxing Great Fight (USA).nes" 859C65E1, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Little League Baseball - Championship Series (USA).nes" 85A6C0D5, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Tecmo Bowl (USA) (Rev A).nes" 85BC0777, NTSC, 4, 0, 1, 32, 0, false, Horizontal, "Mahjong Club Nagatachou - Sousaisen (Japan).nes" 85BFFFEF, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Goonies II, The - Fratelli Saigo no Chousen (Japan) (Beta).nes" 85C5953F, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Hostages - The Embassy Mission (Japan).nes" 85D02CD4, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Bugs Bunny Birthday Bash (USA) (Beta) [b].nes" 85E0090B, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Choujinrou Senki Warwolf (Japan).nes" 85F12D37, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Gambler Jiko Chuushinha 2 (Japan).nes" 86083FBC, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Corre Benny (Spain) (Gluk Video) (Unl).nes" 8650BE49, NTSC, 4, 0, 16, 8, 0, true, Vertical, "McDonaldland (France).nes" 86670C93, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Slalom (USA).nes" 86759C0F, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Monopoly (Japan).nes" 86867830, PAL, 3, 0, 4, 2, 0, false, Vertical, "Adventure Island Classic (Europe).nes" 869501CA, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Dragon Quest III - Soshite Densetsu e... (Japan) (Rev B).nes" 86964EDD, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Teenage Mutant Ninja Turtles - Tournament Fighters (USA).nes" 86974CCC, NTSC, 11, 0, 16, 8, 0, false, Vertical, "King of Kings, The (USA) (v1.3) (Unl).nes" 86ACB36B, NTSC, 3, 0, 2, 2, 0, false, Horizontal, "Banana (Japan).nes" 86B0D1CF, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Robin Hood - Prince of Thieves (USA).nes" 86C495C6, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Gargoyle's Quest II (Europe).nes" 86DBA660, NTSC, 219, 0, 0, 2, 0, false, Vertical, "3-D Block (Asia) (Hwang Shinwei) (Unl).nes" 86E02D65, PAL, 4, 0, 8, 4, 0, false, Vertical, "Tecmo World Cup Soccer (Europe).nes" 872DE7A2, NTSC, 4, 0, 32, 16, 0, false, Vertical, "F-15 Strike Eagle (Sweden) (Sv,Da,Fi).nes" 8752DCCB, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Puzznic (Japan).nes" 87CE3F34, NTSC, 4, 0, 16, 16, 0, true, Horizontal, "Dragon Wars (Japan).nes" 87D7CAF0, NTSC, 3, 0, 1, 2, 0, false, Vertical, "Othello (Japan).nes" 87DA4BD0, NTSC, 185, 0, 0, 2, 0, false, Vertical, "Sansuu 3 Nen - Keisan Game (Japan).nes" 88053D25, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Dragon Quest (Japan).nes" 88062D9A, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Nekketsu! Street Basket - Ganbare Dunk Heroes (Japan).nes" 882E1901, NTSC, 79, 0, 4, 2, 0, false, Vertical, "Venice Beach Volleyball (USA) (Unl).nes" 88338ED5, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Cyberball (USA).nes" 883454EA, NTSC, 87, 0, 0, 2, 0, false, Vertical, "Choplifter (Japan).nes" 8889C564, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Immortal, The (USA).nes" 889129CB, NTSC, 4, 0, 32, 16, 0, true, Horizontal, "StarTropics (USA).nes" 8897A8F1, PAL, 2, 0, 1, 8, 0, false, Vertical, "Goonies II, The (Europe).nes" 88A6B192, NTSC, 79, 0, 4, 2, 0, false, Horizontal, "F15 City War (USA) (v1.0) (Unl).nes" 88C30FDA, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Super Turrican (Europe).nes" 88E1A5F4, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Lee Trevino's Fighting Golf (USA).nes" 8904149E, PAL, 7, 0, 1, 16, 0, false, Horizontal, "Solar Jetman - Hunt for the Golden Warpship (Europe).nes" 892434DD, NTSC, 71, 0, 1, 16, 0, false, Vertical, "Ultimate Stuntman, The (USA) (Unl).nes" 8927FD4C, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Rockin' Kats (USA).nes" 892CBBC2, NTSC, 185, 0, 0, 2, 0, false, Vertical, "Sansuu 2 Nen - Keisan Game (Japan).nes" 894EFDBC, NTSC, 16, 0, 1, 16, 0, false, Horizontal, "Datach - Crayon Shin-chan - Ora to Poi Poi (Japan).nes" 89550500, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Igo Shinan (Japan).nes" 89567668, NTSC, 1, 0, 4, 2, 0, false, Horizontal, "Boulder Dash (Japan).nes" 89821E2B, PAL, 1, 0, 16, 8, 0, false, Horizontal, "RoboCop 2 (Europe).nes" 898E4232, PAL, 7, 0, 1, 8, 0, false, Horizontal, "Cobra Triangle (Europe).nes" 899213DC, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Mickey Mouse - Dream Balloon (USA) (Beta).nes" 89984244, PAL, 7, 0, 1, 16, 0, false, Horizontal, "Lion King, The (Europe).nes" 89A45446, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Bram Stoker's Dracula (Europe).nes" 89D42098, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Pac-Land (Japan).nes" 89E085FE, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Zoids 2 - Zenebas no Gyakushuu (Japan).nes" 89EC53C8, PAL, 2, 0, 1, 8, 0, false, Vertical, "DuckTales 2 (Europe) (Beta).nes" 8A043CD6, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Mafat Conspiracy, The (USA).nes" 8A0C7337, PAL, 0, 0, 1, 1, 0, false, Vertical, "Excitebike (Europe).nes" 8A12A7D9, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Family Trainer 8 - Totsugeki! Fuuun Takeshi-jou (Japan).nes" 8A368744, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Ginga no Sannin (Japan).nes" 8A36D2B7, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Gojira (Japan).nes" 8A5BC0D3, NTSC, 4, 0, 8, 4, 0, false, Horizontal, "Tecmo World Cup Soccer (Japan).nes" 8A640AEF, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Double Dragon II - The Revenge (USA) (Rev A).nes" 8A65E57C, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Banana Prince (Germany).nes" 8A7D0ABE, NTSC, 33, 0, 0, 8, 0, false, Horizontal, "Akira (Japan).nes" 8A96E00D, NTSC, 23, 0, 0, 8, 0, false, Horizontal, "Wai Wai World (Japan).nes" 8AB52A24, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Blades of Steel (USA).nes" 8ADA3497, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "RoadBlasters (USA).nes" 8AF25130, NTSC, 4, 0, 4, 2, 0, false, Horizontal, "Babel no Tou (Japan).nes" 8B03F74D, NTSC, 21, 0, 0, 16, 0, false, Horizontal, "Wai Wai World 2 - SOS!! Paseri Jou (Japan).nes" 8B4A2866, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Sansuu 4 Nen - Keisan Game (Japan).nes" 8B4D2443, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Rescue - The Embassy Mission (Europe).nes" 8B7D3C75, PAL, 1, 0, 4, 2, 0, false, Vertical, "Anticipation (Europe).nes" 8B9CB8F2, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Dong Dong Nao II - Guo Zhong Ying Wen (Yi) (Asia) (Unl).nes" 8B9D3E9C, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Rad Racer (USA).nes" 8BA75848, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Karateka (Japan).nes" 8BCA5146, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Indiana Jones and the Last Crusade (USA) (Taito).nes" 8BCB0993, NTSC, 4, 0, 16, 16, 0, true, Horizontal, "Pachio-kun 3 (Japan).nes" 8BCDE59A, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Athena (Japan).nes" 8BDD3D93, PAL, 4, 0, 32, 8, 0, false, Vertical, "Gremlins 2 - The New Batch (Europe) (Beta).nes" 8BF29CB6, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Chip 'n Dale - Rescue Rangers (USA).nes" 8C252AC4, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Bad Dudes vs. Dragon Ninja (Europe).nes" 8C4D59D6, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Derby Stallion - Zenkoku Ban (Japan).nes" 8C5A784E, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Dragon Warrior II (USA).nes" 8C71F706, NTSC, 1, 0, 4, 2, 0, false, Horizontal, "King Neptune's Adventure (USA) (Unl).nes" 8C88536F, PAL, 4, 0, 32, 8, 0, false, Vertical, "George Foreman's KO Boxing (Europe).nes" 8C8DEDB6, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "G.I. Joe - The Atlantis Factor (USA).nes" 8C95A69F, NTSC, 4, 0, 1, 32, 0, true, Vertical, "Shen Tan Ke Nan (China) (Unl).nes" 8CA72D80, NTSC, 82, 0, 0, 8, 0, true, Horizontal, "Kyuukyoku Harikiri Koushien (Japan).nes" 8CACCA85, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Hudson's Adventure Island II (USA) (Beta).nes" 8CE478DB, NTSC, 5, 0, 16, 16, 0, true, Horizontal, "Nobunaga's Ambition II (USA).nes" 8D26FDEA, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "SD Sengoku Bushou Retsuden (Japan).nes" 8D3C33B3, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Soccer League - Winner's Cup (Japan).nes" 8D5B77C0, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Ginga Eiyuu Densetsu (Japan).nes" 8D77E5E6, NTSC, 4, 0, 1, 16, 0, true, Horizontal, "Business Wars (Japan).nes" 8D901FAD, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Captain Planet and the Planeteers (Europe).nes" 8D97155C, NTSC, 2, 0, 1, 8, 0, false, Vertical, "California Raisins - The Grape Escape (USA) (Proto 2).nes" 8D9AD3BF, PAL, 2, 0, 1, 8, 0, false, Horizontal, "Indiana Jones and the Last Crusade (Europe).nes" 8DA4E539, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Idol Hakkenden (Japan).nes" 8DA651D4, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Street Fighter 2010 - The Final Fight (USA).nes" 8DA6667D, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Nintendo World Cup (Europe).nes" 8DB31730, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Mario Bros. (USA) (GameCube Edition).nes" 8DB43824, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Darkwing Duck (USA) (Beta).nes" 8DCD9486, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Jumbo Ozaki no Hole in One Professional (Japan).nes" 8DD92725, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Adventures of Lolo 3 (USA).nes" 8E0D9179, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Chuuka Taisen (Japan).nes" 8E373118, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Battle Storm (Japan).nes" 8E62D229, NTSC, 185, 0, 0, 2, 0, false, Horizontal, "Sansuu 1 Nen - Keisan Game (Japan).nes" 8E7ABDFC, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Magnum Kikiippatsu - Empire City - 1931 (Japan).nes" 8EAB381C, NTSC, 79, 0, 8, 4, 0, false, Vertical, "Deathbots (USA) (Rev 1) (Unl).nes" 8EE25F78, NTSC, 80, 0, 0, 8, 0, true, Vertical, "Minelvaton Saga - Ragon no Fukkatsu (Japan).nes" 8EE7C43E, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Kid Klown in Night Mayor World (USA).nes" 8EEF8B76, NTSC, 4, 0, 1, 32, 0, true, Horizontal, "Last Armageddon (Japan).nes" 8F011713, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Kurogane Hiroshi no Yosou Daisuki! - Kachiuma Densetsu (Japan).nes" 8F154A0D, NTSC, 3, 0, 4, 1, 0, false, Horizontal, "Pu Ke Jing Ling (China) (Unl).nes" 8F197B0A, PAL, 2, 0, 1, 8, 0, false, Vertical, "Rygar (Europe).nes" 8F4497EE, NTSC, 3, 0, 2, 2, 0, false, Horizontal, "Peepar Time (Japan).nes" 8F628D51, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Hiryuu no Ken III - 5 Nin no Dragon (Japan).nes" 8FA6E92C, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Rackets & Rivals (Europe).nes" 8FF31896, NTSC, 0, 0, 1, 1, 0, false, Vertical, "M.U.S.C.L.E. - Tag Team Match (USA).nes" 900E3A23, NTSC, 4, 0, 32, 16, 0, true, Horizontal, "Silva Saga (Japan).nes" 90150FAC, NTSC, 177, 0, 0, 64, 0, true, Horizontal, "Wang Zi Fu Chou Ji (China) (Unl).nes" 90226E40, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Power Punch II (USA).nes" 902E3168, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Ninja Gaiden III - The Ancient Ship of Doom (USA).nes" 9044550E, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Family Trainer 10 - Rairai Kyonsees (Japan).nes" 905B93F6, NTSC, 3, 0, 1, 2, 0, false, Horizontal, "Monstruo de los Globos, El (Spain) (Gluk Video) (Unl).nes" 90600B85, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Seicross (Japan) (Rev 1).nes" 908505EE, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Super Arabian (Japan).nes" 90C773C1, NTSC, 118, 0, 0, 8, 0, false, Vertical, "Goal! Two (USA).nes" 90D68A43, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Garry Kitchen's BattleTank (USA).nes" 90ECDADE, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Tsuppari Oozumou (Japan).nes" 90F3F161, NTSC, 86, 0, 0, 8, 0, false, Vertical, "Moero!! Pro Yakyuu (Japan) (Rev 1).nes" 90F6FA33, NTSC, 22, 0, 0, 8, 0, false, Horizontal, "Ganbare Pennant Race! (Japan).nes" 91328C1D, NTSC, 23, 0, 0, 8, 0, false, Horizontal, "Tiny Toon Adventures (Japan).nes" 91440AAB, NTSC, 147, 0, 0, 8, 0, false, Vertical, "Chinese KungFu (Asia) (Unl).nes" 915A53A7, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Hyper Sports (Japan).nes" 917770D8, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Where in Time Is Carmen Sandiego (USA).nes" 917D9262, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Shufflepuck Cafe (Japan).nes" 9198279E, PAL, 7, 0, 1, 8, 0, false, Horizontal, "Time Lord (Europe).nes" 91AC514E, NTSC, 88, 0, 16, 8, 0, false, Horizontal, "Namcot Mahjong III - Mahjong Tengoku (Japan).nes" 91B4B1D7, PAL, 66, 0, 2, 4, 0, false, Vertical, "Super Mario Bros. + Duck Hunt (Europe).nes" 91D52E9A, NTSC, 18, 0, 16, 8, 0, false, Horizontal, "USA Ice Hockey in FC (Japan).nes" 91E2E863, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Magician (USA).nes" 92197173, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Magic of Scheherazade, The (USA).nes" 9235B57B, NTSC, 71, 0, 1, 16, 0, false, Vertical, "Micro Machines (USA) (Unl).nes" 9237B447, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Over Horizon (Europe).nes" 923F915B, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Ultraman Club 3 - Matamata Shutsugeki!! Ultra Kyoudai (Japan).nes" 9247C38D, PAL, 119, 0, 0, 8, 0, false, Horizontal, "Pin Bot (Europe).nes" 924CDE0B, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Eliminator Boat Duel (Europe).nes" 92547F1C, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Ys (Japan).nes" 9273F18E, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "America Daitouryou Senkyo (Japan).nes" 927C7A3A, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Joust (Japan).nes" 92924548, PAL, 0, 0, 1, 2, 0, false, Vertical, "Ice Hockey (Europe).nes" 92A2185C, NTSC, 9, 0, 16, 8, 0, false, Horizontal, "Mike Tyson's Punch-Out!! (Japan, USA).nes" 92A3D007, NTSC, 34, 0, 8, 4, 0, false, Vertical, "Impossible Mission II (USA) (Unl).nes" 92C138E4, NTSC, 1, 0, 8, 16, 0, false, Horizontal, "Miracle Piano Teaching System, The (USA).nes" 92DD67EA, NTSC, 1, 0, 16, 8, 0, false, Vertical, "Flying Warriors (USA) (Beta).nes" 92F04530, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Goonies 2 - Fratelli Saigo no Chousen (Japan).nes" 93216279, NTSC, 46, 0, 0, 64, 0, false, Vertical, "Rumble Station - 15 in 1 (USA) (Unl).nes" 93484CC9, NTSC, 234, 0, 0, 32, 0, false, Horizontal, "Maxi-15 Pack (Australia) (Unl).nes" 934DB14A, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "All-Pro Basketball (USA).nes" 9369A2F8, PAL, 2, 0, 1, 8, 0, false, Vertical, "Ghost'n Goblins (Europe).nes" 93991433, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Low G Man - The Low Gravity Man (USA).nes" 93A2EEFB, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Final Fantasy II (USA) (Proto).nes" 93A7D26C, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Keiba Simulation - Honmei (Japan).nes" 93B2CEC4, NTSC, 65, 0, 0, 16, 0, false, Horizontal, "Daiku no Gen-san 2 - Akage no Dan no Gyakushuu (Japan).nes" 93B49582, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Target Renegade (USA).nes" 93F3A490, NTSC, 64, 0, 0, 4, 0, false, Horizontal, "Klax (USA) (Unl).nes" 942B1210, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Michael Andretti's World GP (USA).nes" 94476A70, PAL, 2, 0, 1, 8, 0, false, Vertical, "Mega Man (Europe).nes" 948E0BD6, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Taiyou no Yuusha - Fighbird (Japan).nes" 9509F703, NTSC, 4, 0, 32, 16, 0, true, Horizontal, "Metal Max (Japan).nes" 952A9E77, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Choujin Sentai Jetman (Japan).nes" 9552E8DF, NTSC, 66, 0, 4, 8, 0, false, Vertical, "Dragon Ball - Shen Long no Nazo (Japan).nes" 9561798D, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Saint Seiya - Ougon Densetsu Kanketsu Hen (Japan).nes" 9568EB74, NTSC, 1, 0, 1, 8, 0, false, Vertical, "Mezase Pachi Pro - Pachio-kun (Japan) (Beta).nes" 956E3D90, NTSC, 4, 0, 32, 8, 0, true, Horizontal, "Daikaijuu Deburas (Japan).nes" 958E4BAE, NTSC, 1, 0, 16, 4, 0, false, Horizontal, "Orb 3D (USA).nes" 95CE3B58, PAL, 4, 0, 16, 16, 0, false, Vertical, "Simpsons, The - Bartman Meets Radioactive Man (Europe).nes" 95D3BFFF, PAL, 3, 0, 4, 2, 0, true, Horizontal, "Tiger-Heli (Europe) (Rev A).nes" 95E4E594, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "QIX (USA).nes" 96087988, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "RoboCop 3 (USA).nes" 9630A7E5, PAL, 1, 0, 16, 8, 0, false, Vertical, "Lee Trevino's Fighting Golf (Europe).nes" 9632C470, PAL, 1, 0, 16, 8, 0, false, Vertical, "Addams Family, The - Pugsley's Scavenger Hunt (Europe).nes" 965834BD, NTSC, 18, 0, 16, 8, 0, false, Horizontal, "Ninja Jajamaru - Ginga Daisakusen (Japan) (Beta).nes" 967011AD, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Mario Is Missing! (Europe).nes" 96773F32, NTSC, 19, 0, 32, 16, 0, true, Vertical, "Digital Devil Story - Megami Tensei II (Japan).nes" 969EF9E4, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Winter Games (USA) (Rev A).nes" 96BA90B0, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Houma ga Toki (Japan).nes" 96CFB4D8, PAL, 7, 0, 1, 8, 0, false, Horizontal, "Digger T. Rock - The Legend of the Lost City (Europe).nes" 96DFC776, NTSC, 4, 0, 8, 8, 0, false, Vertical, "R.B.I. Baseball 2 (USA) (Unl).nes" 96E6C1CE, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Major League Baseball (USA) (Rev A).nes" 972D08C5, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Tsuppari Wars (Japan).nes" 972D2784, PAL, 0, 0, 1, 2, 0, false, Vertical, "Soccer (Europe) (Rev A).nes" 9735D267, PAL, 1, 0, 4, 2, 0, false, Horizontal, "Dr. Mario (Europe).nes" 973BBF75, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Secret Scout in the Temple of Demise (USA) (Unl).nes" 9747AC09, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Monopoly (USA).nes" 974D0745, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Fighting Road (Japan).nes" 974E8840, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Best Play Pro Yakyuu '90 (Japan).nes" 976893D2, PAL, 3, 0, 4, 2, 0, false, Horizontal, "Alpha Mission (Europe).nes" 978E19FC, NTSC, 1, 0, 16, 8, 0, false, Vertical, "Predator (Australia).nes" 979C5314, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Super Pitfall (USA).nes" 97BC4585, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Touhou Kenbun Roku (Japan).nes" 97CAD370, NTSC, 10, 0, 16, 16, 0, true, Horizontal, "Fire Emblem - Ankoku Ryuu to Hikari no Tsurugi (Japan).nes" 97D52C06, PAL, 1, 0, 16, 8, 0, true, Horizontal, "Zelda II - The Adventure of Link (Europe) (Rev A).nes" 9806CB84, NTSC, 7, 0, 1, 16, 0, false, Horizontal, "Battletoads (Japan).nes" 980BE936, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Hyper Olympic (Japan).nes" 982DFB38, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Mickey's Safari in Letterland (USA).nes" 983948A5, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Karate Kid, The (USA).nes" 983D8175, NTSC, 16, 0, 1, 16, 0, false, Horizontal, "Datach - Battle Rush - Build Up Robot Tournament (Japan).nes" 985B1D05, NTSC, 3, 0, 2, 2, 0, false, Horizontal, "TwinBee (Japan).nes" 988798A8, NTSC, 4, 0, 1, 32, 0, false, Horizontal, "Mega Man 6 (USA).nes" 988B446D, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Lunar Pool (USA).nes" 988C290E, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Bard's Tale, The (Japan) (Sample).nes" 98977591, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Kage (Japan).nes" 989C1019, PAL, 4, 0, 32, 16, 0, false, Horizontal, "F-15 Strike Eagle (Europe).nes" 98A97A59, NTSC, 4, 0, 1, 32, 0, false, Horizontal, "Masuzoe Youichi - Asa Made Famicom (Japan).nes" 98C7B4DA, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Wanpaku Duck Yume Bouken (Japan) (Beta).nes" 98CCC9AB, NTSC, 68, 0, 0, 8, 0, false, Horizontal, "Maharaja (Japan).nes" 99083B3A, NTSC, 11, 0, 16, 8, 0, false, Vertical, "Joshua & the Battle of Jericho (USA) (v6.0) (Unl).nes" 990985C0, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "RoboCop 2 (USA) (Rev A).nes" 99240573, NTSC, 16, 0, 32, 16, 0, false, Horizontal, "Dragon Ball Z II - Gekishin Freeza!! (Japan).nes" 992AF039, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Hokkaidou Rensa Satsujin - Okhotsk ni Kiyu (Japan).nes" 99686DAD, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Chip to Dale no Daisakusen 2 (Japan).nes" 998422FC, PAL, 4, 0, 32, 16, 0, true, Horizontal, "StarTropics (Europe).nes" 9992F445, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Championship Bowling (Japan).nes" 999577B6, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Goonies II, The (USA).nes" 999584A8, PAL, 0, 0, 1, 2, 0, false, Horizontal, "Galaga (Europe).nes" 99A28276, PAL, 2, 0, 1, 8, 0, false, Vertical, "Robo Warrior (Europe).nes" 99A62E47, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Black Bass II, The (Japan).nes" 99A9F57E, NTSC, 1, 0, 4, 2, 0, false, Horizontal, "Anticipation (USA).nes" 99C395F9, NTSC, 33, 0, 0, 8, 0, false, Horizontal, "Captain Saver (Japan).nes" 99D15A91, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Spelunker (USA).nes" 99D38676, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Taito Chase H.Q. (Japan).nes" 99DDDB04, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Tiny Toon Adventures (USA).nes" 9A23A458, NTSC, 3, 0, 8, 2, 0, false, Horizontal, "AV Pachi-Slot (Japan) (Unl).nes" 9A2DB086, PAL, 0, 0, 1, 2, 0, false, Vertical, "Super Mario Bros. (Europe) (Rev A).nes" 9A808C3B, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Quarth (Japan).nes" 9AACD75D, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Sansuu 5 & 6 Nen - Keisan Game (Japan).nes" 9AB274AE, NTSC, 228, 0, 0, 16, 0, false, Vertical, "Cheetahmen II (USA) (Unl).nes" 9ACE456E, PAL, 0, 0, 16, 8, 0, false, Horizontal, "Silver Eagle (Asia) (PAL) (Unl).nes" 9ADFC8F0, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Ultraman Club - Kaijuu Daikessen!! (Japan).nes" 9B05B278, PAL, 4, 0, 16, 8, 0, false, Vertical, "World Champ - Super Boxing Great Fight (Europe).nes" 9B3C5124, NTSC, 4, 0, 16, 16, 0, true, Horizontal, "Pachio-kun 5 (Japan).nes" 9B506A48, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Wrecking Crew (World).nes" 9B53F848, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Shikinjou (Japan).nes" 9B568CC4, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Lemmings (Europe).nes" 9B821A83, NTSC, 1, 0, 1, 8, 0, true, Horizontal, "Bard's Tale, The (USA).nes" 9BAC73EF, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Uninvited (USA).nes" 9BD3F3C2, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Bugs Bunny Blowout, The (Europe).nes" 9BDCD892, NTSC, 4, 0, 32, 16, 0, true, Horizontal, "Might and Magic - Book One - Secret of the Inner Sanctum (Japan).nes" 9BDE3267, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Adventures of Dino Riki (USA).nes" 9C04C8D5, NTSC, 16, 0, 16, 8, 0, false, Horizontal, "Sakigake!! Otoko Juku - Shippuu Ichi Gou Sei (Japan).nes" 9C053F24, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Igo Shinan '93 (Japan).nes" 9C18762B, NTSC, 5, 0, 16, 16, 0, true, Horizontal, "Empereur, L' (USA).nes" 9C304DEC, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Hunt for Red October, The (Europe).nes" 9C3E8FC0, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Mezase Pachi Pro - Pachio-kun (Japan).nes" 9C521240, NTSC, 185, 0, 0, 2, 0, false, Horizontal, "Mighty Bomb Jack (Japan).nes" 9C537919, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Tetris 2 (USA).nes" 9C58F4A6, NTSC, 4, 0, 1, 32, 0, true, Horizontal, "Momotarou Densetsu Gaiden (Japan).nes" 9C924719, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Rockin' Kats (Europe).nes" 9C9F3571, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Track & Field (USA).nes" 9CBADC25, NTSC, 5, 0, 32, 32, 0, true, Horizontal, "Just Breed (Japan).nes" 9CBB0291, NTSC, 4, 0, 8, 4, 0, false, Vertical, "Super Sprint (Japan).nes" 9CBC8253, NTSC, 4, 0, 4, 8, 0, false, Horizontal, "Family Circuit (Japan).nes" 9CFA55E7, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Famicom Meijin Sen (Japan).nes" 9D048EA4, NTSC, 96, 0, 0, 8, 0, false, Vertical, "Oeka Kids - Anpanman to Oekaki Shiyou!! (Japan).nes" 9D21FE96, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Lupin Sansei - Pandora no Isan (Japan).nes" 9D34EDC5, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Musashi no Ken - Tadaima Shugyou Chuu (Japan).nes" 9D38F8F9, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Pac-Man (USA) (Tengen) (Unl).nes" 9D45D8EC, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Emoyan no 10 Bai Pro Yakyuu (Japan).nes" 9D779B08, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Donkey Kong (USA) (GameCube Edition).nes" 9D976153, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Akagawa Jirou no Yuurei Ressha (Japan).nes" 9D9A4A26, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Karakuri Kengou Den Musashi Lord - Karakuribito Hashiru (Japan).nes" 9DC96EC7, NTSC, 18, 0, 16, 16, 0, false, Horizontal, "Moe Pro! '90 - Kandou Hen (Japan).nes" 9DDF9017, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "G.I. Joe - A Real American Hero - The Atlantis Factor (USA) (Beta).nes" 9DF58E80, NTSC, 1, 0, 8, 16, 0, false, Horizontal, "Miracle Piano Teaching System, The (France).nes" 9E356267, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Morita Shougi (Japan).nes" 9E36080E, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Meimon! Takonishi Ouendan - Kouha 6 Nin Shuu (Japan).nes" 9E379698, NTSC, 71, 0, 1, 16, 0, false, Vertical, "Linus Spacehead's Cosmic Crusade (USA) (En,Fr,De,Es) (Unl).nes" 9E382EBF, NTSC, 1, 0, 4, 4, 0, false, Horizontal, "Dance Aerobics (USA).nes" 9E4701CB, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Mickey Mouse - Fushigi no Kuni no Daibouken (Japan).nes" 9E4E9CC2, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Pac-Man (USA) (Namco).nes" 9E6092A4, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Tiny Toon Adventures Cartoon Workshop (USA).nes" 9E66A66B, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Dynamite Bowl (Japan) (Rev A).nes" 9E777EA5, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Ninja Cop Saizou (Japan).nes" 9E898385, NTSC, 65, 0, 0, 8, 0, false, Horizontal, "Kaiketsu Yancha Maru 3 - Taiketsu! Zouringen (Japan).nes" 9EA1DC76, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Rainbow Islands (USA).nes" 9EBDC94E, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Hototogisu (Japan).nes" 9ECB9DCD, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Perfect Bowling (Japan).nes" 9EDBE2E2, NTSC, 19, 0, 16, 8, 0, false, Horizontal, "Rolling Thunder (Japan).nes" 9EDD2159, NTSC, 7, 0, 1, 16, 0, false, Horizontal, "R.C. Pro-Am II (USA).nes" 9EE83916, NTSC, 72, 0, 0, 8, 0, false, Vertical, "Moero!! Juudou Warriors (Japan).nes" 9EEFB4B4, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Pachi-Slot Adventure 2 - Sorotta-kun no Pachi-Slot Tanteidan (Japan).nes" 9EF351DC, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Sansuu 5 & 6 Nen - Keisan Game (Japan) (Beta).nes" 9EFF96D2, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Raf World (Japan).nes" 9F01687D, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Shadowgate (France).nes" 9F03B11F, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Moero TwinBee - Cinnamon Hakase o Sukue! (Japan).nes" 9F2712DF, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Cosmic Wars (Japan).nes" 9F2EEF20, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Blues Brothers, The (USA).nes" 9F432594, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Starship Hector (USA).nes" 9F5138CB, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Super Rugby (Japan).nes" 9F6C119C, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Remote Control (USA).nes" 9F6CE171, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Ultimate Basketball (USA).nes" 9F8336DB, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Miracle Ropit's - 2100 Nen no Daibouken (Japan).nes" 9FAE4D46, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Ide Yousuke Meijin no Jissen Mahjong (Japan) (Rev A).nes" 9FB32923, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Godzilla 2 - War of the Monsters (USA).nes" 9FD35802, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Buggy Popper (Japan).nes" 9FD718FD, NTSC, 4, 0, 1, 32, 0, false, Horizontal, "Gorilla Man, The (Japan).nes" 9FFE2F55, NTSC, 1, 0, 16, 4, 0, false, Horizontal, "Sky Shark (USA) (Rev 0A).nes" A0006B26, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Golgo 13 - Daiisshou - Kamigami no Tasogare (Japan).nes" A0230D75, NTSC, 2, 0, 1, 8, 0, false, Vertical, "WWF Wrestlemania Challenge (USA).nes" A038AFF2, PAL, 4, 0, 16, 8, 0, false, Vertical, "Tiny Toon Adventures (Europe).nes" A03A422B, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Major League Baseball (USA).nes" A045FE1D, PAL, 232, 0, 0, 16, 0, false, Vertical, "Super Sports Challenge (Europe) (Unl).nes" A0568E1D, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Renegade (USA).nes" A058219D, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Taro's Quest (USA) (Proto).nes" A07C1F81, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Kinnikuman - Muscle Tag Match (Japan) (Rev 1).nes" A08B4701, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Kinnikuman - Muscle Tag Match (Japan).nes" A0A095C4, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Bump'n'Jump (USA).nes" A0A5A0B9, PAL, 4, 0, 16, 8, 0, false, Horizontal, "James Bond Jr (Europe).nes" A0B0B742, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Super Mario Bros. 3 (USA).nes" A0C31A57, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Indiana Jones and the Temple of Doom (USA).nes" A0DF4B8F, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Krusty's Fun House (USA).nes" A0F99BB8, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Akumajou Dracula (Japan).nes" A166548F, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Dragon Fighter (USA).nes" A189843D, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Magic Jewelry (Asia) (Unl).nes" A1A0C13F, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Double Dragon (Europe).nes" A1C0DA00, PAL, 0, 0, 1, 2, 0, false, Vertical, "Mario Bros. Classic (Europe).nes" A1DC16C0, NTSC, 0, 0, 64, 32, 0, false, FourScreen, "Street Heroes (Asia) (Unl).nes" A1F90826, PAL, 1, 0, 4, 8, 0, false, Vertical, "Air Fortress (Europe).nes" A1FF4E1D, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Swamp Thing (USA).nes" A20B4983, NTSC, 0, 0, 8, 8, 0, false, Horizontal, "Popo Team (Asia) (Unl).nes" A2194CAD, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Legendary Wings (USA).nes" A21E675C, NTSC, 34, 0, 1, 8, 0, false, Horizontal, "Mashou (Japan).nes" A222F5A0, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Ninja-kun - Majou no Bouken (Japan).nes" A22657FA, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Nintendo World Cup (USA).nes" A23CB659, NTSC, 79, 0, 4, 2, 0, false, Vertical, "Volley Ball (Spain) (Gluk Video) (Unl).nes" A23F0A27, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Exerion (Japan) (En) (Proto) [b].nes" A2469526, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Xevious (Japan) (En) (Rev 1).nes" A25A750F, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Ultima - Quest of the Avatar (USA).nes" A2623BC1, NTSC, 68, 0, 0, 8, 0, true, Horizontal, "Nantettatte!! Baseball (Japan).nes" A262A81F, NTSC, 16, 0, 32, 16, 0, false, Horizontal, "Rokudenashi Blues (Japan).nes" A2AF25D0, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Defender II (USA).nes" A2D074F5, PAL, 0, 0, 2, 1, 0, false, Horizontal, "Lucky Bingo 777 (Asia) (PAL) (Unl).nes" A2F713C0, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "SD Gundam - Gachapon Senshi 2 - Capsule Senki (Japan).nes" A31142FF, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Adventures of Rad Gravity, The (Europe).nes" A342A5FD, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Rambo (USA).nes" A38857EB, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Lipple Island (Japan).nes" A3BF2ADA, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Hero Quest (USA) (Proto).nes" A3C0D49F, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Classic Concentration (USA).nes" A4062017, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Ultima - Exodus (USA).nes" A46D7F02, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Astyanax (USA) (Beta).nes" A485ABED, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Ferrari (Japan).nes" A48D26C1, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Caesars Palace (USA) (Beta).nes" A49253C6, NTSC, 4, 0, 8, 4, 0, false, Horizontal, "Family Tennis (Japan).nes" A49B48B8, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Dragon Quest III - Soshite Densetsu e... (Japan) (Rev 0A).nes" A4BDCC1D, PAL, 1, 0, 1, 8, 0, true, Horizontal, "Elite (Europe) (En,Fr,De).nes" A4DCDF28, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Kujaku Ou II (Japan).nes" A4DCF72E, PAL, 4, 0, 32, 16, 0, false, Horizontal, "Mega Man 5 (Europe).nes" A4E935DF, NTSC, 69, 0, 32, 16, 0, false, Horizontal, "Honoo no Toukyuuji - Dodge Danpei 2 (Japan).nes" A547A6EC, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Raid on Bungeling Bay (Japan) (En) (Rev A).nes" A55701DD, NTSC, 11, 0, 16, 8, 0, false, Vertical, "King of Kings, The (USA) (v1.1) (Unl).nes" A558FB52, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Destiny of an Emperor (USA).nes" A55FA397, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Back to the Future (USA).nes" A56208A0, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Jongbou (Japan).nes" A58A8DA1, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Ushio to Tora - Shinen no Taiyou (Japan).nes" A5E6BAF9, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Dragon Slayer 4 - Drasle Family (Japan).nes" A5E89675, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Captain Comic - The Adventure (USA) (Unl).nes" A5E8D2CD, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "BreakThru (USA).nes" A60CA3D6, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "Nightshade (USA).nes" A60FBA51, NTSC, 4, 0, 1, 32, 0, true, Horizontal, "Double Moon Densetsu (Japan).nes" A6153536, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Attack of the Killer Tomatoes (Europe).nes" A6638CBA, PAL, 1, 0, 1, 16, 0, false, Horizontal, "Mega Man 2 (Europe).nes" A6648353, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Conflict (Japan).nes" A66596D9, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Sou Setsu Ryuu II - The Revenge (Japan).nes" A69A1F2A, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Donkey Kong Jr. (USA) (GameCube Edition).nes" A69F29FA, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Lemmings (USA).nes" A6A725B8, NTSC, 2, 0, 1, 16, 0, false, Vertical, "Best of the Best - Championship Karate (USA).nes" A713DD30, PAL, 69, 0, 16, 16, 0, false, Vertical, "Mr. Gimmick (Europe).nes" A725B2D3, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Defenders of Dynatron City (USA).nes" A72FDE03, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Densetsu no Kishi - Elrond (Japan).nes" A781FFAA, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Free Fall (USA) (Proto).nes" A7B0536C, NTSC, 33, 0, 0, 8, 0, false, Horizontal, "Don Doko Don 2 (Japan).nes" A7D3635E, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Nekketsu Kouha Kunio-kun (Japan).nes" A7DE65E4, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Predator (USA).nes" A7E784ED, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Igo Shinan '94 (Japan).nes" A7EF8F80, NTSC, 0, 0, 32, 8, 0, false, Horizontal, "Gaiapolis (Asia) (Unl).nes" A80A0F01, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Incredible Crash Dummies, The (USA).nes" A80FA181, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Faxanadu (Japan).nes" A851CAE9, NTSC, 16, 0, 32, 8, 0, false, Horizontal, "Nishimura Kyoutarou Mystery - Blue Train Satsujin Jiken (Japan).nes" A86A5318, NTSC, 1, 0, 1, 32, 0, true, Horizontal, "Dragon Warrior III (USA).nes" A8784932, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Gun.Smoke (USA).nes" A8923256, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Snoopy's Silly Sports Spectacular! (USA).nes" A8A9B982, NTSC, 185, 0, 0, 1, 0, false, Vertical, "Bird Week (Japan).nes" A8B0DA56, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Race America (USA).nes" A8D93537, PAL, 1, 0, 16, 8, 0, false, Horizontal, "RoboCop 3 (Europe).nes" A8E6A7C2, PAL, 0, 0, 8, 4, 0, false, Horizontal, "Mahjong World, The - Ma Que Shi Jie (Asia) (PAL) (Unl).nes" A8F4D99E, NTSC, 1, 0, 4, 2, 0, false, Horizontal, "Boulder Dash (USA).nes" A8F5C2AB, NTSC, 4, 0, 4, 4, 0, false, Vertical, "Vindicators (USA) (Unl).nes" A9068D17, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Virus (USA) (Beta) (1989).nes" A91460B8, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Solstice (Japan).nes" A9217EA2, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "Teenage Mutant Ninja Turtles II - The Arcade Game (USA).nes" A923E441, NTSC, 133, 0, 0, 4, 0, false, Horizontal, "Jovial Race (Unknown) (Unl).nes" A93527E2, PAL, 2, 0, 1, 8, 0, false, Vertical, "Castlevania (Europe).nes" A9415562, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Kid Niki - Radical Ninja (USA) (Rev A).nes" A94591B0, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Contra Force (USA).nes" A9541452, NTSC, 16, 0, 32, 16, 0, false, Horizontal, "Dragon Ball Z II - Gekishin Freeza!! (Japan) (Rev 1).nes" A95A915A, NTSC, 243, 0, 0, 2, 0, false, Horizontal, "Tasac (Asia) (Unl).nes" A9660690, PAL, 1, 0, 16, 8, 0, false, Vertical, "Snow Brothers (Europe).nes" A97567A4, PAL, 1, 0, 1, 8, 0, false, Horizontal, "Battle of Olympus, The (Europe).nes" A98046B8, NTSC, 10, 0, 16, 16, 0, true, Horizontal, "Fire Emblem Gaiden (Japan).nes" A9842027, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Mahjong (Japan) (Rev A).nes" A99016C6, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Rollerball (Australia).nes" A9BBF44F, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Pac-Man (USA) (Tengen).nes" AA20F73D, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Shatterhand (USA).nes" AA4318AE, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "I Love Softball (Japan).nes" AA4997C1, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Rollergames (USA).nes" AA6BB985, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Kid Kool and the Quest for the Seven Wonder Herbs (USA).nes" AA74A4D8, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Kung-Fu Heroes (USA).nes" AA9F9765, NTSC, 23, 0, 0, 8, 0, false, Vertical, "Mad City (Japan) (Beta).nes" AAA985D7, PAL, 1, 0, 16, 8, 0, false, Vertical, "Swamp Thing (Europe).nes" AAC2E75E, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Mighty Bomb Jack (USA).nes" AAED295C, NTSC, 1, 0, 4, 2, 0, false, Horizontal, "R.C. Pro-Am (USA) (Rev A).nes" AAEF2264, NTSC, 168, 0, 0, 4, 0, true, Horizontal, "Racermate Challenge II (USA) (v5.01.033) (Unl).nes" AAF49344, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Snow Bros. (Japan).nes" AB2006B4, PAL, 3, 0, 2, 2, 0, false, Vertical, "Donkey Kong Classics (USA, Europe).nes" AB2AC325, PAL, 1, 0, 16, 8, 0, false, Vertical, "David Crane's A Boy and His Blob - Trouble on Blobolonia (Europe).nes" AB41445E, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Super Spy Hunter (USA).nes" AB47A50E, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Ikari (Japan).nes" AB671224, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Journey to Silius (Europe).nes" ABAA6F78, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Matsumoto Tooru no Kabushiki Hisshou Gaku - Vol. 1 (Japan).nes" ABBF7217, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Sangokushi (Japan).nes" AC136F2D, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "King's Knight (Japan).nes" AC3E5677, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Hudson Hawk (Europe).nes" AC4BF9DC, NTSC, 75, 0, 0, 8, 0, false, Vertical, "Exciting Boxing (Japan).nes" AC609320, PAL, 4, 0, 32, 8, 0, false, Horizontal, "Flintstones, The - The Rescue of Dino & Hoppy (Europe).nes" AC652B47, NTSC, 73, 0, 0, 8, 0, false, Vertical, "Salamander (Japan).nes" AC8DCDEA, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Cybernoid - The Fighting Machine (USA).nes" AC92E9E0, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Brush Roller (Asia) (Unl).nes" AC97AA09, NTSC, 172, 0, 0, 2, 0, false, Vertical, "1991 Du Ma Racing (Asia) (Unl).nes" AC9895CC, NTSC, 23, 0, 0, 8, 0, false, Horizontal, "Dragon Scroll - Yomigaerishi Maryuu (Japan).nes" ACA145D8, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Karate Champ (USA).nes" ACA15643, NTSC, 5, 0, 16, 32, 0, true, Horizontal, "Uncharted Waters (USA).nes" ACE56F39, NTSC, 19, 0, 16, 8, 0, false, Horizontal, "Mindseeker (Japan).nes" AD0394F0, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Roundball - 2-on-2 Challenge (Europe).nes" AD12A34F, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Tecmo Baseball (USA).nes" AD9C63E2, NTSC, 70, 0, 0, 4, 0, false, Horizontal, "Space Shadow (Japan).nes" ADA1B12F, NTSC, 3, 0, 8, 2, 0, false, Horizontal, "Hot Slots (Asia) (Unl).nes" ADA40FB2, NTSC, 1, 0, 16, 8, 0, false, Vertical, "Arcadia VI (USA) (Proto).nes" ADB5D0B3, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Championship Lode Runner (Japan).nes" ADB810F8, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Woody Poko (Japan).nes" ADF606F6, NTSC, 33, 0, 0, 16, 0, false, Horizontal, "Bakushou!! Jinsei Gekijou (Japan).nes" ADFAD6B6, NTSC, 188, 0, 0, 8, 0, false, Horizontal, "Karaoke Studio (Japan).nes" ADFFD64F, NTSC, 210, 2, 16, 8, 0, false, Vertical, "Famista '93 (Japan).nes" AE128FAC, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Law of the West (Japan).nes" AE280E20, NTSC, 4, 0, 1, 16, 0, false, Horizontal, "Shougi Meikan '93 (Japan).nes" AE321339, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Pro Yakyuu - Family Stadium '88 (Japan).nes" AE52DECE, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Millipede (USA).nes" AE5C3D94, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Baseball Star - Mezase Sankanou!! (Japan).nes" AE64CA77, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Ice Hockey (USA).nes" AE7DF77F, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "M.C. Kids (USA) (Beta).nes" AE8666B4, NTSC, 3, 0, 4, 2, 0, false, Vertical, "City Connection (USA).nes" AE97627C, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Bugs Bunny Fun House (USA) (Beta).nes" AE9F33D0, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "North and South (USA).nes" AEB2D754, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Double Dragon II - The Revenge (Europe).nes" AEB7FCE9, NTSC, 33, 0, 0, 8, 0, false, Horizontal, "Power Blazer (Japan).nes" AEBD6549, NTSC, 33, 0, 0, 16, 0, false, Horizontal, "Bakushou!! Jinsei Gekijou 3 (Japan).nes" AF05F37E, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "George Foreman's KO Boxing (USA).nes" AF4010EA, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "World Class Track Meet (USA) (Rev A).nes" AF5676DE, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Lode Runner (USA).nes" AF65AA84, PAL, 4, 0, 16, 8, 0, false, Vertical, "Low G Man - The Low Gravity Man (Europe).nes" AF6B5B85, NTSC, 1, 0, 1, 16, 0, true, Vertical, "Sweet Home (Japan) (Beta).nes" AFB46DD6, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Thundercade (USA).nes" AFC32114, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Safety Rally (Japan).nes" AFDCBD24, PAL, 0, 0, 1, 1, 0, false, Horizontal, "Baseball (USA, Europe).nes" B0480AE9, NTSC, 5, 0, 16, 8, 0, false, Horizontal, "Laser Invasion (USA).nes" B049A8C4, NTSC, 16, 0, 32, 16, 0, true, Horizontal, "SD Gundam Gaiden - Knight Gundam Monogatari 2 - Hikari no Knight (Japan).nes" B04BA659, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Black Bass, The (Japan).nes" B06C0674, NTSC, 75, 0, 0, 8, 0, false, Vertical, "King Kong 2 - Ikari no Megaton Punch (Japan).nes" B0874760, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Bowling (Unknown) (Proto).nes" B0BC46D1, NTSC, 66, 0, 4, 8, 0, false, Vertical, "Dragon Ball - Le Secret du Dragon (France) (Rev A).nes" B0CD000F, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Wayne's World (USA).nes" B0EBF3DB, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "M.C. Kids (USA).nes" B1250D0C, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Super Contra (Japan).nes" B134D713, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Abadox - The Deadly Inner War (USA).nes" B13F00D4, PAL, 2, 0, 1, 8, 0, false, Vertical, "Probotector (Europe).nes" B14EA4D2, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Iron Tank - The Invasion of Normandy (USA).nes" B15653BD, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Heracles no Eikou - Toujin Makyou Den (Japan).nes" B1612FE6, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Xexyz (USA).nes" B1723338, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Star Voyager (USA).nes" B174B680, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Dig Dug (Japan).nes" B17574F3, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Advanced Dungeons & Dragons - Heroes of the Lance (USA).nes" B19A55DD, NTSC, 64, 0, 0, 4, 0, false, Horizontal, "Road Runner (USA) (Unl).nes" B1A94B82, NTSC, 70, 0, 0, 8, 0, false, Horizontal, "Pocket Zaurus - Juu Ouken no Nazo (Japan).nes" B1B16B8A, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Wit's (Japan).nes" B1C937C8, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Parasol Stars - Rainbow Islands II (Europe).nes" B20C1030, NTSC, 93, 0, 0, 8, 0, false, Vertical, "Shanghai (Japan).nes" B2530AFC, NTSC, 0, 0, 1, 2, 0, true, Vertical, "Family BASIC (Japan) (v3.0).nes" B2781C19, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Tom & Jerry - The Ultimate Game of Cat and Mouse! (Europe).nes" B27B8CF4, NTSC, 23, 0, 0, 8, 0, false, Horizontal, "Contra (Japan).nes" B297B5E7, NTSC, 92, 0, 0, 16, 0, false, Vertical, "Moero!! Pro Yakyuu '88 - Ketteiban (Japan).nes" B2EF7F4B, NTSC, 4, 0, 32, 32, 0, true, Vertical, "Kirby's Adventure (France).nes" B30599A1, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Star Wars (USA) (Beta).nes" B3769A51, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Platoon (USA).nes" B3783F2A, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Rygar (USA).nes" B3974D6C, NTSC, 1, 0, 8, 16, 0, false, Horizontal, "Miracle Piano Teaching System, The (Germany).nes" B39A3F5B, NTSC, 2, 0, 1, 8, 0, false, Vertical, "JJ (Japan).nes" B3C30BEA, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Xevious (Japan).nes" B3D74C0D, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Donkey Kong 3 (World).nes" B400172A, PAL, 2, 0, 1, 8, 0, false, Vertical, "California Games (Europe).nes" B4113F3C, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Hatris (Japan).nes" B422A67A, NTSC, 64, 0, 0, 8, 0, false, Horizontal, "Skull & Crossbones (USA) (Unl).nes" B4241FCC, NTSC, 1, 0, 8, 16, 0, false, Horizontal, "Bases Loaded (USA).nes" B459EDC4, NTSC, 66, 0, 4, 2, 0, false, Vertical, "Uforce Power Games (USA) (Proto 1).nes" B462718E, PAL, 232, 0, 0, 16, 0, false, Vertical, "Super Sports Challenge (Europe) (Plug-Thru Cart) (Unl).nes" B462BF6F, PAL, 3, 0, 2, 2, 0, false, Horizontal, "Mighty Bomb Jack (Europe).nes" B4735FAC, NTSC, 5, 0, 64, 32, 0, true, Horizontal, "Metal Slader Glory (Japan).nes" B47569E2, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Happy Birthday Bugs (Japan).nes" B4801882, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Secret Ties (USA) (Proto).nes" B4BADF56, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Tanque (Spain) (Gluk Video) (Unl).nes" B4C81ADB, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Indiana Jones and the Temple of Doom (USA) (Rev A).nes" B4CDF95F, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "RoboCop 2 (USA).nes" B4E4879E, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Ghosts'n Goblins (USA).nes" B4FF91E7, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Garfield - A Week of Garfield (Japan).nes" B5576820, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Momotarou Densetsu (Japan).nes" B5D10D5C, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Jeopardy! (USA).nes" B5D28EA2, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Mystery Quest (USA).nes" B5E24324, NTSC, 4, 0, 1, 8, 0, false, Horizontal, "Ninja Crusaders - Ryuuga (Japan).nes" B5E392E2, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Little Samson (USA).nes" B5E83C9A, NTSC, 178, 0, 0, 32, 0, true, Horizontal, "Xing Ji Zheng Ba (China) (Unl).nes" B5F7E661, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "World Grand-Prix - Pole to Finish (Japan).nes" B5FF71AB, NTSC, 19, 0, 16, 8, 0, false, Horizontal, "Battle Fleet (Japan).nes" B629D555, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Totally Rad (USA).nes" B64078F3, NTSC, 4, 0, 16, 8, 0, true, Vertical, "Shadowgate (Germany).nes" B6661BDA, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Wall Street Kid (USA).nes" B668C7FC, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Castlevania (USA) (Rev A).nes" B67D16F6, PAL, 1, 0, 1, 16, 0, false, Vertical, "Robin Hood - Prince of Thieves (Europe).nes" B683A856, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Blaster Master (USA) (Beta).nes" B68F9814, PAL, 4, 0, 16, 8, 0, false, Vertical, "Astyanax (Europe).nes" B69F7C0F, NTSC, 3, 0, 1, 2, 0, false, Horizontal, "Fire Dragon (Asia) (Unl).nes" B6A2B981, NTSC, 79, 0, 4, 2, 0, false, Vertical, "Solitaire (USA) (Unl).nes" B6A727FA, NTSC, 113, 0, 8, 2, 0, false, Horizontal, "Papillon (Asia) (Unl).nes" B6B5C372, PAL, 2, 0, 1, 8, 0, false, Vertical, "Trog! (Europe).nes" B6BF5137, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Fester's Quest (USA).nes" B6D2D300, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Ms. Pac-Man (USA).nes" B70129F4, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Tokkyuu Shirei Solbrain (Japan).nes" B7773A07, NTSC, 4, 0, 1, 32, 0, false, Horizontal, "Aa Yakyuu Jinsei Icchokusen (Japan).nes" B780521C, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Ninja Gaiden II - The Dark Sword of Chaos (USA).nes" B786AB95, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Dynamite Bowl (Japan).nes" B786C2AC, NTSC, 11, 0, 16, 8, 0, false, Vertical, "Spiritual Warfare (USA) (v6.0) (Unl).nes" B79C320D, PAL, 2, 0, 1, 8, 0, false, Horizontal, "Gun.Smoke (Europe).nes" B79F2651, NTSC, 0, 0, 4, 2, 0, false, Vertical, "Chiller (USA) (Unl).nes" B7D69A6D, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Magician (USA) (Beta 1).nes" B7F28915, NTSC, 16, 0, 16, 8, 0, true, Horizontal, "Magical Taruruuto-kun 2 - Mahou Daibouken (Japan).nes" B7F39933, PAL, 2, 0, 1, 8, 0, false, Vertical, "Prince of Persia (Europe).nes" B80192B7, PAL, 2, 0, 1, 8, 0, false, Horizontal, "Jimmy Connors Tennis (Europe).nes" B811C054, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Fleet Commander (Japan).nes" B834EB30, NTSC, 2, 0, 1, 8, 0, false, Vertical, "City Adventure Touch - Mystery of Triangle (Japan).nes" B843EB84, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Silent Service (USA) (Rev A).nes" B84A73CC, NTSC, 4, 0, 1, 32, 0, true, Horizontal, "Tenchi o Kurau II - Shokatsu Koumei Den (Japan) (Rev A).nes" B8747ABF, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Best Play Pro Yakyuu Special (Japan).nes" B87AB35A, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Circus Charlie (Japan).nes" B89888C9, NTSC, 232, 0, 0, 16, 0, false, Vertical, "Quattro Adventure (USA) (Unl).nes" B8B9ACA3, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Wild Gunman (Japan, USA).nes" B8DAD5D2, NTSC, 0, 0, 8, 4, 0, false, Horizontal, "Mahjan Samit Kabukicho Hen (Asia) (Unl).nes" B918580C, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Treasure Master (USA).nes" B9582F60, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Kidou Senshi Z Gundam - Hot Scramble (Japan) (Final Version).nes" B95E9E7F, NTSC, 9, 0, 16, 8, 0, false, Horizontal, "Punch-Out!! (USA).nes" B96F8321, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Hottarman no Chitei Tanken (Japan) (Beta).nes" B976219A, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Jumpin' Kid - Jack to Mame no Ki Monogatari (Japan).nes" B9762DA8, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Fisher-Price - Perfect Fit (USA).nes" B979CAD5, NTSC, 32, 0, 0, 8, 0, false, Horizontal, "Kaiketsu Yancha Maru 2 - Karakuri Land (Japan).nes" B97BFDD7, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Exploding Fist (USA) (Proto 1).nes" B9AB06AA, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Money Game II, The - Kabutochou no Kiseki (Japan).nes" B9B4D9E0, NTSC, 118, 0, 0, 8, 0, false, Horizontal, "NES Play Action Football (USA).nes" B9CF171F, NTSC, 1, 0, 16, 16, 0, false, Horizontal, "Bases Loaded II - Second Season (USA).nes" B9DC755E, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Ikinari Musician (Japan) (Beta).nes" BA322865, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Zelda II - The Adventure of Link (USA).nes" BA327FD9, PAL, 69, 0, 32, 8, 0, false, Horizontal, "Batman - Return of the Joker (Europe).nes" BA43568A, NTSC, 80, 0, 0, 8, 0, false, Horizontal, "Kyonshiizu 2 (Japan) (Sample).nes" BA51AC6F, NTSC, 78, 0, 0, 8, 0, false, FourScreen, "Holy Diver (Japan).nes" BA58ED29, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "'89 Dennou Kyuusei Uranai (Japan).nes" BA766EC6, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Heracles no Eikou II - Titan no Metsubou (Japan).nes" BAACF521, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Bloque Magico, El (Spain) (Gluk Video) (Unl).nes" BACA10A9, NTSC, 33, 0, 0, 8, 0, false, Horizontal, "Golfkko Open (Japan).nes" BAD36C17, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Star Wars (Japan) (Victor).nes" BAEBA201, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Mario Open Golf (Japan).nes" BB0F2D56, NTSC, 132, 0, 0, 4, 0, false, Vertical, "Creatom (Spain) (Gluk Video) (Unl).nes" BB435255, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Shinsenden (Japan).nes" BB6D7949, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "Teenage Mutant Ninja Turtles III - The Manhattan Project (USA).nes" BB7F829A, NTSC, 5, 0, 16, 8, 0, false, Horizontal, "Uchuu Keibitai SDF (Japan).nes" BBA58BE5, NTSC, 70, 0, 0, 8, 0, false, Horizontal, "Family Trainer 6 - Manhattan Police (Japan).nes" BBB710D9, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "F-15 Strike Eagle (France) (En,Fr,Nl).nes" BBE40DC4, NTSC, 11, 0, 2, 4, 0, false, Vertical, "Baby Boomer (USA) (Unl).nes" BBED6E6E, NTSC, 3, 0, 2, 2, 0, false, Horizontal, "Legend of Kage, The (USA).nes" BBF464EB, PAL, 0, 0, 2, 4, 0, false, Vertical, "Pyramid II (Asia) (PAL) (Unl).nes" BBFE23F4, PAL, 4, 0, 16, 8, 0, false, Vertical, "Panic Restaurant (Europe).nes" BC06543C, NTSC, 0, 0, 1, 1, 0, true, Vertical, "Booky Man (Spain) (Gluk Video) (Unl).nes" BC11E61A, NTSC, 19, 0, 16, 8, 0, false, Horizontal, "Kaijuu Monogatari (Japan).nes" BC25A18B, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Shadow Warriors II - Ninja Gaiden II (Europe).nes" BC7364BB, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Wait and See (Russia) (Unl).nes" BC7485B5, NTSC, 1, 0, 1, 8, 0, true, Horizontal, "Elite (Unknown) (NTSC Demo).nes" BC7B1D0F, NTSC, 33, 0, 0, 8, 0, false, Horizontal, "Bakushou!! Jinsei Gekijou 2 (Japan).nes" BC7FEDB9, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Little Ninja Brothers (USA).nes" BC80FB52, NTSC, 5, 0, 32, 16, 0, true, Horizontal, "Royal Blood (Japan).nes" BC9BFFCB, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Adventures of Bayou Billy, The (Europe).nes" BCACBBF4, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Metal Storm (USA).nes" BCCFEF1C, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Kabuki - Quantum Fighter (Europe).nes" BCE77871, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Pipe Dream (USA).nes" BCF68611, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Great Deal (Japan).nes" BD018F0F, PAL, 0, 0, 1, 2, 0, false, Vertical, "Dancing Blocks (Asia) (PAL) (Unl).nes" BD139BE7, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Escape from Atlantis, The (USA) (Proto 1) (Unl).nes" BD154C3E, NTSC, 71, 0, 1, 16, 0, false, Vertical, "Big Nose the Caveman (USA) (Unl).nes" BD29178A, NTSC, 79, 0, 4, 2, 0, false, Horizontal, "Dudes with Attitude (USA) (Unl).nes" BD339E75, PAL, 1, 0, 16, 8, 0, false, Vertical, "Best of the Best - Championship Karate (Europe).nes" BD50F230, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Doki!Doki! Yuuenchi - Crazy Land Daisakusen (Japan).nes" BD523011, NTSC, 19, 0, 32, 16, 0, false, Horizontal, "Namco Prism Zone - Dream Master (Japan).nes" BD9D0E85, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Bomber King (Japan).nes" BDA183BB, NTSC, 4, 0, 32, 16, 0, false, Vertical, "Teenage Mutant Ninja Turtles II - The Arcade Game (Unknown) (Beta).nes" BDA7925E, NTSC, 87, 0, 0, 2, 0, false, Horizontal, "Kage no Densetsu (Japan).nes" BDA8F8E4, NTSC, 70, 0, 0, 8, 0, false, Horizontal, "Gegege no Kitarou 2 - Youkai Gundan no Chousen (Japan).nes" BDC124E5, NTSC, 4, 0, 1, 16, 0, false, Horizontal, "Shaffle Fight (Japan).nes" BDE3AE9B, NTSC, 66, 0, 4, 8, 0, false, Vertical, "Doraemon (Japan).nes" BDE7A7B5, NTSC, 113, 0, 4, 2, 0, false, Horizontal, "Rad Racket - Deluxe Tennis II (USA) (Unl).nes" BDE93999, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Dynowarz - Destruction of Spondylus (USA).nes" BDF046EF, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Cabal (USA).nes" BE06853F, NTSC, 16, 0, 1, 16, 0, false, Horizontal, "Datach - J League Super Top Players (Japan).nes" BE0E93C3, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Rainbow Islands - Bubble Bobble 2 (Europe).nes" BE17E27B, NTSC, 243, 0, 0, 4, 0, false, Horizontal, "Poke III (Asia) (Unl).nes" BE250388, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Capcom's Gold Medal Challenge '92 (USA).nes" BE387AF0, NTSC, 3, 0, 2, 1, 0, false, Horizontal, "Joust (USA).nes" BE3BF3B3, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Shingen the Ruler (USA).nes" BE95B219, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Tatakai no Banka (Japan).nes" BEB15855, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Friday the 13th (USA).nes" BEB30478, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Exerion (Japan) (En).nes" BEB8AB01, PAL, 66, 0, 4, 8, 0, false, Vertical, "Gumshoe (USA, Europe).nes" BED47813, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Rollergames (Europe).nes" BEE1C0D9, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Silver Surfer (USA).nes" BEE30C5F, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Cross Fire (USA) (Proto).nes" BEE54426, NTSC, 79, 0, 8, 4, 0, false, Vertical, "Deathbots (USA) (Unl).nes" BEFE5480, NTSC, 177, 0, 0, 64, 0, true, Horizontal, "Xing Zhan Qing Yuan (China) (Unl).nes" BF0C485D, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Jackpot (Australia) (Unl).nes" BF250AF2, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Tag Team Wrestling (USA).nes" BF3635CF, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Makai Mura (Japan).nes" BF4F4BA6, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Castle Quest (Japan).nes" BF700470, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Addams Family, The (Europe) (En,Fr,De).nes" BF7F54B4, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Baseball Fighter (Japan).nes" BF888B75, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Hook (Europe).nes" BF93112A, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Metal Flame Psybuster (Japan).nes" BFBFD25D, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Adventure Island 3 (USA).nes" C0103592, PAL, 1, 0, 16, 16, 0, false, Horizontal, "Goal! (Europe).nes" C05A365B, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Chou Fuyuu Yousai Exed Exes (Japan).nes" C05A63B2, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Super Spike V'Ball (Europe).nes" C060ED0A, NTSC, 1, 0, 16, 16, 0, false, Horizontal, "Grand Master (Japan).nes" C06FACFC, PAL, 243, 0, 0, 2, 0, false, Horizontal, "Strategist (Asia) (PAL) (Unl).nes" C09227A0, NTSC, 4, 0, 16, 8, 0, false, Vertical, "RoboCop (Japan).nes" C0B23520, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Q-bert (USA).nes" C0EDEDD0, PAL, 2, 0, 1, 8, 0, false, Vertical, "Blades of Steel (Europe).nes" C0F251EA, PAL, 4, 0, 32, 16, 0, false, Horizontal, "Ultimate Air Combat (Europe) (En,Fr,De).nes" C115A022, PAL, 4, 0, 16, 8, 0, false, Vertical, "Rampart (Europe).nes" C1719664, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Tatakae!! Rahmen Man - Sakuretsu Choujin 102 Gei (Japan).nes" C1B43207, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Danny Sullivan's Indy Heat (USA).nes" C1B79B14, NTSC, 146, 0, 4, 2, 0, false, Vertical, "Shuang Ying (Asia) (Unl).nes" C1BA8BB9, NTSC, 4, 0, 1, 16, 0, true, Horizontal, "Project Q (Japan).nes" C1C3636B, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Star Wars (USA).nes" C1D7AB1D, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Choujikuu Yousai - Macross (Japan).nes" C1E91D3F, PAL, 0, 0, 1, 2, 0, false, Vertical, "Spy vs Spy (Europe).nes" C1FBF659, NTSC, 23, 0, 0, 8, 0, false, Horizontal, "Akumajou Special - Boku Dracula-kun (Japan).nes" C226157D, NTSC, 79, 0, 4, 2, 0, false, Vertical, "Venice Beach Volleyball (USA) (Beta) (Unl).nes" C22BC87B, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Seiryaku Simulation - Inbou no Wakusei - Shancara (Japan).nes" C22C23AB, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Pachinko Daisakusen (Japan).nes" C22F3E9F, NTSC, 1, 0, 16, 8, 0, true, Vertical, "Advanced Dungeons & Dragons - Heroes of the Lance (USA) (Beta).nes" C22FF1D8, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "RoboCop 2 (Japan).nes" C247A23D, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Batman Returns (USA).nes" C247CC80, NTSC, 210, 1, 16, 32, 0, true, Vertical, "Family Circuit '91 (Japan).nes" C2730C30, NTSC, 34, 0, 1, 8, 0, false, Horizontal, "Deadly Towers (USA).nes" C2840372, NTSC, 16, 0, 32, 16, 0, true, Vertical, "SD Gundam Gaiden - Knight Gundam Monogatari 3 - Densetsu no Kishi Dan (Japan).nes" C2A4612E, NTSC, 79, 0, 4, 2, 0, false, Vertical, "Blackjack (USA) (Unl).nes" C2EF3422, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Best Play Pro Yakyuu II (Japan).nes" C30848D3, PAL, 0, 0, 1, 2, 0, false, Vertical, "Slalom (Europe).nes" C30C9EC9, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Shoukoushi Ceddie (Japan).nes" C313EF54, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Jack Nicklaus' Greatest 18 Holes of Major Championship Golf (USA).nes" C32E9672, PAL, 4, 0, 16, 8, 0, false, Vertical, "Tiny Toon Adventures 2 - Trouble in Wackyland (Europe).nes" C3463A3D, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Where's Waldo (USA).nes" C372399B, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Genpei Touma Den - Computer Boardgame (Japan).nes" C37F225C, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Spelunker II - Yuusha e no Chousen (Japan).nes" C3A0A3E0, PAL, 0, 0, 1, 2, 0, false, Vertical, "Lunar Pool (Europe).nes" C3C0811D, NTSC, 96, 0, 0, 8, 0, false, Vertical, "Oeka Kids - Anpanman no Hiragana Daisuki (Japan).nes" C3C7A568, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Tiger-Heli (USA).nes" C3CCC493, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Robin Hood - Prince of Thieves (USA) (Rev A).nes" C3DE7C69, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Best Play Pro Yakyuu Special (Japan) (Rev A).nes" C42E648A, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Bigfoot (USA).nes" C46969DF, PAL, 4, 0, 16, 16, 0, false, Horizontal, "Super Mario Bros. 3 (Europe) (Wii VC).nes" C471E42D, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Castlevania II - Simon's Quest (USA).nes" C47EFC0E, NTSC, 79, 0, 4, 2, 0, false, Vertical, "Trolls on Treasure Island (USA) (Unl).nes" C48363B4, NTSC, 4, 0, 32, 16, 0, true, Horizontal, "Dai-2-ji Super Robot Taisen (Japan).nes" C48DDB52, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Dragon's Lair (Japan).nes" C49FCAB4, NTSC, 1, 0, 4, 2, 0, false, Vertical, "Dr. Mario (USA) (Beta).nes" C4A02712, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Spy vs Spy (USA).nes" C4B6ED3C, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Bill & Ted's Excellent Video Game Adventure (USA).nes" C4BC85A2, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Life Force (USA).nes" C4C3949A, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Mario Bros. (World).nes" C4E1886F, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Bard's Tale II, The - The Destiny Knight (Japan).nes" C4E81924, PAL, 4, 0, 32, 8, 0, false, Horizontal, "Beauty and the Beast (Europe).nes" C527C297, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Alien 3 (USA).nes" C528ED56, PAL, 4, 0, 16, 8, 0, false, Vertical, "Super Spy Hunter (Europe).nes" C53CF1D0, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Track & Field II (Europe).nes" C5657C12, PAL, 4, 0, 32, 16, 0, false, Horizontal, "Teenage Mutant Hero Turtles II - The Arcade Game (Europe).nes" C58EEA57, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Mahjong Trap - Si Cuan Ma Que (Asia) (Unl).nes" C5B0B1AB, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Stinger (USA).nes" C5CFE54E, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Toki no Tabibito (Japan).nes" C6000085, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Conan (USA).nes" C6182024, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Romance of the Three Kingdoms (USA).nes" C6224026, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Bikkuriman World - Gekitou Sei Senshi (Japan).nes" C6557E02, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Family Mahjong (Japan).nes" C67865A2, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Star Force (Japan).nes" C68363F6, NTSC, 180, 0, 0, 8, 0, false, Vertical, "Crazy Climber (Japan).nes" C6ADD8C5, NTSC, 4, 0, 4, 2, 0, false, Horizontal, "Valkyrie no Bouken - Toki no Kagi Densetsu (Japan).nes" C6B5D7E0, NTSC, 1, 0, 4, 2, 0, false, Horizontal, "Palamedes (Japan).nes" C6C2EDB5, NTSC, 1, 0, 16, 4, 0, false, Horizontal, "Magic Johnson's Fast Break (USA).nes" C6DD7E69, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Stealth ATF (USA).nes" C7197FB1, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Spy Hunter (USA).nes" C739E88E, NTSC, 86, 0, 0, 8, 0, false, Vertical, "Moero!! Pro Yakyuu (Japan) (Beta).nes" C73B82FC, NTSC, 11, 0, 4, 2, 0, false, Vertical, "Shockwave (USA) (Unl).nes" C740EB46, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Arch Rivals - A Basketbrawl! (USA).nes" C7642467, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Konamic Sports in Seoul (Japan).nes" C769BB34, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Parasol Henbee (Japan).nes" C76AADF4, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Mugen Senshi Valis (Japan).nes" C7BCC981, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Super Mogura Tataki!! - Pokkun Mogurar (Japan).nes" C7F0C457, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Crash 'n' the Boys - Street Challenge (USA).nes" C811DC7A, NTSC, 19, 0, 16, 8, 0, false, Vertical, "Youkai Douchuuki (Japan).nes" C8228B54, PAL, 4, 0, 32, 8, 0, false, Horizontal, "Incredible Crash Dummies, The (Europe).nes" C829007E, NTSC, 66, 0, 8, 4, 0, false, Horizontal, "AV Mahjong Club (Japan) (Unl).nes" C8AD4F32, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Cobra Triangle (USA).nes" C8BD1908, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Ghostbusters (Japan) (Beta).nes" C8EBD977, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Tiny Toon Adventures Cartoon Workshop (Europe).nes" C8EDC97E, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Mitsume ga Tooru (Japan).nes" C8F203F9, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Addams Family, The - Pugsley's Scavenger Hunt (Europe) (Beta).nes" C9187B43, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Kagerou Densetsu (Japan).nes" C92B814B, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Blue Shadow (Europe).nes" C9484BB3, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Pro Yakyuu Satsujin Jiken! (Japan).nes" C9556B36, NTSC, 1, 0, 1, 32, 0, true, Horizontal, "Final Fantasy I, II (Japan).nes" C973699D, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Vegas Dream (USA).nes" C99B690A, PAL, 1, 0, 4, 8, 0, false, Horizontal, "Bubble Bobble (Europe).nes" C9EDF585, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Little Magic (Japan).nes" CA033B3A, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Dragon's Lair (USA).nes" CA094848, NTSC, 32, 0, 0, 16, 0, false, Horizontal, "Perman Part 2 - Himitsu Kessha Madoodan o Taose! (Japan).nes" CA0A869E, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Silent Assault (USA) (Unl).nes" CA24A1A2, NTSC, 67, 0, 0, 8, 0, false, Horizontal, "Mito Koumon - Sekai Manyuu Ki (Japan).nes" CA503F32, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Itadaki Street - Watashi no Omise ni Yottette (Japan).nes" CA594ACE, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Super Mario Bros. 2 (USA) (Rev A).nes" CA5EDBFC, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Home Alone (USA).nes" CA69751B, NTSC, 19, 0, 16, 8, 0, false, Horizontal, "Star Wars (Japan) (Namco).nes" CA6A7BF1, NTSC, 4, 0, 4, 2, 0, false, Horizontal, "Sky Kid (Japan).nes" CA730971, NTSC, 4, 0, 16, 32, 0, true, Horizontal, "Advanced Dungeons & Dragons - Pool of Radiance (Japan).nes" CA96AD0E, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Top Gun - Dual Fighters (Japan).nes" CAB40A6C, PAL, 243, 0, 0, 2, 0, false, Horizontal, "Magic Cube (Asia) (PAL) (Unl).nes" CB04726D, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Alfombra Magica, La (Spain) (Gluk Video) (Unl).nes" CB0A3AF4, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Captain Silver (Japan).nes" CB0A76B1, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Square no Tom Sawyer (Japan).nes" CB17D41E, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Color a Dinosaur (USA) (Beta).nes" CB32E243, NTSC, 4, 0, 1, 32, 0, true, Vertical, "Tenchi o Kurau II - Shokatsu Koumei Den (Japan).nes" CB35FA90, NTSC, 23, 0, 0, 8, 0, false, Horizontal, "Contra (Japan) (Sample).nes" CB5ACB49, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Capcom Barcelona '92 (Japan).nes" CB8F9AB7, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Batman - The Video Game (USA) (Beta 2).nes" CBF4366F, NTSC, 118, 0, 0, 8, 0, false, Vertical, "Alien Syndrome (USA) (Unl).nes" CBFB6DE5, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Yume Penguin Monogatari (Japan).nes" CC3544B0, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Triathron, The (Japan).nes" CC37094C, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Phantom Fighter (USA).nes" CC553FC4, NTSC, 4, 0, 32, 16, 0, false, Vertical, "Star Trek - 25th Anniversary (Germany).nes" CC6CA4DC, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Chip 'n Dale - Rescue Rangers 2 (USA) (Beta).nes" CC7A4DCA, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Abarenbou Tengu (Japan).nes" CCAF543A, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "JuJu Densetsu (Japan).nes" CCC03440, NTSC, 156, 0, 0, 8, 0, false, Horizontal, "Buzz & Waldog (USA) (Proto) (Unl).nes" CCCAF368, NTSC, 232, 0, 0, 16, 0, false, Vertical, "Quattro Sports (USA) (Unl).nes" CCDCBFC6, NTSC, 71, 0, 1, 16, 0, false, Vertical, "Big Nose Freaks Out (USA) (Unl).nes" CCF35C02, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Sangokushi (Japan) (Rev A).nes" CD10DCE2, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Kickle Cubicle (USA).nes" CD50A092, NTSC, 4, 0, 8, 8, 0, false, FourScreen, "Gauntlet (USA) (Unl).nes" CD7A2FD7, NTSC, 1, 0, 1, 8, 0, true, Horizontal, "Hanjuku Hero (Japan).nes" CD883CDC, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Nintendo World Class Service - Joystick Test Cartridge (USA).nes" CDC641FC, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Championship Pool (USA).nes" CE00022D, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Tecmo Bowl (USA).nes" CE06F2D4, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Over Horizon (Japan).nes" CE07194F, NTSC, 66, 0, 4, 8, 0, false, Horizontal, "Kidou Senshi Z Gundam - Hot Scramble (Japan).nes" CE228874, NTSC, 4, 0, 4, 2, 0, false, Horizontal, "Burai Fighter (USA).nes" CE2450C0, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Gegege no Kitarou - Youkai Daimakyou (Japan) (Beta).nes" CE67507A, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "BurgerTime (Japan).nes" CE77B4BE, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Ferrari Grand Prix Challenge (USA).nes" CEA35D5A, NTSC, 80, 0, 0, 8, 0, false, Vertical, "Kyuukyoku Harikiri Stadium (Japan).nes" CEB65B06, NTSC, 7, 0, 1, 16, 0, false, Horizontal, "Battletoads-Double Dragon (USA).nes" CEBD2A31, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Final Fantasy (USA).nes" CEC28502, NTSC, 136, 0, 0, 2, 0, false, Vertical, "Wei Lai Xiao Zi (Asia) (Unl).nes" CEE5857B, NTSC, 1, 0, 1, 32, 0, false, Horizontal, "Ninjara Hoi! (Japan).nes" CF0C9D97, NTSC, 185, 0, 0, 2, 0, false, Vertical, "Spy vs Spy (Japan).nes" CF23290F, NTSC, 19, 0, 32, 16, 0, true, Horizontal, "Juvei Quest (Japan).nes" CF26A149, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Twin Eagle (USA).nes" CF322BB3, NTSC, 3, 0, 4, 2, 0, false, Vertical, "John Elway's Quarterback (USA).nes" CF40B1C5, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Shadowgate (Japan).nes" CF4483AF, NTSC, 3, 0, 2, 2, 0, false, Horizontal, "Banana (Japan) (Beta) (Earlier).nes" CF4487A2, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Super Jeopardy! (USA).nes" CF4DBDBE, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Front Line (Japan).nes" CF5F8AF0, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Golf Grand Slam (USA).nes" CF6D0D7A, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Top Gun (USA) (Rev A).nes" CF701DA4, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Flappy (Japan).nes" CF7CA9BD, PAL, 2, 0, 1, 8, 0, false, Vertical, "Castelian (Europe).nes" CF849F72, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Tecmo World Wrestling (Europe).nes" CF9CF7A2, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Romancia (Japan).nes" CFAE9DFA, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Spot - The Video Game (USA).nes" CFD29C93, NTSC, 4, 0, 32, 16, 0, false, Vertical, "Star Wars - The Empire Strikes Back (USA) (Beta).nes" CFD4A281, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Money Game, The (Japan).nes" CFD5AC62, NTSC, 11, 0, 16, 8, 0, false, Vertical, "Bible Buffet (USA) (v6.0) (Unl).nes" CFE02ADA, PAL, 0, 0, 16, 8, 0, false, Horizontal, "Darkman (Europe).nes" D029F841, PAL, 2, 0, 1, 8, 0, false, Vertical, "DuckTales (Europe).nes" D04A40E6, NTSC, 0, 0, 4, 2, 0, false, Horizontal, "Bingo 75 (Asia) (Unl).nes" D054FFB0, NTSC, 4, 0, 32, 16, 0, true, Vertical, "Zoda's Revenge - StarTropics II (USA).nes" D074653D, NTSC, 3, 0, 2, 2, 0, false, Vertical, "Tetris (Bulletproof) (Japan) (Rev A).nes" D09B74DC, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Great Tank (Japan).nes" D0A9F4E1, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Famicom Yakyuu Ban (Japan).nes" D0CC5EC8, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Mashin Eiyuu Den Wataru Gaiden (Japan).nes" D0DF525E, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Power Blade (Europe).nes" D0DF726E, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Zoids - Chuuou Tairiku no Tatakai (Japan).nes" D0E53454, NTSC, 80, 0, 0, 8, 0, false, Vertical, "Kyuukyoku Harikiri Stadium - '88 Senshu Shin Data Version (Japan).nes" D0E96F6B, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Double Dribble (USA) (Rev A).nes" D0EB749F, NTSC, 18, 0, 16, 8, 0, false, Horizontal, "Lord of King, The (Japan).nes" D0F70E36, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Batman - The Video Game (Europe).nes" D1397940, NTSC, 97, 0, 0, 16, 0, false, Horizontal, "Kaiketsu Yancha Maru (Japan).nes" D152FB02, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Circus Caper (USA).nes" D153CAF6, PAL, 2, 0, 1, 8, 0, false, Vertical, "Swords and Serpents (Europe).nes" D161888B, PAL, 2, 0, 1, 8, 0, false, Vertical, "Kick Off (Europe).nes" D1691028, NTSC, 154, 0, 16, 8, 0, false, Horizontal, "Devil Man (Japan).nes" D175B0CB, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Super Real Baseball '88 (Japan).nes" D17B76DA, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Donald Land (Japan).nes" D188963D, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Challenge of the Dragon (USA) (Unl).nes" D18E6BE3, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "Cowboy Kid (USA).nes" D19ADDEB, NTSC, 119, 0, 0, 8, 0, false, Horizontal, "Pin Bot (USA).nes" D19DCB2B, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Gun Nac (USA).nes" D1E50064, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Game Designer Yousei Soft - Dezaemon (Japan).nes" D1EA84C3, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Caveman Games (USA).nes" D1F7DF3A, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Hokuto no Ken 2 (Japan).nes" D201EDCE, NTSC, 113, 0, 8, 2, 0, false, Horizontal, "AV Hanafuda Club (Japan) (Unl).nes" D2038FC5, NTSC, 32, 0, 0, 8, 0, false, Horizontal, "Image Fight (Japan).nes" D20BB617, PAL, 118, 0, 0, 8, 0, false, Vertical, "Eric Cantona Football Challenge - Goal! 2 (Europe).nes" D229FD5C, PAL, 9, 0, 16, 8, 0, false, Horizontal, "Punch-Out!! (Europe).nes" D2562072, NTSC, 7, 0, 1, 16, 0, false, Horizontal, "Wizards & Warriors III - Kuros...Visions of Power (USA).nes" D2574720, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Bionic Commando (USA).nes" D2674B0A, NTSC, 0, 0, 4, 4, 0, false, Horizontal, "Qi Wang - Chinese Chess (Asia) (Unl).nes" D2699893, NTSC, 88, 0, 16, 8, 0, false, Horizontal, "Dragon Spirit - Aratanaru Densetsu (Japan).nes" D26EFD78, NTSC, 66, 0, 2, 4, 0, false, Vertical, "Super Mario Bros. + Duck Hunt (USA).nes" D273B409, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Power Blade 2 (USA).nes" D27B9D50, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Terminator 2 (Japan).nes" D29DB3C7, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Final Fantasy II (Japan).nes" D2BC86F3, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Ikari II - Dogosoken (Japan).nes" D308D52C, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Raid on Bungeling Bay (USA).nes" D31DC910, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Rockman (Japan) (En).nes" D31EB7BB, NTSC, 1, 0, 16, 8, 0, false, Vertical, "Star Trek V - The Final Frontier (Unknown) (Proto).nes" D323B806, NTSC, 210, 2, 32, 16, 0, false, Vertical, "Wagyan Land 3 (Japan).nes" D343C66A, NTSC, 16, 0, 16, 16, 0, false, Horizontal, "Famicom Jump - Eiyuu Retsuden (Japan).nes" D353D351, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "F-15 Strike Eagle (Germany).nes" D364F816, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Barbie (Europe).nes" D3BFF72E, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Town & Country Surf Designs - Wood & Water Rage (USA).nes" D3EC98AA, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Columbus - Ougon no Yoake (Japan) (Sample).nes" D40FA953, NTSC, 185, 0, 0, 2, 0, false, Horizontal, "Mighty Bomb Jack (Japan) (Rev A).nes" D445F698, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Super Mario Bros. (World).nes" D44B412E, PAL, 1, 0, 1, 8, 0, true, Horizontal, "Legend of Zelda, The (Europe) (Rev A).nes" D4611B79, NTSC, 4, 0, 32, 8, 0, false, Vertical, "WWF Wrestlemania Steel Cage Challenge (USA).nes" D467C0CC, NTSC, 23, 0, 0, 8, 0, false, Horizontal, "Parodius Da! (Japan).nes" D4924CBA, NTSC, 0, 0, 2, 2, 0, false, Horizontal, "Taiwan Mahjong - Tai Wan Ma Que 16 (Asia) (Unl).nes" D49DCA84, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Robin Hood - Prince of Thieves (Germany).nes" D4D9E21A, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Tennis (Japan, USA).nes" D532E98F, NTSC, 5, 0, 16, 16, 0, false, Horizontal, "Shin 4 Nin Uchi Mahjong - Yakuman Tengoku (Japan).nes" D534C98E, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Fire 'n Ice (USA).nes" D568563F, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Wanpaku Kokkun no Gourmet World (Japan).nes" D5941AA9, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Final Mission (Japan).nes" D5C588DF, PAL, 1, 0, 16, 8, 0, false, Vertical, "Snowboard Challenge (Europe).nes" D5C64257, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Kung Fu (Japan, USA).nes" D6190C63, NTSC, 4, 0, 16, 16, 0, true, Vertical, "Tower of Radia (USA) (Proto).nes" D630EE8F, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Aussie Rules Footy (Australia).nes" D63B30F5, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Tom & Jerry - The Ultimate Game of Cat and Mouse! (USA).nes" D679627A, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Spider-Man - Return of the Sinister Six (USA).nes" D67FD6A6, PAL, 1, 0, 1, 8, 0, false, Horizontal, "Kid Icarus (Europe) (Rev A).nes" D68A6F33, NTSC, 1, 0, 1, 8, 0, true, Horizontal, "Dungeon Kid (Japan).nes" D6AD4E9D, PAL, 0, 0, 1, 1, 0, false, Horizontal, "Pinball (Europe) (Rev A).nes" D6BBD8BA, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Tennis (USA) (GameCube Edition).nes" D6EFAB8D, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Solomon's Key 2 (USA) (Beta).nes" D6F7383E, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Castlevania II - Simon's Quest (Europe).nes" D6FE9826, NTSC, 1, 0, 16, 8, 0, false, Vertical, "Viva! Las Vegas (Japan).nes" D7077D96, NTSC, 4, 0, 16, 8, 0, false, Vertical, "U.S. Championship V'Ball (Japan) (Beta).nes" D7215873, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Doraemon - Giga Zombie no Gyakushuu (Japan).nes" D72560E1, PAL, 1, 0, 16, 16, 0, false, Horizontal, "Racket Attack (Europe).nes" D738C059, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Dick Tracy (USA).nes" D73AA04C, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Cool World (USA).nes" D745D7CB, PAL, 0, 0, 1, 2, 0, false, Horizontal, "Xevious (Europe).nes" D74B2719, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Super Team Games (USA).nes" D754F500, NTSC, 68, 0, 0, 8, 0, true, Vertical, "Nantettatte!! Baseball (Japan) (Sample).nes" D7794AFC, NTSC, 4, 0, 32, 32, 0, true, Horizontal, "Kirby's Adventure (USA).nes" D78BFB28, PAL, 2, 0, 1, 8, 0, false, Horizontal, "Top Gun (Europe).nes" D7AA0B6D, NTSC, 88, 0, 8, 8, 0, false, Vertical, "Dragon Buster II - Yami no Fuuin (Japan).nes" D7B35F7D, PAL, 2, 0, 1, 8, 0, false, Horizontal, "Konami Hyper Soccer (Europe).nes" D7CB398F, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Ninja Ryuuken Den (Japan).nes" D7E29C03, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Dragon Spirit - The New Legend (USA).nes" D7F6320C, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Chessmaster, The (USA).nes" D7FABAC1, NTSC, 22, 0, 0, 8, 0, false, Horizontal, "TwinBee 3 - Poko Poko Daimaou (Japan).nes" D80B44BC, NTSC, 66, 0, 4, 8, 0, false, Horizontal, "Thunder & Lightning (USA).nes" D821A1C6, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Solomon no Kagi (Japan).nes" D8230D0E, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Hook (USA).nes" D8578BFD, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Zen - Intergalactic Ninja (USA).nes" D8748E0A, NTSC, 18, 0, 16, 8, 0, false, Horizontal, "Magic John (Japan).nes" D898A900, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Dou Zhi Pin Pan - Wisdom Boy (China) (Unl).nes" D89E5A67, NTSC, 3, 0, 2, 2, 0, false, Horizontal, "Arkanoid (Japan).nes" D8D42F2F, NTSC, 0, 0, 2, 2, 0, false, Horizontal, "Chinese Checkers (Asia) (NTSC) (Unl).nes" D8EE7669, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Adventures of Rad Gravity, The (USA).nes" D8EFF0DF, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Gradius (Japan).nes" D8F651E2, NTSC, 2, 0, 1, 16, 0, false, Vertical, "Seirei Gari (Japan).nes" D9084936, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Gun Nac (Japan).nes" D91104F1, NTSC, 0, 0, 1, 1, 0, false, Vertical, "4 Nin Uchi Mahjong (Japan).nes" D920F9DF, NTSC, 33, 0, 0, 8, 0, false, Horizontal, "Takeshi no Sengoku Fuuunji (Japan).nes" D923EB5B, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Chip to Dale no Daisakusen (Japan).nes" D9323EE6, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Willow (Europe).nes" D97595A3, NTSC, 87, 0, 0, 1, 0, false, Vertical, "Ninja Jajamaru-kun (Japan).nes" D97C31B0, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Lasalle Ishii no Child's Quest (Japan).nes" D996AB4E, NTSC, 66, 0, 4, 2, 0, false, Vertical, "Uforce Power Games (USA) (Proto 2) [b].nes" D99A8804, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Super Pitfall II (USA) (Proto).nes" D9BB572C, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Wizardry - Proving Grounds of the Mad Overlord (USA).nes" D9C093B1, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Space Invaders (Japan).nes" D9F0749F, PAL, 1, 0, 1, 8, 0, false, Horizontal, "Kid Icarus (USA, Europe).nes" D9F1E47C, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Rockman 3 - Dr. Wily no Saigo! (Japan).nes" D9F45BE9, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Gimmi a Break - Shijou Saikyou no Quiz Ou Ketteisen (Japan).nes" DA2CB59A, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Nightmare on Elm Street, A (USA).nes" DA430FB3, NTSC, 113, 0, 8, 2, 0, false, Horizontal, "AV Soccer (Japan) (Unl).nes" DA690D17, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Nihonichi no Mei Kantoku (Japan).nes" DA8E4AF4, NTSC, 4, 0, 32, 8, 0, true, Horizontal, "Tecmo NBA Basketball (USA) (Rev A).nes" DA8F65AE, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Untouchables, The (Japan).nes" DAB84A9C, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Flintstones, The - The Surprise at Dinosaur Peak! (USA).nes" DAD34EE6, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Tao (Japan).nes" DAD88CC5, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Guardic Gaiden (Japan).nes" DAEE19F2, NTSC, 1, 0, 8, 16, 0, false, Horizontal, "Bases Loaded (USA) (Rev A).nes" DAF9D7E3, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "BurgerTime (USA).nes" DB05106E, NTSC, 16, 0, 16, 8, 0, false, Horizontal, "Crayon Shin-chan - Ora to Poi Poi (Japan).nes" DB196068, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Takeshi no Chousenjou (Japan).nes" DB1D03E5, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Caesars Palace (USA).nes" DB2D4F9D, NTSC, 4, 0, 16, 16, 0, true, Vertical, "Tecmo Super Bowl (USA) (Beta).nes" DB479677, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Kiteretsu Daihyakka (Japan).nes" DB564628, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Mario Open Golf (Japan) (Rev A).nes" DB99D0CB, NTSC, 71, 0, 1, 8, 0, false, Vertical, "Dizzy the Adventurer (USA) (Aladdin Compact Cartridge) (Unl).nes" DB9C072D, PAL, 7, 0, 1, 8, 0, false, Vertical, "Arch Rivals - A Basketbrawl! (Europe).nes" DB9DCF89, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Bomberman (USA).nes" DBB06A25, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Dig Dug II - Trouble in Paradise (USA).nes" DBC5ECD9, NTSC, 0, 0, 32, 16, 0, false, Horizontal, "Super Cartridge Ver 4 - 6 in 1 (Asia) (Unl).nes" DBECE74F, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "SD Hero Soukessen - Taose! Aku no Gundan (Japan).nes" DBF90772, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Alpha Mission (USA).nes" DC02F095, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Pictionary - The Game of Video Quick Draw (USA).nes" DC1E07D2, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Hyakkiyagyou (Japan).nes" DC320617, NTSC, 0, 0, 16, 16, 0, false, Horizontal, "Super Cartridge Ver 5 - 7 in 1 (Asia) (Unl).nes" DC45A886, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Downtown Special - Kunio-kun no Jidaigeki Da yo Zenin Shuugou! (Japan).nes" DC4DA5D4, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Side Pocket (USA).nes" DC529482, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Maniac Mansion (Italy).nes" DC52BF0C, NTSC, 16, 0, 32, 16, 0, false, Horizontal, "Dragon Ball Z III - Ressen Jinzou Ningen (Japan).nes" DC75732F, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Cosmic Epsilon (Japan).nes" DCB7C0A1, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Wanpaku Duck Yume Bouken (Japan).nes" DCB972CE, NTSC, 16, 0, 16, 8, 0, true, Horizontal, "Magical Taruruuto-kun - Fantastic World!! (Japan) (Rev 1).nes" DCD8D6F4, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Bakushou!! Ai no Gekijou (Japan).nes" DCDF06DE, NTSC, 4, 0, 4, 4, 0, false, Horizontal, "Pro Yakyuu - Family Stadium (Japan).nes" DD062F9C, NTSC, 7, 0, 1, 4, 0, false, Horizontal, "R.C. Pro-Am (USA).nes" DD29FD59, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Family Mahjong II - Shanghai e no Michi (Japan).nes" DD8ED0F7, NTSC, 70, 0, 0, 8, 0, false, Horizontal, "Kamen Rider Club (Japan).nes" DDA190F9, PAL, 146, 0, 4, 2, 0, false, Vertical, "Twin Eagle (Asia) (PAL) (Unl).nes" DDC6D9C9, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Alien 3 (Europe).nes" DDCBDA16, NTSC, 243, 0, 0, 4, 0, false, Vertical, "Lightgun Game 2 in 1 - Tough Cop + Super Tough Cop (Asia) (Unl).nes" DDD90C39, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Shadow of the Ninja (USA).nes" DDDC56B8, NTSC, 240, 0, 0, 8, 0, true, Horizontal, "Sheng Huo Lie Zhuan (Asia) (Unl).nes" DE0C29A9, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Maniac Mansion (USA) (Beta).nes" DE25B90F, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Rush'n Attack (USA).nes" DE395EFD, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Gradius (Japan) (ArchiMENdes Hen).nes" DE581355, NTSC, 1, 0, 4, 2, 0, false, Horizontal, "Dr. Mario (Japan, USA) (Rev A).nes" DE7E4629, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Simpsons, The - Bart vs. the Space Mutants (Europe).nes" DE84354A, NTSC, 1, 0, 16, 8, 0, false, Vertical, "Superman (Japan) (Beta).nes" DE8FD935, NTSC, 4, 0, 4, 2, 0, false, Horizontal, "To the Earth (USA).nes" DE9C9C64, NTSC, 80, 0, 0, 8, 0, false, Vertical, "Kyonshiizu 2 (Japan).nes" DF31B364, NTSC, 112, 0, 0, 4, 0, false, Horizontal, "Cobra Mission (Asia) (Unl).nes" DF3776C6, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Sword Master (Japan).nes" DF3E45D2, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Mad City (Japan).nes" DF43E073, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "RoboCop versus The Terminator (USA) (Proto).nes" DF4EDC13, PAL, 1, 0, 4, 2, 0, false, Horizontal, "Adventures of Lolo (Europe).nes" DF64963B, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Infiltrator (USA).nes" DF67DAA1, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Stack-Up (World).nes" DF6D0CE8, PAL, 1, 0, 16, 8, 0, false, Vertical, "Iron Tank - The Invasion of Normandy (Europe).nes" DFA111F1, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Bram Stoker's Dracula (USA).nes" DFAD3F66, NTSC, 245, 0, 0, 16, 0, true, Vertical, "Ying Xiong Yuan Yi Jing Chuan Qi (China) (Unl).nes" DFC0CE21, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Super Black Onyx (Japan).nes" DFD70E27, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Xevious - The Avenger (USA).nes" DFEFE8CD, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Clu Clu Land (USA) (GameCube Edition).nes" E02133AC, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Magic Darts (Japan).nes" E043C6A5, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Lethal Weapon (Europe).nes" E0604F76, NTSC, 0, 0, 1, 1, 0, false, Vertical, "F-1 Race (Japan).nes" E08C8A60, NTSC, 4, 0, 1, 32, 0, true, Horizontal, "Pachio-kun 4 (Japan).nes" E095C3F2, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Bubble Bobble Part 2 (USA).nes" E0AC6242, PAL, 2, 0, 1, 8, 0, false, Vertical, "Rush'n Attack (Europe).nes" E0B6B7BB, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Zoids - Chuuou Tairiku no Tatakai (Japan) (Rev A).nes" E0CBC2BA, NTSC, 1, 0, 16, 16, 0, true, Horizontal, "Chaos World (Japan).nes" E0FFFBD2, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Little Nemo - The Dream Master (Europe).nes" E116447F, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "BinGuo 75 (Asia) (Unl).nes" E1383DEB, NTSC, 26, 0, 32, 16, 0, false, Horizontal, "Mouryou Senki Madara (Japan).nes" E145B441, NTSC, 1, 0, 16, 16, 0, false, Horizontal, "Day Dreamin' Davey (USA).nes" E149E0B2, NTSC, 1, 0, 8, 2, 0, true, Horizontal, "Nintendo - NTF2 System Cartridge (USA).nes" E14F0A3F, NTSC, 1, 0, 1, 8, 0, false, Vertical, "Super Pinball (Japan) (Beta).nes" E1526228, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Ki no Bouken - The Quest of Ki (Japan).nes" E15C973D, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Exciting Rally - World Rally Championship (Japan).nes" E170404C, NTSC, 16, 0, 16, 16, 0, true, Horizontal, "SD Gundam Gaiden - Knight Gundam Monogatari (Japan).nes" E19293A2, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Best Play Pro Yakyuu - Shin Data (Japan).nes" E19EE99C, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Bucky O'Hare (USA).nes" E1B260DA, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Argos no Senshi (Japan).nes" E1C03EB6, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Egypt (Japan).nes" E1C41D7C, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Rally Bike (USA).nes" E1C59D94, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Top Gun - The Second Mission (Europe).nes" E211B93A, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Donkey Kong Jr. (Japan).nes" E2265BF4, NTSC, 0, 0, 8, 4, 0, false, Horizontal, "Rockball (Asia) (Unl).nes" E2281986, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Marusa no Onna (Japan).nes" E2313813, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Code Name - Viper (USA).nes" E24483B1, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Obocchama-kun (Japan).nes" E24DF353, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Totsuzen! Macchoman (Japan).nes" E292AA10, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Zanac (USA).nes" E2A79A57, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Rollerball (Japan).nes" E2B43A68, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Amagon (USA).nes" E2C4EDCE, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Journey to Silius (USA).nes" E3027EBE, PAL, 1, 0, 4, 8, 0, false, Horizontal, "Chessmaster, The (Europe).nes" E305202E, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Time Zone (Japan).nes" E30B2BCF, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Pachicom (Japan).nes" E326E0F5, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "ASO - Armored Scrum Object (Japan) (En) (Beta).nes" E333FFA1, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Igo Shinan '91 (Japan).nes" E349AF38, NTSC, 24, 0, 16, 16, 0, false, Horizontal, "Akumajou Densetsu (Japan).nes" E353969F, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Last Ninja, The (USA).nes" E37A39AB, PAL, 4, 0, 8, 8, 0, false, Vertical, "Yoshi's Cookie (Europe).nes" E387C77F, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "Ultimate Air Combat (USA).nes" E3C5BB3D, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Mission Impossible (USA).nes" E3E2C3BF, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "ASO - Armored Scrum Object (Japan).nes" E402B134, PAL, 3, 0, 4, 2, 0, false, Vertical, "Dropzone (Europe).nes" E40B4973, NTSC, 4, 0, 4, 2, 0, false, Horizontal, "Metro-Cross (Japan).nes" E429F0D3, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Cycle Race - Road Man (Japan).nes" E4362167, NTSC, 85, 0, 0, 8, 0, false, Horizontal, "Tiny Toon Adventures 2 - Montana Land e Youkoso (Japan).nes" E44001D8, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Casino Derby (Japan).nes" E46AEE21, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Thomas the Tank Engine and Friends (USA) (Proto).nes" E46B1C5D, NTSC, 140, 0, 0, 8, 0, false, Vertical, "Mississippi Satsujin Jiken (Japan).nes" E4776A2B, PAL, 2, 0, 1, 8, 0, false, Vertical, "Blues Brothers, The (Europe).nes" E47E9FA7, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Sky Destroyer (Japan).nes" E492D45A, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Zippy Race (Japan).nes" E4A6E151, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Family Trainer 10 - Rairai Kyonsees (Japan) (Beta).nes" E4A7D436, NTSC, 4, 0, 32, 32, 0, true, Horizontal, "Hoshi no Kirby - Yume no Izumi no Monogatari (Japan).nes" E4E7C62D, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Time Diver Eon Man (USA) (Proto).nes" E50A9130, NTSC, 1, 0, 4, 4, 0, false, Horizontal, "Bugs Bunny Crazy Castle, The (USA).nes" E53F7A55, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Cosmo Police Galivan (Japan).nes" E54138A9, PAL, 0, 0, 1, 1, 0, false, Horizontal, "Balloon Fight (Europe).nes" E542E3CF, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Monster in My Pocket (USA).nes" E56AA5E8, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Sou Setsu Ryuu II - The Revenge (Japan) (Beta).nes" E575687C, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Star Trek - The Next Generation (USA).nes" E57E5384, PAL, 0, 0, 1, 2, 0, false, Vertical, "Mach Rider (Europe).nes" E5901A99, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Kickle Cubicle (Europe).nes" E592F53A, PAL, 3, 0, 4, 2, 0, false, Vertical, "Athletic World (Europe).nes" E5A8401B, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Stealth ATF (Europe).nes" E5A972BE, PAL, 7, 0, 1, 4, 0, false, Horizontal, "R.C. Pro-Am (Europe).nes" E5EA0EBE, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Tecmo Bowl (USA) (Beta).nes" E5FCC4C1, PAL, 1, 0, 4, 2, 0, false, Horizontal, "Boulder Dash (Europe).nes" E616FF0A, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Advanced Dungeons & Dragons - Dragons of Flame (Japan).nes" E62E3382, NTSC, 71, 0, 1, 8, 0, false, Vertical, "MiG 29 - Soviet Fighter (USA) (Unl).nes" E63D9193, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Tanigawa Kouji no Shougi Shinan III (Japan).nes" E64B8975, NTSC, 19, 0, 16, 8, 0, true, Vertical, "Sangokushi - Chuugen no Hasha (Japan).nes" E661918C, NTSC, 69, 0, 16, 16, 0, false, Vertical, "Gimmick! (Japan) (Beta).nes" E66AD6B8, PAL, 0, 0, 1, 2, 0, false, Vertical, "25th Anniversary Super Mario Bros. (Europe) (Promo, Virtual Console).nes" E66BDDCF, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Donald Duck (Japan).nes" E681B300, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Trolls in Crazyland, The (Europe).nes" E6857563, NTSC, 4, 0, 32, 8, 0, false, Vertical, "Mike Tyson's Intergalactic Power Punch (USA) (Beta) [b].nes" E6A477B2, NTSC, 2, 0, 1, 8, 0, false, Vertical, "3-D WorldRunner (USA).nes" E6B30BB3, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Kamen no Ninja - Hanamaru (Japan).nes" E6C9029E, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Motocross Champion (Japan).nes" E6DF6616, NTSC, 87, 0, 0, 2, 0, false, Vertical, "Goonies (Japan).nes" E6F08E93, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Pro Wrestling (USA).nes" E71D034E, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Snake's Revenge (Europe).nes" E71DB268, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Faxanadu (USA) (Rev A).nes" E73E7260, NTSC, 4, 0, 4, 4, 0, false, Vertical, "Pac-Mania (USA) (Unl).nes" E74A91BB, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Silk Worm (USA).nes" E74AA15A, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Kaettekita! Gunjin Shougi - Nanya Sore! (Japan).nes" E78A394C, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "SD Battle Oozumou - Heisei Hero Basho (Japan).nes" E7C981A2, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Widget (USA).nes" E7D2C49D, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Golf (USA).nes" E7DA8A04, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Last Action Hero (USA).nes" E7DDFEE3, NTSC, 4, 0, 16, 16, 0, false, Horizontal, "Super Mario Bros. 3 (Japan).nes" E8000BF7, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Pinball (USA) (GameCube Edition).nes" E840FD21, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Super Spike V'Ball (USA).nes" E85B4D3D, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Hit Marmot (Asia) (Unl).nes" E8A11BD7, NTSC, 3, 0, 1, 2, 0, false, Horizontal, "Porter (Asia) (Unl).nes" E8AF6FF5, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Nagagutsu o Haita Neko - Sekai Isshuu 80 Nichi Daibouken (Japan).nes" E8BAA782, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Gunhed - Aratanaru Tatakai (Japan).nes" E9023072, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Formation Z (Japan) (Rev A).nes" E911BCC4, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Galaga (Japan).nes" E9176129, NTSC, 4, 0, 4, 2, 0, false, Horizontal, "Burai Fighter (Japan).nes" E943EC4D, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Frankenstein - The Monster Returns (USA).nes" E949EF8A, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Astro Fang - Super Machine (Japan).nes" E94D5181, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Future Wars - Mirai Senshi Lios (Japan).nes" E94E883D, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Super Mario Bros. 2 (Europe).nes" E95454FC, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Cross Fire (Japan).nes" E95E51E0, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Hyaku no Sekai no Monogatari - The Tales on a Watery Wilderness (Japan).nes" E98AB943, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Ren & Stimpy Show, The - Buckeroo$! (USA).nes" E9A6C211, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Ski or Die (USA).nes" E9AD2163, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Cosmos Cop (Spain) (Gluk Video) (Unl).nes" E9C387EC, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "River City Ransom (USA).nes" E9D352EB, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Double Dragon III - The Sacred Stones (Europe).nes" E9EDBA24, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Karateka (Japan) (Beta).nes" E9F16673, NTSC, 3, 0, 8, 2, 0, false, Horizontal, "Peek-A-Boo Poker (Asia) (Unl).nes" E9F8EF15, PAL, 4, 0, 16, 16, 0, false, Horizontal, "Simpsons, The - Bart vs. the World (Europe).nes" EA113128, NTSC, 11, 0, 8, 4, 0, false, Vertical, "Operation Secret Storm (USA) (Unl).nes" EA19080A, NTSC, 79, 0, 4, 2, 0, false, Horizontal, "Puzzle (USA) (Unl).nes" EA27B477, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Terminator 2 - Judgment Day (USA).nes" EA31CCD3, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Layla (Japan).nes" EA3E78DD, NTSC, 18, 0, 32, 8, 0, false, Horizontal, "Toukon Club (Japan).nes" EA4EB69E, NTSC, 1, 0, 4, 8, 0, false, Horizontal, "Touch Down Fever (USA).nes" EA89963F, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Tatakae! Chou Robot Seimeitai Transformers - Convoy no Nazo (Japan).nes" EA90F3E2, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Family Trainer 2 - Running Stadium (Japan).nes" EAB002AE, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Dino-Hockey (USA) (Proto).nes" EAB93CFB, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Adventures of Lolo 2 (Japan).nes" EAC38105, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Championship Bowling (USA).nes" EAF7ED72, NTSC, 1, 0, 1, 8, 0, true, Horizontal, "Legend of Zelda, The (USA) (Rev A).nes" EB0BDA7E, NTSC, 64, 0, 0, 8, 0, false, Horizontal, "Shinobi (USA) (Unl).nes" EB15169E, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Heavy Shreddin' (USA).nes" EB465156, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Kero Kero Keroppi no Daibouken (Japan).nes" EB4CCA31, PAL, 0, 0, 4, 2, 0, false, Vertical, "Master Chu and the Drunkard Hu (Asia) (PAL) (Unl).nes" EB61133B, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Puzznic (USA).nes" EB764567, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Druaga no Tou (Japan).nes" EB803610, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Wurm - Journey to the Center of the Earth! (USA).nes" EB84C54C, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Beetlejuice (USA).nes" EB92B32A, NTSC, 25, 0, 0, 16, 0, true, Horizontal, "Ganbare Goemon Gaiden - Kieta Ougon Kiseru (Japan).nes" EB9960EE, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Destination Earthstar (USA).nes" EBB5E666, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Little Mermaid - Ningyo Hime (Japan) (Beta).nes" EBCF8419, NTSC, 4, 0, 16, 16, 0, true, Horizontal, "Samsara Naga (Japan).nes" EBCFE7C5, NTSC, 1, 0, 16, 4, 0, false, Horizontal, "Knight Rider (USA).nes" EBD0644D, NTSC, 0, 0, 4, 2, 0, false, Vertical, "Dao Shuai (Asia) (Unl).nes" EC0517C4, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Zunou Senkan Galg (Japan).nes" EC0FC2DE, NTSC, 1, 0, 4, 2, 0, false, Horizontal, "Sesame Street ABC (USA).nes" EC40E71B, NTSC, 140, 0, 0, 8, 0, false, Vertical, "Bio Senshi Dan - Increaser Tono Tatakai (Japan).nes" EC8A884F, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Moai-kun (Japan).nes" EC968C51, NTSC, 4, 0, 8, 8, 0, false, FourScreen, "Gauntlet (USA).nes" ECBF33CE, NTSC, 4, 0, 16, 16, 0, true, Horizontal, "F1 Circus (Japan).nes" ECCD4089, NTSC, 1, 0, 16, 16, 0, false, Horizontal, "Racket Attack (USA).nes" ED2465BE, NTSC, 5, 0, 16, 16, 0, false, Horizontal, "Castlevania III - Dracula's Curse (USA).nes" ED3FA60E, PAL, 4, 0, 32, 8, 0, false, Horizontal, "Spider-Man - Return of the Sinister Six (Europe).nes" ED4D696F, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Magic Block (Asia) (Mega Soft) (Unl).nes" ED77B453, PAL, 2, 0, 1, 8, 0, false, Vertical, "Asterix (Europe) (En,Fr,De,Es,It).nes" ED7F5555, PAL, 1, 0, 1, 8, 0, true, Horizontal, "Legend of Zelda, The (Europe).nes" EDC3662B, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Operation Wolf (USA) (Rev 0A).nes" EDCF1B71, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Solstice - The Quest for the Staff of Demnos (USA).nes" EDDCC468, NTSC, 2, 0, 1, 8, 0, false, Vertical, "DuckTales 2 (Japan).nes" EE219A49, PAL, 3, 0, 4, 2, 0, false, Horizontal, "Paperboy (Europe).nes" EE6892EB, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Trog! (USA).nes" EE7E61DE, NTSC, 0, 0, 1, 2, 0, false, Vertical, "U-Force Test (USA).nes" EE810D55, NTSC, 74, 0, 0, 32, 0, true, Vertical, "You Ling Xing Dong (China) (Unl).nes" EE8E6553, NTSC, 5, 0, 32, 16, 0, true, Horizontal, "Sangokushi II (Japan) (Rev AB).nes" EE921D8E, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Teenage Mutant Ninja Turtles (USA).nes" EEE111C2, NTSC, 3, 0, 8, 2, 0, false, Vertical, "Soap Panic (Japan) (Unl).nes" EEE6314E, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Solomon no Kagi 2 - Coolmintou Kyuushutsu Sakusen (Japan).nes" EEE9A682, NTSC, 5, 0, 16, 16, 0, true, Horizontal, "Nobunaga no Yabou - Sengoku Gunyuu Den (Japan).nes" EF7996BF, NTSC, 19, 0, 16, 8, 0, false, Vertical, "Erika to Satoru no Yume Bouken (Japan).nes" EFB09075, NTSC, 2, 0, 1, 8, 0, false, Vertical, "DuckTales (USA).nes" EFB2B7E8, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Penguin & Seal, The (Asia, Australia) (Unl).nes" EFCF375D, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Super Glove Ball (USA).nes" EFD26E37, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Captain Skyhawk (USA) (Rev A).nes" F00584B6, NTSC, 4, 0, 16, 8, 0, true, Horizontal, "Cyber Stadium Series - Base Wars (USA).nes" F009DDD2, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Toxic Crusaders (USA).nes" F011E490, NTSC, 5, 0, 32, 16, 0, true, Horizontal, "Romance of the Three Kingdoms II (USA).nes" F03E6D72, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Squashed (USA) (Proto).nes" F053AC5F, NTSC, 4, 0, 32, 8, 0, true, Horizontal, "Deja Vu (Japan).nes" F05870D5, NTSC, 79, 0, 8, 4, 0, false, Vertical, "Mermaids of Atlantis - The Riddle of the Magic Bubble (USA) (Unl).nes" F08E8EF0, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Parallel World (Japan).nes" F0C198FF, PAL, 1, 0, 16, 8, 0, false, Horizontal, "New Ghostbusters II (Europe).nes" F0E9971B, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Gargoyle's Quest II (USA).nes" F161A5D8, NTSC, 4, 0, 1, 32, 0, false, Horizontal, "Rockman 4 - Aratanaru Yabou!! (Japan).nes" F17486DF, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Super Chinese 3 (Japan).nes" F181C021, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Legacy of the Wizard (USA).nes" F184EB2D, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Solomon's Key 2 (Europe).nes" F19A11AF, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Sted - Iseki Wakusei no Yabou (Japan).nes" F1C76AED, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Game Party (Japan).nes" F1E6B576, NTSC, 86, 0, 0, 8, 0, false, Vertical, "Moero!! Pro Yakyuu (Japan) (Rev 3).nes" F1FED9B8, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Flying Dragon - The Secret Scroll (USA).nes" F2096D9C, NTSC, 2, 0, 1, 8, 0, false, Vertical, "California Raisins - The Grape Escape (USA) (Proto 1).nes" F24D4F03, NTSC, 177, 0, 0, 64, 0, true, Horizontal, "Shang Gu Shen Jian (China) (Unl).nes" F2594374, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Matendouji (Japan).nes" F283CF58, PAL, 3, 0, 4, 2, 0, false, Horizontal, "Colorful Dragon (Asia) (PAL) (Unl).nes" F2CE3641, NTSC, 68, 0, 0, 8, 0, false, Horizontal, "After Burner (Japan).nes" F2FC8212, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Bomber Man (Japan).nes" F304F1B9, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Archon (USA).nes" F31D36A3, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Home Alone (USA) (Rev A).nes" F31DCC15, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Summer Carnival '92 - Recca (Japan).nes" F32748A1, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Chiyonofuji no Ooichou (Japan).nes" F3623561, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Zoids Mokushiroku (Japan).nes" F37BEFD5, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Blue Marlin, The (USA).nes" F3808245, NTSC, 2, 0, 1, 8, 0, false, Vertical, "WWF Wrestlemania Challenge (Japan).nes" F3841DCD, NTSC, 79, 0, 4, 2, 0, false, Horizontal, "F15 City War (USA) (v1.1) (Unl).nes" F3F1269D, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Captain Tsubasa (Japan).nes" F41ADD60, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Sanma no Mei Tantei (Japan).nes" F42B0DBD, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Karate Champ (USA) (Rev A).nes" F450DB3A, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Elnark no Zaihou (Japan).nes" F4615036, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Hoops (USA).nes" F46EF39A, PAL, 37, 0, 0, 16, 0, false, Horizontal, "Super Mario Bros. + Tetris + Nintendo World Cup (Europe) (Rev A).nes" F471827D, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Addams Family, The - Uncle Fester's Quest (USA) (Beta).nes" F4B70BFE, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Maniac Mansion (France).nes" F4DD5BA5, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Puzslot (Japan).nes" F4DFDB14, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "KlashBall (USA).nes" F4E5DF0E, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Kawa no Nushi Tsuri (Japan).nes" F518DD58, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Captain Skyhawk (USA).nes" F51A7F46, NTSC, 16, 0, 1, 16, 0, false, Horizontal, "Datach - Yu Yu Hakusho - Bakutou Ankoku Bujutsukai (Japan).nes" F532F09A, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Golgo 13 - Top Secret Episode (USA).nes" F540677B, NTSC, 5, 0, 32, 32, 0, true, Horizontal, "Nobunaga no Yabou - Bushou Fuuun Roku (Japan).nes" F54B34BD, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Kings of the Beach - Professional Beach Volleyball (USA).nes" F56135C0, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Nishimura Kyoutarou Mystery - Super Express Satsujin Jiken (Japan).nes" F568A7A4, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Family School (Japan).nes" F59CFC3D, PAL, 1, 0, 1, 16, 0, true, Horizontal, "Maniac Mansion (Europe).nes" F5A1B8FB, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Street Gangs (Europe).nes" F5B2AFCA, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Maniac Mansion (Spain).nes" F5CEEF8F, NTSC, 206, 0, 8, 8, 0, false, Horizontal, "Family Mahjong (Japan) (Rev A).nes" F5F435B1, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Binary Land (Japan).nes" F6035030, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Contra (USA).nes" F6139EE9, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Labyrinth (Japan).nes" F613A8F9, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "World Games (USA).nes" F6271A51, NTSC, 25, 0, 0, 8, 0, false, Horizontal, "Racer Mini Yonku - Japan Cup (Japan).nes" F62B0327, NTSC, 2, 0, 1, 16, 0, false, Vertical, "Big Nose and the Witchdoctor (USA) (Beta) (Unl).nes" F635C594, NTSC, 184, 0, 0, 2, 0, false, Vertical, "Kanshakudama Nage Kantarou no Toukaidou Gojuusan Tsugi (Japan).nes" F64CB545, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Chuugoku Senseijutsu (Japan).nes" F651398D, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Wrath of the Black Manta (USA).nes" F66EC512, NTSC, 4, 0, 8, 8, 0, false, Horizontal, "Yoshi no Cookie (Japan).nes" F6751D3D, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Flying Hero (Japan).nes" F6898A59, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "James Bond Jr (USA).nes" F699EE7E, NTSC, 68, 0, 0, 8, 0, false, Horizontal, "After Burner (USA) (Unl).nes" F6A9CB75, NTSC, 168, 0, 0, 4, 0, true, Horizontal, "Racermate Challenge II (USA) (v9.03.128) (Unl).nes" F6AB12A2, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Rescue - The Embassy Mission (USA) (Beta).nes" F6B9799C, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "NES Open Tournament Golf (USA).nes" F714FAE3, NTSC, 1, 0, 1, 8, 0, true, Horizontal, "Mahjong Taikai (Japan).nes" F71E7EDD, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Final Fantasy (Japan) (Rev B).nes" F732C8FD, NTSC, 71, 0, 1, 16, 0, false, Vertical, "Fantastic Adventures of Dizzy, The (USA) (Aladdin Compact Cartridge) (Unl).nes" F74DFC91, NTSC, 1, 0, 1, 8, 0, false, Horizontal, "Win, Lose or Draw (USA).nes" F7606810, NTSC, 0, 0, 1, 2, 0, true, Vertical, "Family BASIC (Japan) (v2.0a).nes" F760F1CB, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Dough Boy (Japan).nes" F7762A20, NTSC, 4, 0, 4, 8, 0, false, Horizontal, "Side Pocket (Japan).nes" F7893859, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Higemaru - Makai-jima - Nanatsu no Shima Daibouken (Japan).nes" F79A75D7, NTSC, 4, 0, 32, 16, 0, true, Horizontal, "Wario's Woods (USA).nes" F7A9822E, NTSC, 0, 0, 16, 16, 0, false, Horizontal, "Super Cartridge Ver 6 - 6 in 1 (Asia) (Unl).nes" F7B852E4, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Sanrio Cup - Pon Pon Volley (Japan).nes" F7D20181, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Ikari III (Japan).nes" F7E07B83, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Moulin Rouge Senki - Melville no Honoo (Japan).nes" F808AF60, NTSC, 184, 0, 0, 2, 0, false, Vertical, "Atlantis no Nazo (Japan).nes" F80BDC50, NTSC, 33, 0, 0, 8, 0, false, Horizontal, "Insector X (Japan).nes" F83E0D2D, PAL, 1, 0, 16, 8, 0, false, Vertical, "Chip 'n Dale - Rescue Rangers 2 (Europe).nes" F85E264D, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "Tetrastar - The Fighter (Japan).nes" F863D5BB, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Donkey Kong (Japan).nes" F885D931, NTSC, 1, 0, 16, 8, 0, true, Horizontal, "Faria - Fuuin no Tsurugi (Japan).nes" F89300FB, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Mottomo Abunai Deka (Japan).nes" F8A713BE, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Hudson's Adventure Island (USA).nes" F8C1A690, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Famicom Doubutsu Seitai Zukan! - Katte ni Shirokuma - Mori o Sukue no Maki! (Japan).nes" F8C358D7, PAL, 0, 0, 2, 2, 0, false, Vertical, "Millionaire (Asia) (PAL) (Unl).nes" F8D53171, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "8 Eyes (Japan).nes" F919795D, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Dragon's Lair (Europe).nes" F927FA43, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Blue Marlin, The (Japan).nes" F92BE3EC, NTSC, 64, 0, 0, 8, 0, false, Horizontal, "Rolling Thunder (USA) (Unl).nes" F92BE7F2, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Flight of the Intruder (USA).nes" F96D07C8, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Moeru! Oniisan (Japan).nes" F989296C, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Batsu & Terry - Makyou no Tetsujin Race (Japan).nes" F99E37EB, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Chris Evert & Ivan Lendl in Top Players' Tennis (USA).nes" F9B4240F, NTSC, 5, 0, 16, 16, 0, true, Horizontal, "Nobunaga no Yabou - Sengoku Gunyuu Den (Japan) (Rev A).nes" F9FC0700, PAL, 2, 0, 1, 8, 0, false, Horizontal, "Hero Quest (Europe) (Proto).nes" FA014BA1, PAL, 2, 0, 1, 8, 0, false, Vertical, "Silent Service (Europe).nes" FA2A8A8B, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Nichibutsu Mahjong III - Mahjong G Men (Japan).nes" FA43146B, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Guardian Legend, The (USA).nes" FA434E09, NTSC, 1, 0, 1, 8, 0, true, Vertical, "Bard's Tale, The - Tales of the Unknown (USA) (Beta 2).nes" FA6D4281, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Seikima II - Akuma no Gyakushuu! (Japan).nes" FA704C86, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Rainbow Islands - The Story of Bubble Bobble 2 (Japan).nes" FA73D3A2, PAL, 4, 0, 32, 8, 0, false, Horizontal, "Days of Thunder (Europe).nes" FA74F656, NTSC, 4, 0, 32, 16, 0, false, Vertical, "F-15 Strike Eagle (Italy).nes" FA7E02FA, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Hayauchi Super Igo (Japan).nes" FA7EE642, PAL, 1, 0, 1, 16, 0, false, Horizontal, "Bionic Commando (Europe).nes" FB1C0551, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Monster Maker - 7 Tsu no Hihou (Japan).nes" FB2B6B10, NTSC, 178, 0, 0, 64, 0, true, Horizontal, "Fan Kong Jing Ying (China) (Unl).nes" FB2F949F, NTSC, 112, 0, 0, 8, 0, false, Vertical, "San Guo Zhi - Qun Xiong Zheng Ba (Asia) (Unl).nes" FB3439FC, NTSC, 0, 0, 32, 8, 0, false, Horizontal, "Super Cartridge Ver 1 - 4 in 1 (Asia) (Unl).nes" FB69743A, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Aoki Ookami to Shiroki Mejika - Genghis Khan (Japan).nes" FB77099E, NTSC, 1, 0, 4, 8, 0, false, Vertical, "Garfield - A Week of Garfield (Japan) (Sample).nes" FB8A9B80, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Airwolf (Japan).nes" FB98D46E, PAL, 0, 0, 1, 1, 0, false, Horizontal, "Ice Climber (USA, Europe).nes" FBD48274, PAL, 4, 0, 16, 8, 0, false, Vertical, "Felix the Cat (Europe).nes" FBDD0F1B, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Totsuzen! Macchoman (Japan) (Beta).nes" FBF8A785, NTSC, 7, 0, 1, 8, 0, false, Horizontal, "Wheel of Fortune (USA).nes" FC00A282, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Ninja-kun - Majou no Bouken (Japan) (Rev 1).nes" FC2DA286, PAL, 3, 0, 4, 2, 0, false, Horizontal, "Puzznic (Europe).nes" FC2F9B2D, PAL, 4, 0, 16, 8, 0, false, Horizontal, "McDonaldland (Europe).nes" FC3236D1, PAL, 2, 0, 1, 8, 0, false, Vertical, "Total Recall (Europe).nes" FC3E5C86, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Trojan (USA).nes" FC5026EE, PAL, 3, 0, 4, 2, 0, false, Horizontal, "Battleship (Europe) (En,Fr,De,Es).nes" FC5783A7, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Chip 'n Dale - Rescue Rangers 2 (USA).nes" FC778215, NTSC, 0, 0, 8, 4, 0, false, Horizontal, "Mahjong World, The - Ma Que Shi Jie (Asia) (NTSC) (Unl).nes" FC8DEBEF, NTSC, 152, 0, 0, 4, 0, false, Vertical, "Arkanoid II (Japan) (Beta).nes" FCB13110, NTSC, 1, 0, 16, 8, 0, false, Horizontal, "Golf Club - Birdy Rush (Japan).nes" FCB5CB1E, NTSC, 2, 0, 1, 8, 0, false, Horizontal, "Puyo Puyo (Japan).nes" FCBF28B1, NTSC, 23, 0, 0, 8, 0, false, Horizontal, "Crisis Force (Japan).nes" FCD772EB, PAL, 4, 0, 16, 8, 0, false, Horizontal, "Star Wars (Europe).nes" FCDACA80, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Elevator Action (Japan).nes" FCE408A4, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Star Force (USA).nes" FCE71311, NTSC, 3, 0, 4, 2, 0, false, Horizontal, "Stadium Events (USA).nes" FCEBCC5F, PAL, 0, 0, 1, 1, 0, false, Vertical, "M82 Game Selectable Working Product Display (Europe).nes" FD21F54D, NTSC, 1, 0, 1, 16, 0, false, Horizontal, "Robin Hood - Prince of Thieves (Spain).nes" FD45E9C1, NTSC, 1, 0, 1, 8, 0, true, Horizontal, "Tetris 2 + Bombliss (Japan) (Rev A).nes" FD55DD33, NTSC, 112, 0, 0, 16, 0, false, Horizontal, "Fighting Hero III (Asia) (Unl).nes" FD63E7AC, NTSC, 4, 0, 8, 8, 0, false, Vertical, "R.B.I. Baseball 3 (USA) (Unl).nes" FD7E9A7E, PAL, 1, 0, 16, 8, 0, false, Horizontal, "Legend of Prince Valiant, The (Europe).nes" FD8D6C75, NTSC, 2, 0, 1, 8, 0, false, Vertical, "Loopz (USA).nes" FDB8AA9A, NTSC, 4, 0, 32, 8, 0, false, Horizontal, "Juuryoku Soukou Metal Storm (Japan).nes" FDDF2135, NTSC, 4, 0, 32, 16, 0, false, Horizontal, "Rockman 5 - Blues no Wana! (Japan).nes" FDE14CCE, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Volguard II (Japan).nes" FDE1C7ED, NTSC, 1, 0, 16, 16, 0, false, Horizontal, "Sesame Street - Big Bird's Hide & Speak (USA).nes" FDF4569B, NTSC, 1, 0, 4, 2, 0, false, Horizontal, "Snake Rattle n Roll (USA).nes" FDFF80D5, PAL, 1, 0, 2, 2, 0, false, Horizontal, "Tetris (Europe).nes" FE08D602, PAL, 1, 0, 16, 8, 0, false, Horizontal, "TaleSpin (Europe).nes" FE18E6B6, NTSC, 0, 0, 1, 2, 0, false, Vertical, "Bokosuka Wars (Japan).nes" FE3488D1, NTSC, 5, 0, 16, 32, 0, true, Horizontal, "Daikoukai Jidai (Japan).nes" FE364BE5, NTSC, 1, 0, 1, 16, 0, true, Horizontal, "Deep Dungeon IV - Kuro no Youjutsushi (Japan).nes" FE387FE5, NTSC, 32, 0, 0, 8, 0, false, Horizontal, "Perman (Japan).nes" FE4ED42B, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Max Warrior - Wakusei Kaigenrei (Japan).nes" FE84FCAC, NTSC, 4, 0, 16, 8, 0, false, Vertical, "Monster in My Pocket (USA) (Beta).nes" FE907015, PAL, 2, 0, 1, 8, 0, false, Vertical, "Guardian Legend, The (Europe).nes" FE99BBED, NTSC, 4, 0, 16, 8, 0, false, Horizontal, "Soreike! Anpanman - Minna de Hiking Game! (Japan).nes" FE9FE4DA, NTSC, 3, 0, 4, 2, 0, false, Vertical, "Nagagutsu o Haita Neko - Sekai Isshuu 80 Nichi Daibouken (Japan) (Beta).nes" FF1CEFAA, NTSC, 0, 0, 1, 2, 0, false, Horizontal, "Duck Maze (Australia) (Unl).nes" FF24D794, NTSC, 0, 0, 1, 1, 0, false, Vertical, "Hogan's Alley (World).nes" FF53D73E, NTSC, 2, 0, 1, 8, 0, false, Vertical, "DuckTales (USA) (Beta).nes" FFD9DB04, NTSC, 0, 0, 1, 1, 0, false, Horizontal, "Honshougi - Naitou 9 Dan Shougi Hiden (Japan).nes" FFE8507E, NTSC, 4, 0, 32, 8, 0, true, Horizontal, "Nakayoshi to Issho (Japan).nes" FFFDC310, NTSC, 79, 0, 8, 2, 0, false, Horizontal, "Ultimate League Soccer (Italy) (Unl).nes" ================================================ FILE: tetanes-core/src/action.rs ================================================ //! An [`Action`] is an enumerated list of possible state changes to [`ControlDeck`]. //! //! It allows for event handling and test abstractions such as being able to map a custom keybind //! to a given state change. //! //! [`ControlDeck`]: crate::control_deck::ControlDeck use crate::{ apu::Channel, common::{NesRegion, ResetKind}, input::{FourPlayer, JoypadBtn, Player}, mapper::MapperRevision, video::VideoFilter, }; use serde::{Deserialize, Serialize}; /// A user action that maps to a possible state change on [`ControlDeck`]. Used for event /// handling and test abstractions. /// /// [`ControlDeck`]: crate::control_deck::ControlDeck #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Action { /// Reset the [`ControlDeck`](crate::control_deck::ControlDeck). Reset(ResetKind), /// Update the [`Joypad`](crate::input::Joypad) button state. Joypad((Player, JoypadBtn)), /// Toggle the [`Zapper`](crate::input::Zapper) connected state. ToggleZapperConnected, /// Update the [`Zapper`](crate::input::Zapper) aim position. ZapperAim((u16, u16)), /// Update the [`Zapper`](crate::input::Zapper) aim position to offscreen. ZapperAimOffscreen, /// Trigger the [`Zapper`](crate::input::Zapper) trigger. ZapperTrigger, /// Set [`FourPlayer`] mode. FourPlayer(FourPlayer), /// Set the slot to use for save states. SetSaveSlot(u8), /// Save the current state to the currently set save slot. SaveState, /// Load the current state from the currently set save slot. LoadState, /// Toggle the [`Apu`](crate::apu::Apu) [`Channel`]. ToggleApuChannel(Channel), /// Set the [`MapperRevision`]. MapperRevision(MapperRevision), /// Set the [`NesRegion`]. SetNesRegion(NesRegion), /// Set the [`VideoFilter`]. SetVideoFilter(VideoFilter), } ================================================ FILE: tetanes-core/src/apu/dmc.rs ================================================ //! APU DMC (Delta Modulation Channel) implementation. //! //! See: use crate::{ apu::timer::{Timer, TimerCycle}, common::{Clock, NesRegion, Regional, Reset, ResetKind, Sample}, }; use serde::{Deserialize, Serialize}; use tracing::trace; /// APU DMC (Delta Modulation Channel) provides sample playback. /// /// See: #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Dmc { pub region: NesRegion, pub timer: Timer, pub force_silent: bool, pub irq_enabled: bool, pub irq_pending: bool, pub dma_pending: bool, pub loops: bool, pub addr: u16, pub sample_addr: u16, pub bytes_remaining: u16, pub sample_length: u16, pub sample_buffer: u8, pub buffer_empty: bool, pub init: u8, pub output_level: u8, pub bits_remaining: u8, pub shift: u8, pub silence: bool, pub should_clock: bool, } impl Default for Dmc { fn default() -> Self { Self::new(NesRegion::default()) } } impl Dmc { const PERIOD_TABLE_NTSC: [u16; 16] = [ 428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54, ]; const PERIOD_TABLE_PAL: [u16; 16] = [ 398, 354, 316, 298, 276, 236, 210, 198, 176, 148, 132, 118, 98, 78, 66, 50, ]; pub const fn new(region: NesRegion) -> Self { Self { region, timer: Timer::preload(Self::period(region, 0)), force_silent: false, irq_enabled: false, irq_pending: false, dma_pending: false, loops: false, addr: 0xC000, sample_addr: 0x0000, bytes_remaining: 0x0000, sample_length: 0x0001, sample_buffer: 0x00, buffer_empty: true, init: 0, output_level: 0x00, bits_remaining: 0x08, shift: 0x00, silence: true, should_clock: false, } } #[must_use] pub const fn silent(&self) -> bool { self.force_silent } pub const fn set_silent(&mut self, silent: bool) { self.force_silent = silent; } #[cold] #[must_use] pub fn irq_pending_in(&self, cycles_to_run: u32) -> bool { if self.irq_enabled && self.bytes_remaining > 0 { let cycles_to_empty = (u16::from(self.bits_remaining) + (self.bytes_remaining - 1) * 8) * self.timer.period; cycles_to_run >= u32::from(cycles_to_empty) } else { false } } #[must_use] pub const fn dma_addr(&self) -> u16 { self.addr } fn init_sample(&mut self) { self.addr = self.sample_addr; self.bytes_remaining = self.sample_length; trace!( "APU DMC sample started. bytes remaining: {}", self.bytes_remaining ); self.should_clock = self.bytes_remaining > 0; } /// Load a sample into the DMC buffer - returns `true` if an IRQ is triggered. pub fn load_buffer(&mut self, val: u8) { if self.bytes_remaining > 0 { self.sample_buffer = val; self.buffer_empty = false; if self.addr == 0xFFFF { self.addr = 0x8000; } else { self.addr += 1; } self.bytes_remaining -= 1; trace!("APU DMC bytes remaining: {}", self.bytes_remaining); if self.bytes_remaining == 0 { self.should_clock = false; if self.loops { self.init_sample(); } else if self.irq_enabled { self.irq_pending = true; } } } } const fn period(region: NesRegion, val: u8) -> u16 { let index = (val & 0x0F) as usize; match region { NesRegion::Auto | NesRegion::Ntsc | NesRegion::Dendy => { Self::PERIOD_TABLE_NTSC[index] - 1 } NesRegion::Pal => Self::PERIOD_TABLE_PAL[index] - 1, } } /// $4010 DMC timer pub const fn write_timer(&mut self, val: u8) { self.irq_enabled = val & 0x80 == 0x80; self.loops = val & 0x40 == 0x40; self.timer.period = Self::period(self.region, val); if !self.irq_enabled { self.irq_pending = false; } } /// $4011 DMC output pub const fn write_output(&mut self, val: u8) { self.output_level = val & 0x7F; } /// $4012 DMC addr load pub fn write_addr(&mut self, val: u8) { self.sample_addr = 0xC000 | (u16::from(val) << 6); } /// $4013 DMC length pub fn write_length(&mut self, val: u8) { self.sample_length = (u16::from(val) << 4) | 1; } /// $4015 WRITE pub fn set_enabled(&mut self, enabled: bool, cycle: u32) { if !enabled { self.bytes_remaining = 0; self.should_clock = false; } else if self.bytes_remaining == 0 { self.init_sample(); // Delay a number of cycles based on even/odd cycle self.init = if cycle & 0x01 == 0x00 { 2 } else { 3 }; } } #[inline(always)] pub fn should_clock(&mut self) -> bool { if self.init > 0 { self.init -= 1; if self.init == 0 && self.buffer_empty && self.bytes_remaining > 0 { trace!("APU DMC DMA pending"); self.dma_pending = true; } } self.should_clock } } impl Sample for Dmc { fn output(&self) -> f32 { if self.silent() { 0.0 } else { f32::from(self.output_level) } } } impl TimerCycle for Dmc { fn cycle(&self) -> u32 { self.timer.cycle } } impl Clock for Dmc { // Timer // | // v // Reader ---> Buffer ---> Shifter ---> Output level ---> (to the mixer) fn clock(&mut self) { if self.timer.tick() { if !self.silence { // Update output level but clamp to 0..=127 range if self.shift & 0x01 == 0x01 { if self.output_level <= 125 { self.output_level += 2; } } else if self.output_level >= 2 { self.output_level -= 2; } self.shift >>= 1; } if self.bits_remaining > 0 { self.bits_remaining -= 1; } trace!("APU DMC bits remaining: {}", self.bits_remaining); if self.bits_remaining == 0 { self.bits_remaining = 8; self.silence = self.buffer_empty; if !self.buffer_empty { self.shift = self.sample_buffer; self.buffer_empty = true; if self.bytes_remaining > 0 { trace!("APU DMC DMA pending"); self.dma_pending = true; } } } } } } impl Regional for Dmc { fn region(&self) -> NesRegion { self.region } fn set_region(&mut self, region: NesRegion) { self.region = region; self.timer.period = Self::period(region, 0); } } impl Reset for Dmc { fn reset(&mut self, kind: ResetKind) { self.timer.reset(kind); self.timer.period = Self::period(self.region, 0); self.timer.reload(); self.timer.cycle += 1; // FIXME: Startup timing is slightly wrong, DMA tests fail with the // default if let ResetKind::Hard = kind { self.sample_addr = 0xC000; self.sample_length = 1; } self.irq_enabled = false; self.irq_pending = false; self.dma_pending = false; self.loops = false; self.addr = 0x0000; self.bytes_remaining = 0; self.sample_buffer = 0x00; self.buffer_empty = true; self.output_level = 0x00; self.bits_remaining = 0x08; self.shift = 0x00; self.silence = true; self.should_clock = false; } } ================================================ FILE: tetanes-core/src/apu/envelope.rs ================================================ //! APU Envelope implementation. //! //! See: use crate::common::{Clock, Reset, ResetKind}; use serde::{Deserialize, Serialize}; /// APU Envelope provides volume control for APU waveform channels. /// /// See: #[derive(Default, Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Envelope { pub start: bool, pub constant_volume: bool, pub volume: u8, pub divider: u8, pub counter: u8, pub loops: bool, } impl Envelope { pub const fn new() -> Self { Self { start: false, constant_volume: false, volume: 0, divider: 0, counter: 0, loops: false, } } #[inline] #[must_use] pub const fn volume(&self) -> u8 { if self.constant_volume { self.volume } else { self.counter } } #[inline] pub const fn restart(&mut self) { self.start = true; } /// $4000/$4004/$400C Envelope control #[inline] pub const fn write_ctrl(&mut self, val: u8) { self.loops = (val & 0x20) == 0x20; // D5 self.constant_volume = (val & 0x10) == 0x10; // D4 self.volume = val & 0x0F; // D3..D0 } } impl Clock for Envelope { fn clock(&mut self) { if self.start { self.start = false; self.counter = 15; self.divider = self.volume; } else if self.divider > 0 { self.divider -= 1; } else { self.divider = self.volume; if self.counter > 0 { self.counter -= 1; } else if self.loops { self.counter = 15; } } } } impl Reset for Envelope { fn reset(&mut self, _kind: ResetKind) { self.start = false; self.constant_volume = false; self.volume = 0; self.divider = 0; self.counter = 0; } } ================================================ FILE: tetanes-core/src/apu/filter.rs ================================================ //! Digital filters for the [`Apu`](crate::apu::Apu). //! //! See use crate::{ common::{NesRegion, Sample}, cpu::Cpu, }; use serde::{Deserialize, Serialize}; use std::f32::consts::{PI, TAU}; /// A trait for audio processing that consumes samples. pub trait Consume { fn consume(&mut self, sample: f32); } /// Represents a digital filter with certain characteristics. #[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub enum FilterKind { Identity, HighPass, LowPass, } /// An infinite impulse response (IIR) filter. #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Iir { pub alpha: f32, pub prev_output: f32, pub prev_input: f32, pub delta: f32, pub kind: FilterKind, } impl Iir { pub const fn identity() -> Self { Self { alpha: 0.0, prev_output: 0.0, prev_input: 0.0, delta: 0.0, kind: FilterKind::Identity, } } pub fn high_pass(sample_rate: f32, cutoff: f32) -> Self { let period = 1.0 / sample_rate; let cutoff_period = 1.0 / cutoff; let alpha = cutoff_period / (cutoff_period + period); Self { alpha, prev_output: 0.0, prev_input: 0.0, delta: 0.0, kind: FilterKind::HighPass, } } pub fn low_pass(sample_rate: f32, cutoff: f32) -> Self { let period = 1.0 / sample_rate; let cutoff_period = 1.0 / (TAU * cutoff); let alpha = cutoff_period / (cutoff_period + period); Self { alpha, prev_output: 0.0, prev_input: 0.0, delta: 0.0, kind: FilterKind::LowPass, } } } impl Consume for Iir { fn consume(&mut self, sample: f32) { self.prev_output = self.output(); self.delta = sample - self.prev_input; self.prev_input = sample; } } impl Sample for Iir { fn output(&self) -> f32 { match self.kind { FilterKind::Identity => self.prev_input, FilterKind::HighPass => self.alpha * self.prev_output + self.alpha * self.delta, FilterKind::LowPass => self.prev_output + self.alpha * self.delta, } } } /// A finite impulse response (FIR) filter. #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Fir { pub kernel: Box<[f32]>, pub inputs: Box<[f32]>, pub input_index: usize, pub kind: FilterKind, } impl Fir { pub fn low_pass(sample_rate: f32, cutoff: f32, window_size: usize) -> Self { Self { kernel: windowed_sinc_kernel(sample_rate, cutoff, window_size), inputs: vec![0.0; window_size + 1].into(), input_index: 0, kind: FilterKind::LowPass, } } } impl Consume for Fir { fn consume(&mut self, sample: f32) { self.inputs[self.input_index] = sample; self.input_index += 1; if self.input_index >= self.inputs.len() { self.input_index = 0; } } } impl Sample for Fir { fn output(&self) -> f32 { let kernel = &self.kernel[..]; let inputs = &self.inputs[..]; let idx = self.input_index; let mut sum = 0f32; // input_index..inputs.len() let end = (inputs.len() - idx).min(kernel.len()); for i in 0..end { sum = kernel[i].mul_add(inputs[i + idx], sum); } // 0..input_index for i in 0..idx { sum = kernel[end + i].mul_add(inputs[i], sum); } sum } } /// Generate a windowed sinc kernel. pub fn windowed_sinc_kernel(sample_rate: f32, cutoff: f32, window_size: usize) -> Box<[f32]> { fn blackman_window(index: usize, window_size: usize) -> f32 { let i = index as f32; let m = window_size as f32; 0.42 - 0.5 * ((TAU * i) / m).cos() + 0.08 * ((2.0 * TAU * i) / m).cos() } fn sinc(index: usize, fc: f32, window_size: usize) -> f32 { let i = index as f32; let m = window_size as f32; let shifted_index = i - (m / 2.0); if index == (window_size / 2) { TAU * fc } else { (TAU * fc * shifted_index).sin() / shifted_index } } fn normalize(input: Box<[f32]>) -> Box<[f32]> { let sum: f32 = input.iter().sum(); input.into_iter().map(|x| x / sum).collect() } let fc = cutoff / sample_rate; let mut kernel = Vec::with_capacity(window_size); for i in 0..=window_size { kernel.push(sinc(i, fc, window_size) * blackman_window(i, window_size)); } normalize(kernel.into()) } /// Represents a digital audio filter. #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub enum Filter { Iir(Iir), Fir(Fir), } impl Consume for Filter { fn consume(&mut self, sample: f32) { match self { Filter::Iir(iir) => iir.consume(sample), Filter::Fir(fir) => fir.consume(sample), } } } impl Sample for Filter { fn output(&self) -> f32 { match self { Filter::Iir(iir) => iir.output(), Filter::Fir(fir) => fir.output(), } } } impl From for Filter { fn from(filter: Iir) -> Self { Self::Iir(filter) } } impl From for Filter { fn from(filter: Fir) -> Self { Self::Fir(filter) } } /// Represents a filter with a given sampling period. #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct SampledFilter { pub filter: Filter, pub sample_period: f32, pub period_counter: f32, } impl SampledFilter { pub fn new(filter: impl Into, sample_rate: f32) -> Self { Self { filter: filter.into(), sample_period: 1.0 / sample_rate, period_counter: 0.0, } } } /// Represents a chain of filters for a given [`NesRegion`]. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FilterChain { pub region: NesRegion, pub dt: f32, pub filters: [SampledFilter; 6], } impl FilterChain { pub fn new(region: NesRegion, output_rate: f32) -> Self { let clock_rate = Cpu::region_clock_rate(region); let intermediate_sample_rate = output_rate * 2.0 + (PI / 32.0); let intermediate_cutoff = output_rate * 0.4; let filters = [ SampledFilter::new(Iir::identity(), 1.0), SampledFilter::new(Iir::low_pass(clock_rate, intermediate_cutoff), clock_rate), // first-order high-pass filter at 90 Hz SampledFilter::new( Iir::high_pass(intermediate_sample_rate, 90.0), intermediate_sample_rate, ), // first-order high-pass filter at 440 Hz SampledFilter::new( Iir::high_pass(intermediate_sample_rate, 440.0), intermediate_sample_rate, ), // first-order low-pass filter at 14 kHz SampledFilter::new( Iir::low_pass(intermediate_sample_rate, 14000.0), intermediate_sample_rate, ), // TODO: Support famicom filter selection // // first-order high-pass filter at 37 Hz // filters.push(SampledFilter::new( // Iir::high_pass(intermediate_sample_rate, 37.0), // intermediate_sample_rate, // )); // high-quality low-pass filter { let window_size = 160; let intermediate_cutoff = output_rate * 0.45; SampledFilter::new( Fir::low_pass(intermediate_sample_rate, intermediate_cutoff, window_size), intermediate_sample_rate, ) }, ]; Self { region, dt: 1.0 / clock_rate, filters, } } } impl Consume for FilterChain { fn consume(&mut self, sample: f32) { // Add sample to identity filter self.filters[0].filter.consume(sample); for i in 1..self.filters.len() { let prev = i - 1; let current = i; while self.filters[current].period_counter >= self.filters[current].sample_period { self.filters[current].period_counter -= self.filters[current].sample_period; let prev_output = self.filters[prev].filter.output(); self.filters[current].filter.consume(prev_output); } self.filters[current].period_counter += self.dt; } } } impl Sample for FilterChain { fn output(&self) -> f32 { self.filters.last().map_or(0.0, |f| f.filter.output()) } } ================================================ FILE: tetanes-core/src/apu/frame_counter.rs ================================================ //! The APU Frame Counter implementation. //! //! See: use crate::common::{NesRegion, Reset, ResetKind}; use serde::{Deserialize, Serialize}; use tracing::trace; /// The APU Frame Counter generates a low-frequency clock for each APU channel. /// /// See: #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct FrameCounter { pub region: NesRegion, pub step_cycles: [u32; 6], pub step: usize, pub mode: u8, pub write_buffer: Option, pub write_delay: u8, pub block_counter: u8, pub cycle: u32, pub inhibit_irq: bool, // Set by $4017 D6 pub irq_pending: bool, } /// The Frame Counter clock type. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum FrameType { #[default] None, Quarter, Half, } impl FrameCounter { const STEP4_CYCLES_NTSC: [u32; 6] = [7457, 14913, 22371, 29828, 29829, 29830]; const STEP5_CYCLES_NTSC: [u32; 6] = [7457, 14913, 22371, 29829, 37281, 37282]; const STEP4_CYCLES_PAL: [u32; 6] = [8313, 16627, 24939, 33252, 33253, 33254]; const STEP5_CYCLES_PAL: [u32; 6] = [8313, 16627, 24939, 33253, 41565, 41566]; const FRAME_TYPE: [FrameType; 6] = [ FrameType::Quarter, FrameType::Half, FrameType::Quarter, FrameType::None, FrameType::Half, FrameType::None, ]; pub const fn new(region: NesRegion) -> Self { let mode = 0; let step_cycles = Self::step_cycles(mode, region); Self { region, step_cycles, step: 0, mode, write_buffer: None, write_delay: 0, block_counter: 0, cycle: 0, inhibit_irq: false, irq_pending: false, } } pub const fn set_region(&mut self, region: NesRegion) { self.region = region; self.step_cycles = Self::step_cycles(self.mode, region); } const fn step_cycles(mode: u8, region: NesRegion) -> [u32; 6] { match (mode, region) { (0, NesRegion::Auto | NesRegion::Ntsc | NesRegion::Dendy) => Self::STEP4_CYCLES_NTSC, (0, NesRegion::Pal) => Self::STEP4_CYCLES_PAL, (_, NesRegion::Auto | NesRegion::Ntsc | NesRegion::Dendy) => Self::STEP5_CYCLES_NTSC, (_, NesRegion::Pal) => Self::STEP5_CYCLES_PAL, } } /// On write to $4017 pub fn write(&mut self, val: u8, cycle: u32) { self.write_buffer = Some(val); // Writes occurring on odd clocks are delayed self.write_delay = if cycle & 0x01 == 0x01 { 4 } else { 3 }; trace!("APU $4017 write delay cycles: {}", self.write_delay); self.inhibit_irq = val & 0x40 == 0x40; // D6 if self.inhibit_irq { trace!("APU Frame Counter IRQ inhibit"); self.irq_pending = false; } } #[inline(always)] pub const fn should_clock(&mut self, cycles: u32) -> bool { self.block_counter > 0 || self.write_buffer.is_some() || (self.cycle + cycles) >= (self.step_cycles[self.step] - 1) } // mode 0: 4-step effective rate (approx) // --------------------------------------- // - - - f f f 60 Hz // - l - - l - 120 Hz // e e e - e - 240 Hz // // mode 1: 5-step effective rate (approx) // --------------------------------------- // - - - - - - (interrupt flag never set) // - l - - l - 96 Hz // e e e - e - 192 Hz pub fn clock_with(&mut self, cycles: u32, mut on_clock: impl FnMut(FrameType)) -> u32 { let mut cycles_ran = 0; let step_cycles = self.step_cycles[self.step]; if self.cycle + cycles >= step_cycles { if !self.inhibit_irq && self.mode == 0 && self.step >= 3 { trace!( "APU Frame Counter IRQ pending - cycles: {} >= {step_cycles}", self.cycle + cycles ); self.irq_pending = true; } let ty = Self::FRAME_TYPE[self.step]; if ty != FrameType::None && self.block_counter == 0 { on_clock(ty); // Do not allow writes to $4017 to clock for the next cycle (odd + following even // cycle) self.block_counter = 2; } if step_cycles >= self.cycle { cycles_ran = step_cycles - self.cycle; } self.step += 1; if self.step == 6 { trace!( "APU Frame Counter total cycles: {}", self.cycle + cycles_ran ); self.step = 0; self.cycle = 0; } else { self.cycle += cycles_ran; } } else { cycles_ran = cycles; self.cycle += cycles_ran; } if let Some(val) = self.write_buffer { self.write_delay -= 1; if self.write_delay == 0 { self.mode = if val & 0x80 == 0x80 { 1 } else { 0 }; self.step_cycles = Self::step_cycles(self.mode, self.region); self.step = 0; self.cycle = 0; self.write_buffer = None; if self.mode == 1 && self.block_counter == 0 { // Writing to $4017 with bit 7 set will immediately generate a quarter/half frame on_clock(FrameType::Half); self.block_counter = 2; } } } if self.block_counter > 0 { self.block_counter -= 1; } cycles_ran } } impl Reset for FrameCounter { fn reset(&mut self, kind: ResetKind) { self.cycle = 0; if kind == ResetKind::Hard { self.mode = 0; self.step_cycles = Self::step_cycles(self.mode, self.region); // After reset, APU acts as if $4017 was written 9-12 clocks before first instruction, // Reset acts as if $00 was written to $4017 self.write(0x00, 0); self.write_delay -= 1; // FIXME: Startup timing is slightly wrong, reset_timing fails // with the default } self.step = 0; self.block_counter = 0; self.irq_pending = false; } } ================================================ FILE: tetanes-core/src/apu/length_counter.rs ================================================ //! APU Length Counter implementation. //! //! See: use crate::{ apu::Channel, common::{Clock, Reset, ResetKind}, }; use serde::{Deserialize, Serialize}; /// APU Length Counter provides duration control for APU waveform channels. /// /// See: #[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct LengthCounter { pub enabled: bool, pub channel: Channel, pub halt: bool, pub new_halt: bool, pub counter: u8, // Entry into LENGTH_TABLE pub previous_counter: u8, pub reload: u8, } impl LengthCounter { const LENGTH_TABLE: [u8; 32] = [ 10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, 12, 16, 24, 18, 48, 20, 96, 22, 192, 24, 72, 26, 16, 28, 32, 30, ]; pub const fn new(channel: Channel) -> Self { Self { enabled: false, channel, halt: false, new_halt: false, counter: 0, previous_counter: 0, reload: 0, } } #[inline] pub const fn write(&mut self, val: u8) { if self.enabled { self.reload = Self::LENGTH_TABLE[val as usize]; // D7..D3 self.previous_counter = self.counter; } } #[inline] pub const fn set_enabled(&mut self, enabled: bool) { if !enabled { self.counter = 0; } self.enabled = enabled; } #[inline] pub const fn reload(&mut self) { if self.reload > 0 { if self.counter == self.previous_counter { self.counter = self.reload; } self.reload = 0; } self.halt = self.new_halt; } #[inline] pub const fn write_ctrl(&mut self, halt: bool) { self.new_halt = halt; } } impl Clock for LengthCounter { fn clock(&mut self) { if self.counter > 0 && !self.halt { self.counter -= 1; } } } impl Reset for LengthCounter { fn reset(&mut self, kind: ResetKind) { self.enabled = false; match kind { ResetKind::Soft => { if self.channel != Channel::Triangle { self.halt = false; self.new_halt = false; self.counter = 0; self.reload = 0; self.previous_counter = 0; } } ResetKind::Hard => { self.halt = false; self.new_halt = false; self.counter = 0; self.reload = 0; self.previous_counter = 0; } } } } ================================================ FILE: tetanes-core/src/apu/noise.rs ================================================ //! APU Noise Channel implementation. //! //! See: use crate::{ apu::{ Channel, envelope::Envelope, length_counter::LengthCounter, timer::{Timer, TimerCycle}, }, common::{Clock, NesRegion, Regional, Reset, ResetKind, Sample}, }; use serde::{Deserialize, Serialize}; /// Noise shift mode. #[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] pub enum ShiftMode { /// Zero (XOR bits 0 and 1) Zero, /// One (XOR bits 0 and 6) One, } /// APU Noise Channel provides pseudo-random noise generation. /// /// See: #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Noise { pub region: NesRegion, pub timer: Timer, pub shift: u16, pub shift_mode: ShiftMode, pub length: LengthCounter, pub envelope: Envelope, pub force_silent: bool, } impl Default for Noise { fn default() -> Self { Self::new(NesRegion::default()) } } impl Noise { const PERIOD_TABLE_NTSC: [u16; 16] = [ 4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068, ]; const PERIOD_TABLE_PAL: [u16; 16] = [ 4, 8, 14, 30, 60, 88, 118, 148, 188, 236, 354, 472, 708, 944, 1890, 3778, ]; pub const fn new(region: NesRegion) -> Self { Self { region, timer: Timer::new(Self::period(region, 0)), shift: 1, // defaults to 1 on power up shift_mode: ShiftMode::Zero, length: LengthCounter::new(Channel::Noise), envelope: Envelope::new(), force_silent: false, } } #[must_use] pub const fn is_muted(&self) -> bool { (self.shift & 0x01) == 0x01 || self.silent() } #[must_use] pub const fn silent(&self) -> bool { self.force_silent } pub const fn set_silent(&mut self, silent: bool) { self.force_silent = silent; } const fn period(region: NesRegion, val: u8) -> u16 { let index = (val & 0x0F) as usize; match region { NesRegion::Auto | NesRegion::Ntsc | NesRegion::Dendy => { Self::PERIOD_TABLE_NTSC[index] - 1 } NesRegion::Pal => Self::PERIOD_TABLE_PAL[index] - 1, } } pub fn clock_quarter_frame(&mut self) { self.envelope.clock(); } pub fn clock_half_frame(&mut self) { self.clock_quarter_frame(); self.length.clock(); } /// $400C Noise control pub const fn write_ctrl(&mut self, val: u8) { self.length.write_ctrl((val & 0x20) == 0x20); // !D5 self.envelope.write_ctrl(val); } /// $400E Noise timer pub const fn write_timer(&mut self, val: u8) { self.timer.period = Self::period(self.region, val); self.shift_mode = if (val & 0x80) == 0x80 { ShiftMode::One } else { ShiftMode::Zero }; } /// $400F Length counter pub const fn write_length(&mut self, val: u8) { self.length.write(val >> 3); self.envelope.restart(); } pub const fn set_enabled(&mut self, enabled: bool) { self.length.set_enabled(enabled); } pub const fn volume(&self) -> u8 { if self.length.counter > 0 { self.envelope.volume() } else { 0 } } } impl Sample for Noise { fn output(&self) -> f32 { if self.is_muted() { 0f32 } else { f32::from(self.volume()) } } } impl TimerCycle for Noise { fn cycle(&self) -> u32 { self.timer.cycle } } impl Clock for Noise { // Timer --> Shift Register Length Counter // | | // v v // Envelope -------> Gate ----------> Gate --> (to mixer) fn clock(&mut self) { if self.timer.tick() { let shift_by = if self.shift_mode == ShiftMode::One { 6 } else { 1 }; let feedback = (self.shift & 0x01) ^ ((self.shift >> shift_by) & 0x01); self.shift >>= 1; self.shift |= feedback << 14; } } } impl Regional for Noise { fn region(&self) -> NesRegion { self.region } fn set_region(&mut self, region: NesRegion) { self.region = region; } } impl Reset for Noise { fn reset(&mut self, kind: ResetKind) { self.timer.reset(kind); self.timer.period = Self::period(self.region, 0); self.length.reset(kind); self.envelope.reset(kind); self.shift = 1; self.shift_mode = ShiftMode::Zero; } } ================================================ FILE: tetanes-core/src/apu/pulse.rs ================================================ //! APU Pulse Channel implementation. //! //! See: use crate::{ apu::{ Channel, envelope::Envelope, length_counter::LengthCounter, timer::{Timer, TimerCycle}, }, common::{Clock, Reset, ResetKind, Sample}, }; use serde::{Deserialize, Serialize}; /// Pulse Channel output frequency. Supports MMC5 being able to pulse at ultrasonic frequencies. #[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] pub enum OutputFreq { Default, Ultrasonic, } /// Pulse Channel selection. #[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] pub enum PulseChannel { One, Two, } /// APU Pulse Channel provides square wave generation. /// /// See: #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Pulse { pub channel: PulseChannel, pub real_period: u16, pub timer: Timer, pub duty: u8, // Select row in DUTY_TABLE pub duty_cycle: u8, // Select column in DUTY_TABLE pub length: LengthCounter, pub envelope: Envelope, pub sweep: Sweep, pub force_silent: bool, pub output_freq: OutputFreq, } impl Default for Pulse { fn default() -> Self { Self::new(PulseChannel::One, OutputFreq::Default) } } impl Pulse { const DUTY_TABLE: [[u8; 8]; 4] = [ [0, 0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 0, 1, 1], [0, 0, 0, 0, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 0, 0], ]; pub const fn new(channel: PulseChannel, output_freq: OutputFreq) -> Self { Self { channel, real_period: 0, timer: Timer::new(0), duty: 0u8, duty_cycle: 0, length: LengthCounter::new(match channel { PulseChannel::One => Channel::Pulse1, PulseChannel::Two => Channel::Pulse2, }), envelope: Envelope::new(), sweep: Sweep::new(channel), force_silent: false, output_freq, } } #[inline] pub fn is_muted(&self) -> bool { // MMC5 doesn't mute at ultasonic frequencies self.output_freq == OutputFreq::Default && (self.real_period < 8 || (!self.sweep.negate && self.sweep.target_period > 0x7FF)) || self.silent() } #[must_use] pub const fn silent(&self) -> bool { self.force_silent } pub const fn set_silent(&mut self, silent: bool) { self.force_silent = silent; } const fn update_target_period(&mut self) { let delta = self.real_period >> self.sweep.shift; if self.sweep.negate { self.sweep.target_period = self.real_period - delta; if let PulseChannel::One = self.channel { self.sweep.target_period = self.sweep.target_period.wrapping_sub(1); } } else { self.sweep.target_period = self.real_period + delta; } } const fn set_period(&mut self, period: u16) { self.real_period = period; self.timer.period = (period * 2) + 1; self.update_target_period(); } const fn clock_sweep(&mut self) { self.sweep.divider = self.sweep.divider.wrapping_sub(1); if self.sweep.divider == 0 { if self.sweep.shift > 0 && self.sweep.enabled && self.real_period >= 8 && self.sweep.target_period <= 0x7FF { self.set_period(self.sweep.target_period); } self.sweep.divider = self.sweep.period; } if self.sweep.reload { self.sweep.divider = self.sweep.period; self.sweep.reload = false; } } pub fn clock_quarter_frame(&mut self) { self.envelope.clock(); } pub fn clock_half_frame(&mut self) { self.clock_quarter_frame(); self.length.clock(); self.clock_sweep(); } /// $4000/$4004 Pulse control pub const fn write_ctrl(&mut self, val: u8) { self.length.write_ctrl((val & 0x20) == 0x20); // !D5 self.envelope.write_ctrl(val); self.duty = (val & 0xC0) >> 6; } /// $4001/$4005 Pulse sweep pub const fn write_sweep(&mut self, val: u8) { self.sweep.enabled = (val & 0x80) == 0x80; self.sweep.negate = (val & 0x08) == 0x08; self.sweep.period = ((val & 0x70) >> 4) + 1; self.sweep.shift = val & 0x07; self.update_target_period(); self.sweep.reload = true; } /// $4002/$4006 Pulse timer lo pub fn write_timer_lo(&mut self, val: u8) { self.set_period(self.real_period & 0x0700 | u16::from(val)); } /// $4003/$4007 Pulse timer hi pub fn write_timer_hi(&mut self, val: u8) { self.length.write(val >> 3); self.set_period(self.real_period & 0xFF | (u16::from(val & 0x07) << 8)); self.duty_cycle = 0; self.envelope.restart(); } pub const fn set_enabled(&mut self, enabled: bool) { self.length.set_enabled(enabled); } pub const fn volume(&self) -> u8 { if self.length.counter > 0 { self.envelope.volume() } else { 0 } } } impl Sample for Pulse { fn output(&self) -> f32 { if self.is_muted() { 0.0 } else { f32::from( Self::DUTY_TABLE[self.duty as usize][self.duty_cycle as usize] * self.volume(), ) } } } impl TimerCycle for Pulse { fn cycle(&self) -> u32 { self.timer.cycle } } impl Clock for Pulse { // Sweep -----> Timer // | | // | | // | v // | Sequencer Length Counter // | | | // | | | // v v v // Envelope -------> Gate -----> Gate -------> Gate --->(to mixer) fn clock(&mut self) { if self.timer.tick() { self.duty_cycle = self.duty_cycle.wrapping_sub(1) & 0x07; } } } impl Reset for Pulse { fn reset(&mut self, kind: ResetKind) { self.timer.reset(kind); self.length.reset(kind); self.envelope.reset(kind); self.sweep.reset(kind); self.update_target_period(); self.duty = 0; self.duty_cycle = 0; } } /// APU Sweep provides frequency sweeping for the APU pulse channels. /// /// See: #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Sweep { pub enabled: bool, pub channel: PulseChannel, pub negate: bool, // Treats PulseChannel 1 differently than PulseChannel 2 pub reload: bool, pub shift: u8, pub timer: u16, pub divider: u8, pub period: u8, pub target_period: u16, } impl Sweep { pub const fn new(channel: PulseChannel) -> Self { Self { enabled: false, channel, negate: false, reload: false, shift: 0, timer: 0, divider: 0, period: 0, target_period: 0, } } } impl Reset for Sweep { fn reset(&mut self, _kind: ResetKind) { self.enabled = false; self.period = 0; self.negate = false; self.reload = false; self.shift = 0; self.divider = 0; self.target_period = 0; } } ================================================ FILE: tetanes-core/src/apu/timer.rs ================================================ //! Timer abstraction for the [`Apu`](crate::apu::Apu). use crate::common::{Reset, ResetKind}; use serde::{Deserialize, Serialize}; /// Trait for types that have timers. pub trait TimerCycle { fn cycle(&self) -> u32; } /// A timer that generates a clock signal based on a divider and a period. The timer is clocked /// every (period + 1) * divider cycles. #[derive(Default, Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Timer { pub cycle: u32, pub counter: u16, pub period: u16, } impl Timer { pub const fn new(period: u16) -> Self { Self { cycle: 0, counter: 0, period, } } pub const fn preload(period: u16) -> Self { let mut timer = Self::new(period); timer.counter = timer.period; timer } pub const fn reload(&mut self) { self.counter = self.period; } pub const fn tick(&mut self) -> bool { self.cycle += 1; if self.counter == 0 { self.counter = self.period; return true; } self.counter -= 1; false } } impl Reset for Timer { fn reset(&mut self, _kind: ResetKind) { self.counter = 0; self.period = 0; self.cycle = 0; } } #[cfg(test)] mod tests { use super::*; #[test] fn timer() { // Period (10 + 1) == 11 + initial clock let mut timer = Timer::new(10); let mut expected = [false; 23]; expected[0] = true; expected[11] = true; expected[22] = true; assert_eq!(expected, [(); 23].map(|_| timer.tick())); assert_eq!(23, timer.cycle); // Period (10 + 1) == 11 let mut timer = Timer::preload(10); let mut expected = [false; 22]; expected[10] = true; expected[21] = true; assert_eq!(expected, [(); 22].map(|_| timer.tick())); assert_eq!(22, timer.cycle); // Period (10 * 2) + 1 == 22 + initial clock let mut timer = Timer::new((10 * 2) + 1); let mut expected = [false; 45]; expected[0] = true; expected[22] = true; expected[44] = true; assert_eq!(expected, [(); 45].map(|_| timer.tick())); assert_eq!(45, timer.cycle); // Period (10 * 2) + 1 == 22 let mut timer = Timer::preload((10 * 2) + 1); let mut expected = [false; 44]; expected[21] = true; expected[43] = true; assert_eq!(expected, [(); 44].map(|_| timer.tick())); assert_eq!(44, timer.cycle); } } ================================================ FILE: tetanes-core/src/apu/triangle.rs ================================================ //! APU Triangle Channel implementation. //! //! See: use crate::{ apu::{ Channel, length_counter::LengthCounter, timer::{Timer, TimerCycle}, }, common::{Clock, Reset, ResetKind, Sample}, }; use serde::{Deserialize, Serialize}; /// APU Triangle Channel provides triangle wave generation. /// /// See: #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Triangle { pub timer: Timer, pub sequence: u8, pub length: LengthCounter, pub linear: LinearCounter, pub force_silent: bool, } impl Default for Triangle { fn default() -> Self { Self::new() } } impl Triangle { const SEQUENCE: [u8; 32] = [ 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, ]; pub const fn new() -> Self { Self { timer: Timer::new(0), sequence: 0, length: LengthCounter::new(Channel::Triangle), linear: LinearCounter::new(), force_silent: false, } } #[must_use] pub const fn silent(&self) -> bool { self.force_silent } pub const fn set_silent(&mut self, silent: bool) { self.force_silent = silent; } pub fn clock_quarter_frame(&mut self) { self.linear.clock(); } pub fn clock_half_frame(&mut self) { self.clock_quarter_frame(); self.length.clock(); } /// $4008 Linear counter control pub const fn write_linear_counter(&mut self, val: u8) { self.linear.control = (val & 0x80) == 0x80; // D7 self.linear.write(val & 0x7F); // D6..D0; self.length.write_ctrl(self.linear.control); // !D7 } /// $400A Triangle timer lo pub fn write_timer_lo(&mut self, val: u8) { self.timer.period = (self.timer.period & 0xFF00) | u16::from(val); // D7..D0 } /// $400B Triangle timer high pub fn write_timer_hi(&mut self, val: u8) { self.length.write(val >> 3); self.timer.period = (self.timer.period & 0x00FF) | (u16::from(val & 0x07) << 8); // D2..D0 self.linear.reload = true; } pub const fn set_enabled(&mut self, enabled: bool) { self.length.set_enabled(enabled); } } impl Sample for Triangle { fn output(&self) -> f32 { if self.silent() { 0.0 } else if self.timer.period < 2 { // This is normally silenced by a lowpass filter on real hardware // See: https://forums.nesdev.org/viewtopic.php?t=10658 7.5 } else { f32::from(Self::SEQUENCE[self.sequence as usize]) } } } impl TimerCycle for Triangle { fn cycle(&self) -> u32 { self.timer.cycle } } impl Clock for Triangle { // Linear Counter Length Counter // | | // v v // Timer ---> Gate ----------> Gate ---> Sequencer ---> (to mixer) fn clock(&mut self) { if self.timer.tick() && self.length.counter > 0 && self.linear.counter > 0 { self.sequence = (self.sequence + 1) & 0x1F; } } } impl Reset for Triangle { fn reset(&mut self, kind: ResetKind) { self.length.reset(kind); self.linear.reset(kind); self.sequence = 0; } } /// APU Linear Counter provides duration control for the APU triangle channel. /// /// See: #[derive(Default, Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct LinearCounter { pub reload: bool, pub control: bool, pub counter_reload: u8, pub counter: u8, } impl LinearCounter { pub const fn new() -> Self { Self { reload: false, control: false, counter_reload: 0u8, counter: 0u8, } } pub const fn write(&mut self, val: u8) { self.counter_reload = val; } } impl Clock for LinearCounter { fn clock(&mut self) { if self.reload { self.counter = self.counter_reload; } else if self.counter > 0 { self.counter -= 1; } if !self.control { self.reload = false; } } } impl Reset for LinearCounter { fn reset(&mut self, _kind: ResetKind) { self.counter = 0; self.counter_reload = 0; self.reload = false; self.control = false; } } ================================================ FILE: tetanes-core/src/apu.rs ================================================ //! NES APU (Audio Processing Unit) implementation. //! //! See: use crate::{ apu::{ dmc::Dmc, filter::{Consume, FilterChain}, frame_counter::{FrameCounter, FrameType}, noise::Noise, pulse::{OutputFreq, Pulse, PulseChannel}, timer::TimerCycle, triangle::Triangle, }, common::{Clock, NesRegion, Regional, Reset, ResetKind, Sample}, cpu::Cpu, }; use serde::{Deserialize, Serialize}; use thiserror::Error; use tracing::{trace, warn}; pub mod dmc; pub mod noise; pub mod pulse; pub mod triangle; pub mod envelope; pub mod filter; pub mod frame_counter; pub mod length_counter; pub mod timer; /// Error when parsing `Channel` from a `usize`. #[derive(Error, Debug)] #[must_use] #[error("failed to parse `Channel`")] pub struct ParseChannelError; /// [`Apu`] Channel. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[must_use] pub enum Channel { Pulse1, Pulse2, Triangle, Noise, Dmc, Mapper, } impl TryFrom for Channel { type Error = ParseChannelError; fn try_from(value: usize) -> Result { match value { 0 => Ok(Self::Pulse1), 1 => Ok(Self::Pulse2), 2 => Ok(Self::Triangle), 3 => Ok(Self::Noise), 4 => Ok(Self::Dmc), 5 => Ok(Self::Mapper), _ => Err(ParseChannelError), } } } /// NES APU (Audio Processing Unit). /// /// See: #[derive(Clone, Serialize, Deserialize)] #[must_use] pub struct Apu { pub frame_counter: FrameCounter, pub master_clock: u32, pub cpu_cycle: u32, pub clock: u32, pub clock_rate: f32, pub region: NesRegion, pub pulse1: Pulse, pub pulse2: Pulse, pub triangle: Triangle, pub noise: Noise, pub dmc: Dmc, pub filter_chain: FilterChain, #[serde(skip, default = "Apu::default_channel_outputs")] pub channel_outputs: Box<[f32]>, #[serde(skip)] pub audio_samples: Vec, pub sample_rate: f32, pub sample_period: f32, pub sample_counter: f32, pub speed: f32, pub mapper_enabled: bool, pub skip_mixing: bool, pub should_clock: bool, } impl Default for Apu { fn default() -> Self { Self::new(NesRegion::default()) } } impl Apu { pub const DEFAULT_SAMPLE_RATE: f32 = 44_100.0; // 5 APU channels + 1 Mapper channel pub const MAX_CHANNEL_COUNT: usize = 6; pub const CYCLE_SIZE: u32 = 10_000; /// Create a new APU instance. pub fn new(region: NesRegion) -> Self { let clock_rate = Cpu::region_clock_rate(region); let sample_rate = Self::DEFAULT_SAMPLE_RATE; let sample_period = clock_rate / sample_rate; Self { frame_counter: FrameCounter::new(region), master_clock: 0, cpu_cycle: 0, clock: 0, clock_rate, region, pulse1: Pulse::new(PulseChannel::One, OutputFreq::Default), pulse2: Pulse::new(PulseChannel::Two, OutputFreq::Default), triangle: Triangle::new(), noise: Noise::new(region), dmc: Dmc::new(region), filter_chain: FilterChain::new(region, sample_rate), channel_outputs: Self::default_channel_outputs(), audio_samples: Vec::with_capacity((sample_rate / 60.0) as usize), sample_rate, sample_period, sample_counter: sample_period, speed: 1.0, mapper_enabled: true, skip_mixing: false, should_clock: false, } } pub fn default_channel_outputs() -> Box<[f32]> { vec![0.0; Self::MAX_CHANNEL_COUNT * Self::CYCLE_SIZE as usize].into() } #[inline(always)] pub fn add_mapper_output(&mut self, output: f32) { self.channel_outputs [(self.master_clock as usize * Self::MAX_CHANNEL_COUNT) + Channel::Mapper as usize] = output; } /// Filter and mix audio sample based on region sampling rate. #[inline] pub fn process_outputs(&mut self) { if self.skip_mixing { return; } for outputs in self .channel_outputs .chunks_exact(Self::MAX_CHANNEL_COUNT) .take(self.master_clock as usize) { let [pulse1, pulse2, triangle, noise, dmc, mapper] = outputs else { warn!("invalid channel outputs"); return; }; let pulse_idx = (pulse1 + pulse2) as usize; let tnd_idx = (3.0f32.mul_add(*triangle, 2.0 * noise) + dmc) as usize; let apu_output = PULSE_TABLE[pulse_idx] + TND_TABLE[tnd_idx]; let mapper_output = self.mapper_enabled as u8 as f32 * *mapper; self.filter_chain.consume(apu_output + mapper_output); self.sample_counter -= 1.0; if self.sample_counter <= 1.0 { self.audio_samples.push(self.filter_chain.output()); self.sample_counter += self.sample_period; } } } /// Set the audio sample rate. #[inline] pub fn set_sample_rate(&mut self, sample_rate: f32) { self.sample_rate = sample_rate; let sample_rate = self.sample_rate / self.speed; self.filter_chain = FilterChain::new(self.region, sample_rate); let clock_rate = Cpu::region_clock_rate(self.region); self.sample_period = clock_rate / sample_rate; } /// Set the frame speed of the APU, which affects the sampling rate. pub fn set_frame_speed(&mut self, speed: f32) { self.speed = speed; let sample_rate = self.sample_rate / self.speed; self.filter_chain = FilterChain::new(self.region, sample_rate); let clock_rate = Cpu::region_clock_rate(self.region); self.sample_period = clock_rate / sample_rate; } /// Whether a given channel is enabled. #[must_use] pub const fn channel_enabled(&self, channel: Channel) -> bool { match channel { Channel::Pulse1 => !self.pulse1.silent(), Channel::Pulse2 => !self.pulse2.silent(), Channel::Triangle => !self.triangle.silent(), Channel::Noise => !self.noise.silent(), Channel::Dmc => !self.dmc.silent(), Channel::Mapper => self.mapper_enabled, } } /// Enable or disable a given channel. pub const fn set_channel_enabled(&mut self, channel: Channel, enabled: bool) { match channel { Channel::Pulse1 => self.pulse1.set_silent(!enabled), Channel::Pulse2 => self.pulse2.set_silent(!enabled), Channel::Triangle => self.triangle.set_silent(!enabled), Channel::Noise => self.noise.set_silent(!enabled), Channel::Dmc => self.dmc.set_silent(!enabled), Channel::Mapper => self.mapper_enabled = enabled, } } /// Toggle a given channel. pub const fn toggle_channel(&mut self, channel: Channel) { match channel { Channel::Pulse1 => self.pulse1.set_silent(!self.pulse1.silent()), Channel::Pulse2 => self.pulse2.set_silent(!self.pulse2.silent()), Channel::Triangle => self.triangle.set_silent(!self.triangle.silent()), Channel::Noise => self.noise.set_silent(!self.noise.silent()), Channel::Dmc => self.dmc.set_silent(!self.dmc.silent()), Channel::Mapper => self.mapper_enabled = !self.mapper_enabled, } } pub fn clock_lazy(&mut self) { self.cpu_cycle = self.cpu_cycle.wrapping_add(1); self.master_clock += 1; if self.master_clock == Self::CYCLE_SIZE - 1 { self.clock_sync(); } else if self.should_clock() { self.clock_to(self.master_clock); } } /// Runs all componnets up to master clock, synchronizing them. #[cold] #[inline(never)] pub fn clock_sync(&mut self) { self.clock_to(self.master_clock); self.process_outputs(); debug_assert_eq!(self.master_clock, self.clock); self.master_clock = 0; self.clock = 0; self.pulse1.timer.cycle = 0; self.pulse2.timer.cycle = 0; self.triangle.timer.cycle = 0; self.noise.timer.cycle = 0; self.dmc.timer.cycle = 0; } #[inline(always)] fn should_clock(&mut self) -> bool { // Clock every cycle while DMC is running to get accurate CPU stalling, sprite DMA // emulation, etc if self.dmc.should_clock() || self.should_clock { self.should_clock = false; return true; } let cycles = self.master_clock - self.clock; self.frame_counter.should_clock(cycles) || self.dmc.irq_pending_in(cycles) } fn channel_clock_to(&mut self, channel: Channel, cycle: u32) { fn clock_to(instance: &mut T, cycle: u32, offset: usize, outputs: &mut [f32]) where T: Clock + TimerCycle + Sample, { while instance.cycle() < cycle { instance.clock(); outputs[((instance.cycle() - 1) as usize * Apu::MAX_CHANNEL_COUNT) + offset] = instance.output(); } } let offset = channel as usize; let outputs = &mut self.channel_outputs; match channel { Channel::Pulse1 => clock_to(&mut self.pulse1, cycle, offset, outputs), Channel::Pulse2 => clock_to(&mut self.pulse2, cycle, offset, outputs), Channel::Triangle => clock_to(&mut self.triangle, cycle, offset, outputs), Channel::Noise => clock_to(&mut self.noise, cycle, offset, outputs), Channel::Dmc => clock_to(&mut self.dmc, cycle, offset, outputs), _ => (), } } fn clock_to(&mut self, cycle: u32) { self.master_clock = cycle; let cycles = self.master_clock - self.clock; trace!( "APU cycles to run: {} ({} - {}) - CYC:{}", cycles, self.master_clock, self.clock, self.cpu_cycle, ); while self.master_clock - self.clock > 0 { self.clock += self .frame_counter .clock_with(self.master_clock - self.clock, |ty| match ty { FrameType::Quarter => { trace!("APU Quarter Frame clock - CYC:{}", self.cpu_cycle); self.pulse1.clock_quarter_frame(); self.pulse2.clock_quarter_frame(); self.triangle.clock_quarter_frame(); self.noise.clock_quarter_frame(); } FrameType::Half => { trace!("APU Half Frame clock - CYC:{}", self.cpu_cycle); self.pulse1.clock_half_frame(); self.pulse2.clock_half_frame(); self.triangle.clock_half_frame(); self.noise.clock_half_frame(); } _ => (), }); self.pulse1.length.reload(); self.pulse2.length.reload(); self.triangle.length.reload(); self.noise.length.reload(); self.channel_clock_to(Channel::Pulse1, self.clock); self.channel_clock_to(Channel::Pulse2, self.clock); self.channel_clock_to(Channel::Triangle, self.clock); self.channel_clock_to(Channel::Noise, self.clock); self.channel_clock_to(Channel::Dmc, self.clock); } } /// $4000 Pulse1, $4004 Pulse2, and $400C Noise Control. pub fn write_ctrl(&mut self, channel: Channel, val: u8) { self.clock_to(self.master_clock); match channel { Channel::Pulse1 => { trace!("APU $4000 write: ${val:02X} - CYC:{}", self.cpu_cycle); self.pulse1.write_ctrl(val); } Channel::Pulse2 => { trace!("APU $4004 write: ${val:02X} - CYC:{}", self.cpu_cycle); self.pulse2.write_ctrl(val); } Channel::Noise => { trace!("APU $400C write: ${val:02X} - CYC:{}", self.cpu_cycle); self.noise.write_ctrl(val); } _ => panic!("{channel:?} does not have a control register"), } self.should_clock = true; } /// $4001 Pulse1 and $4005 Pulse2 Sweep. pub fn write_sweep(&mut self, channel: Channel, val: u8) { self.clock_to(self.master_clock); match channel { Channel::Pulse1 => { trace!("APU $4001 write: ${val:02X} - CYC:{}", self.cpu_cycle); self.pulse1.write_sweep(val); } Channel::Pulse2 => { trace!("APU $4005 write: ${val:02X} - CYC:{}", self.cpu_cycle); self.pulse2.write_sweep(val); } _ => panic!("{channel:?} does not have a sweep register"), } } /// $4002 Pulse1, $4006 Pulse2, $400A Triangle, $400E Noise, and $4010 DMC Timer Low Byte. pub fn write_timer_lo(&mut self, channel: Channel, val: u8) { self.clock_to(self.master_clock); match channel { Channel::Pulse1 => { trace!("APU $4002 write: ${val:02X} - CYC:{}", self.cpu_cycle); self.pulse1.write_timer_lo(val); } Channel::Pulse2 => { trace!("APU $4006 write: ${val:02X} - CYC:{}", self.cpu_cycle); self.pulse2.write_timer_lo(val); } Channel::Triangle => { trace!("APU $400A write: ${val:02X} - CYC:{}", self.cpu_cycle); self.triangle.write_timer_lo(val); } Channel::Noise => { trace!("APU $400E write: ${val:02X} - CYC:{}", self.cpu_cycle); self.noise.write_timer(val); } Channel::Dmc => { trace!("APU $4010 write: ${val:02X} - CYC:{}", self.cpu_cycle); self.dmc.write_timer(val); } _ => panic!("{channel:?} does not have a timer_lo register"), } } /// $4003 Pulse1, $4007 Pulse2, and $400B Triangle Timer High Byte. pub fn write_timer_hi(&mut self, channel: Channel, val: u8) { self.clock_to(self.master_clock); match channel { Channel::Pulse1 => { trace!("APU $4003 write: ${val:02X} - CYC:{}", self.cpu_cycle); self.pulse1.write_timer_hi(val); self.should_clock = self.pulse1.length.enabled; } Channel::Pulse2 => { trace!("APU $4007 write: ${val:02X} - CYC:{}", self.cpu_cycle); self.pulse2.write_timer_hi(val); self.should_clock = self.pulse2.length.enabled; } Channel::Triangle => { trace!("APU $400B write: ${val:02X} - CYC:{}", self.cpu_cycle); self.triangle.write_timer_hi(val); self.should_clock = self.triangle.length.enabled; } _ => panic!("{channel:?} does not have a timer_hi register"), } } /// $4008 Triangle Linear Counter. pub fn write_linear_counter(&mut self, val: u8) { self.clock_to(self.master_clock); trace!("APU $4008 write: ${val:02X} - CYC:{}", self.cpu_cycle); self.triangle.write_linear_counter(val); self.should_clock = true; } /// $400F Noise and $4013 DMC Length. pub fn write_length(&mut self, channel: Channel, val: u8) { self.clock_to(self.master_clock); trace!("APU $400F write: ${val:02X} - CYC:{}", self.cpu_cycle); match channel { Channel::Noise => { self.noise.write_length(val); self.should_clock = self.noise.length.enabled; } Channel::Dmc => self.dmc.write_length(val), _ => panic!("{channel:?} does not have a length register"), } } /// $4011 DMC Output Level. pub fn write_dmc_output(&mut self, val: u8) { self.clock_to(self.master_clock); trace!("APU $4011 write: ${val:02X} - CYC:{}", self.cpu_cycle); // Only 7-bits are used self.dmc.write_output(val & 0x7F); // $4011 applies new output right away, not on timer reload. let offset = Channel::Dmc as usize; self.channel_outputs[(self.dmc.timer.cycle as usize * Apu::MAX_CHANNEL_COUNT) + offset] = self.dmc.output(); } /// $4012 DMC Sample Addr. pub fn write_dmc_addr(&mut self, val: u8) { self.clock_to(self.master_clock); trace!("APU $4012 write: ${val:02X} - CYC:{}", self.cpu_cycle); self.dmc.write_addr(val); } /// Read APU Status. /// /// $4015 if-d nt21 DMC IRQ, frame IRQ, length counter statuses pub fn read_status(&mut self) -> u8 { self.clock_to(self.master_clock); let val = self.peek_status(); trace!("APU $4015 read: ${val:02X} - CYC:{}", self.cpu_cycle); if self.frame_counter.irq_pending { trace!("APU Frame Counter IRQ - CYC:{}", self.cpu_cycle); } self.frame_counter.irq_pending = false; val } /// Read APU Status without side-effects. /// /// Non-mutating version of `read_status`. pub fn peek_status(&self) -> u8 { let mut status = 0x00; if self.pulse1.length.counter > 0 { status |= 0x01; } if self.pulse2.length.counter > 0 { status |= 0x02; } if self.triangle.length.counter > 0 { status |= 0x04; } if self.noise.length.counter > 0 { status |= 0x08; } if self.dmc.bytes_remaining > 0 { trace!("dmc bytes remaining: {}", self.dmc.bytes_remaining); status |= 0x10; } if self.frame_counter.irq_pending { status |= 0x40; } if self.dmc.irq_pending { status |= 0x80; } status } /// Write APU Status. /// /// $4015 ---d nt21 length ctr enable: DMC, noise, triangle, pulse 2, 1 pub fn write_status(&mut self, val: u8) { self.clock_to(self.master_clock); trace!("APU $4015 write: ${val:02X} - CYC:{}", self.cpu_cycle); self.pulse1.set_enabled(val & 0x01 == 0x01); self.pulse2.set_enabled(val & 0x02 == 0x02); self.triangle.set_enabled(val & 0x04 == 0x04); self.noise.set_enabled(val & 0x08 == 0x08); self.dmc.set_enabled(val & 0x10 == 0x10, self.cpu_cycle); self.dmc.irq_pending = false; } /// $4017 APU Frame Counter. pub fn write_frame_counter(&mut self, val: u8) { self.clock_to(self.master_clock); trace!("APU $4017 write: ${val:02X} - CYC:{}", self.cpu_cycle); self.frame_counter.write(val, self.cpu_cycle); } // Return pending IRQ. #[inline(always)] pub const fn irq_pending(&self) -> bool { self.frame_counter.irq_pending | self.dmc.irq_pending } // Return pending DMA. #[inline(always)] pub const fn dma_pending(&self) -> bool { self.dmc.dma_pending } // Clear pending DMA. #[inline(always)] pub const fn clear_dma_pending(&mut self) { self.dmc.dma_pending = false; } } impl Regional for Apu { fn region(&self) -> NesRegion { self.region } fn set_region(&mut self, region: NesRegion) { if self.region != region { self.region = region; self.clock_rate = Cpu::region_clock_rate(region); self.filter_chain = FilterChain::new(region, self.sample_rate); self.sample_period = self.clock_rate / self.sample_rate; self.frame_counter.set_region(region); self.noise.set_region(region); self.dmc.set_region(region); } } } impl Reset for Apu { fn reset(&mut self, kind: ResetKind) { self.cpu_cycle = 0; self.master_clock = 0; self.clock = 0; self.should_clock = false; self.frame_counter.reset(kind); self.pulse1.reset(kind); self.pulse2.reset(kind); self.triangle.reset(kind); self.noise.reset(kind); self.dmc.reset(kind); } } impl std::fmt::Debug for Apu { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("Apu") .field("cpu_cycle", &self.cpu_cycle) .field("master_clock", &self.master_clock) .field("cycle", &self.clock) .field("frame_counter", &self.frame_counter) .field("pulse1", &self.pulse1) .field("pulse2", &self.pulse2) .field("triangle", &self.triangle) .field("noise", &self.noise) .field("dmc", &self.dmc) .field("filter_chain", &self.filter_chain) .field("audio_samples_len", &self.audio_samples.len()) .finish() } } /// [`Pulse`] channel lookup table. /// /// See: /// /// Original calculation: /// /// ```rust /// let mut pulse_table = [0.0; 31]; /// for (i, val) in pulse_table.iter_mut().enumerate().skip(1) { /// *val = 95.52 / (8_128.0 / (i as f32) + 100.0); /// } /// ``` #[rustfmt::skip] pub static PULSE_TABLE: [f32; 31] = [ 0.0, 0.011_609_139, 0.022_939_48, 0.034_000_948, 0.044_803, 0.055_354_66, 0.065_664_53, 0.075_740_82, 0.085_591_4, 0.095_223_75, 0.104_645_04, 0.113_862_15, 0.122_881_64, 0.131_709_8, 0.140_352_64, 0.148_815_96, 0.157_105_25, 0.165_225_88, 0.173_182_92, 0.180_981_26, 0.188_625_59, 0.196_120_46, 0.203_470_17, 0.210_678_94, 0.217_750_76, 0.224_689_5, 0.231_498_87, 0.238_182_47, 0.244_743_78, 0.251_186_07, 0.257_512_57, ]; /// [`Triangle`]/[`Noise`]/[`Dmc`] channels lookup table. /// /// See: /// /// Original calculation: /// /// ```rust /// let mut tnd_table = [0.0; 203]; /// for (i, val) in tnd_table.iter_mut().enumerate().skip(1) { /// *val = 163.67 / (24_329.0 / (i as f32) + 100.0); /// } /// ``` #[rustfmt::skip] pub static TND_TABLE: [f32; 203] = [ 0.0, 0.006_699_824, 0.013_345_02, 0.019_936_256, 0.026_474_18, 0.032_959_443, 0.039_392_676, 0.045_774_5, 0.052_105_535, 0.058_386_38, 0.064_617_634, 0.070_799_87, 0.076_933_69, 0.083_019_62, 0.089_058_26, 0.095_050_134, 0.100_995_794, 0.106_895_77, 0.112_750_58, 0.118_560_754, 0.124_326_79, 0.130_049_18, 0.135_728_45, 0.141_365_05, 0.146_959_5, 0.152_512_22, 0.158_023_7, 0.163_494_4, 0.168_924_76, 0.174_315_24, 0.179_666_28, 0.184_978_3, 0.190_251_74, 0.195_486_98, 0.200_684_47, 0.205_844_63, 0.210_967_81, 0.216_054_44, 0.221_104_92, 0.226_119_6, 0.231_098_88, 0.236_043_11, 0.240_952_72, 0.245_828_, 0.250_669_36, 0.255_477_1, 0.260_251_64, 0.264_993_28, 0.269_702_37, 0.274_379_22, 0.279_024_18, 0.283_637_58, 0.288_219_72, 0.292_770_95, 0.297_291_52, 0.301_781_8, 0.306_242_1, 0.310_672_67, 0.315_073_85, 0.319_445_88, 0.323_789_12, 0.328_103_78, 0.332_390_2, 0.336_648_6, 0.340_879_3, 0.345_082_55, 0.349_258_63, 0.353_407_77, 0.357_530_27, 0.361_626_36, 0.365_696_34, 0.369_740_37, 0.373_758_76, 0.377_751_74, 0.381_719_56, 0.385_662_44, 0.389_580_64, 0.393_474_37, 0.397_343_84, 0.401_189_3, 0.405_011_, 0.408_809_07, 0.412_583_83, 0.416_335_46, 0.420_064_15, 0.423_770_13, 0.427_453_6, 0.431_114_76, 0.434_753_84, 0.438_370_97, 0.441_966_44, 0.445_540_4, 0.449_093_, 0.452_624_53, 0.456_135_06, 0.459_624_9, 0.463_094_12, 0.466_542_93, 0.469_971_57, 0.473_380_15, 0.476_768_94, 0.480_137_94, 0.483_487_52, 0.486_817_7, 0.490_128_73, 0.493_420_7, 0.496_693_88, 0.499_948_32, 0.503_184_26, 0.506_401_84, 0.509_601_2, 0.512_782_45, 0.515_945_85, 0.519_091_4, 0.522_219_5, 0.525_330_07, 0.528_423_25, 0.531_499_3, 0.534_558_36, 0.537_600_5, 0.540_625_93, 0.543_634_8, 0.546_627_04, 0.549_603_04, 0.552_562_83, 0.555_506_47, 0.558_434_3, 0.561_346_23, 0.564_242_5, 0.567_123_23, 0.569_988_5, 0.572_838_4, 0.575_673_2, 0.578_492_94, 0.581_297_7, 0.584_087_6, 0.586_862_8, 0.589_623_45, 0.592_369_56, 0.595_101_36, 0.597_818_9, 0.600_522_3, 0.603_211_6, 0.605_887_, 0.608_548_64, 0.611_196_6, 0.613_830_8, 0.616_451_56, 0.619_059_, 0.621_653_14, 0.624_234_, 0.626_801_85, 0.629_356_7, 0.631_898_64, 0.634_427_7, 0.636_944_2, 0.639_448_05, 0.641_939_34, 0.644_418_24, 0.646_884_86, 0.649_339_2, 0.651_781_4, 0.654_211_5, 0.656_629_74, 0.659_036_04, 0.661_430_6, 0.663_813_4, 0.666_184_66, 0.668_544_35, 0.670_892_6, 0.673_229_46, 0.675_555_05, 0.677_869_44, 0.680_172_74, 0.682_464_96, 0.684_746_2, 0.687_016_6, 0.689_276_2, 0.691_525_04, 0.693_763_3, 0.695_990_9, 0.698_208_03, 0.700_414_8, 0.702_611_1, 0.704_797_2, 0.706_973_1, 0.709_138_8, 0.711_294_5, 0.713_440_1, 0.715_575_9, 0.717_701_8, 0.719_817_9, 0.721_924_25, 0.724_020_96, 0.726_108_, 0.728_185_65, 0.730_253_8, 0.732_312_56, 0.734_361_95, 0.736_402_1, 0.738_433_1, 0.740_454_9, 0.742_467_6, ]; ================================================ FILE: tetanes-core/src/bus.rs ================================================ //! NES Memory/Data Bus implementation. //! //! use crate::{ apu::{Apu, Channel}, cart::Cart, common::{Clock, NesRegion, Regional, Reset, ResetKind, Sample, Sram}, fs, genie::GenieCode, input::{Input, InputRegisters, Player}, mapper::{Map, Mapper}, mem::{ConstArray, RamState, Read, Write}, ppu::Ppu, }; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, path::Path}; /// NES Bus /// /// /// /// |-----------------| $FFFF |-----------------| /// | PRG-ROM | | | /// |-----------------| $8000 |-----------------| /// | PRG-RAM or SRAM | | PRG-RAM or SRAM | /// |-----------------| $6000 |-----------------| /// | Expansion | | Expansion | /// | Modules | | Modules | /// |-----------------| $4020 |-----------------| /// | APU/Input | | | /// | Registers | | | /// |- - - - - - - - -| $4000 | | /// | PPU Mirrors | | I/O Registers | /// | $2000-$2007 | | | /// |- - - - - - - - -| $2008 | | /// | PPU Registers | | | /// |-----------------| $2000 |-----------------| /// | WRAM Mirrors | | | /// | $0000-$07FF | | | /// |- - - - - - - - -| $0800 | | /// | WRAM | | 2K Internal | /// |- - - - - - - - -| $0200 | Work RAM | /// | Stack | | | /// |- - - - - - - - -| $0100 | | /// | Zero Page | | | /// |-----------------| $0000 |-----------------| #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] #[repr(C)] pub struct Bus { /// Picture Processing Unit. pub ppu: Ppu, /// Audio Processing Unit. pub apu: Apu, /// Joypad and Zapper inputs. pub input: Input, // 2K NES Work Ram available to the CPU. pub wram: Box>, /// Game GENIE codes. pub genie_codes: HashMap, /// Whatever was last read or written to to the Bus. pub open_bus: u8, /// RAM initialization state. #[serde(skip)] pub ram_state: RamState, /// NES Region. pub region: NesRegion, } impl Default for Bus { fn default() -> Self { Self::new(NesRegion::default(), RamState::default()) } } pub mod size { // 2K NES Work Ram available to the CPU. pub const WRAM: usize = 0x800; } impl Bus { pub fn new(region: NesRegion, ram_state: RamState) -> Self { Self { wram: Box::new(ConstArray::new()), ppu: Ppu::new(region), apu: Apu::new(region), input: Input::new(region), genie_codes: HashMap::new(), open_bus: 0x00, ram_state, region, } } pub fn load_cart(&mut self, cart: Cart) { self.ppu.load_mapper(cart.mapper); } pub fn unload_cart(&mut self) { self.ppu.load_mapper(Mapper::default()); } #[must_use] #[inline] #[allow(clippy::missing_const_for_fn)] // false positive on non-const deref coercion pub fn wram(&self) -> &[u8; size::WRAM] { &self.wram } /// Add a Game Genie code to override memory reads/writes. /// /// # Errors /// /// Errors if genie code is invalid. pub fn add_genie_code(&mut self, genie_code: GenieCode) { let addr = genie_code.addr(); self.genie_codes.insert(addr, genie_code); } /// Remove a Game Genie code. pub fn remove_genie_code(&mut self, code: &str) { self.genie_codes.retain(|_, gc| gc.code() != code); } /// Remove all Game Genie codes. pub fn clear_genie_codes(&mut self) { self.genie_codes.clear(); } fn genie_read(&self, addr: u16, val: u8) -> u8 { self.genie_codes .get(&addr) .map_or(val, |genie_code| genie_code.read(val)) } #[inline] #[must_use] pub fn audio_samples(&self) -> &[f32] { &self.apu.audio_samples } #[inline] pub fn clear_audio_samples(&mut self) { self.apu.audio_samples.clear(); } #[inline] pub fn cpu_clock(&mut self) { self.ppu.mapper.clock(); let output = self.ppu.mapper.output(); self.input.clock(); self.apu.add_mapper_output(output); self.apu.clock_lazy(); } } impl Read for Bus { fn read(&mut self, addr: u16) -> u8 { let addr = match addr { 0x0800..=0x1FFF => addr & 0x07FF, 0x2008..=0x3FFF => addr & 0x2007, _ => addr, }; self.open_bus = match addr { 0x0000..=0x07FF => self.wram[usize::from(addr)], 0x4100..=0xFFFF => { let val = self.ppu.mapper.prg_read(addr); self.genie_read(addr, val) } 0x2002 => self.ppu.read_status(), 0x2004 => self.ppu.read_oamdata(), 0x2007 => self.ppu.read_data(), 0x4015 => self.apu.read_status(), 0x4016 => self.input.read(Player::One, &self.ppu), 0x4017 => self.input.read(Player::Two, &self.ppu), 0x2000 | 0x2001 | 0x2003 | 0x2005 | 0x2006 => self.ppu.open_bus, _ => self.open_bus, }; self.open_bus } fn peek(&self, addr: u16) -> u8 { let addr = match addr { 0x0800..=0x1FFF => addr & 0x07FF, 0x2008..=0x3FFF => addr & 0x2007, _ => addr, }; match addr { 0x0000..=0x07FF => self.wram[usize::from(addr)], 0x4100..=0xFFFF => { let val = self.ppu.mapper.prg_peek(addr); self.genie_read(addr, val) } 0x2002 => self.ppu.peek_status(), 0x2004 => self.ppu.peek_oamdata(), 0x2007 => self.ppu.peek_data(), 0x4015 => self.apu.peek_status(), 0x4016 => self.input.peek(Player::One, &self.ppu), 0x4017 => self.input.peek(Player::Two, &self.ppu), 0x2000 | 0x2001 | 0x2003 | 0x2005 | 0x2006 => self.ppu.open_bus, _ => self.open_bus, } } } impl Write for Bus { fn write(&mut self, addr: u16, val: u8) { self.open_bus = val; let addr = match addr { 0x0800..=0x1FFF => addr & 0x07FF, 0x2008..=0x3FFF => addr & 0x2007, _ => addr, }; match addr { 0x0000..=0x07FF => self.wram[usize::from(addr)] = val, 0x4100..=0xFFFF => self.ppu.mapper.prg_write(addr, val), 0x2000 => self.ppu.write_ctrl(val), 0x2001 => self.ppu.write_mask(val), 0x2002 => self.ppu.open_bus = val, 0x2003 => self.ppu.write_oamaddr(val), 0x2004 => self.ppu.write_oamdata(val), 0x2005 => self.ppu.write_scroll(val), 0x2006 => self.ppu.write_addr(val), 0x2007 => self.ppu.write_data(val), 0x4000 => self.apu.write_ctrl(Channel::Pulse1, val), 0x4001 => self.apu.write_sweep(Channel::Pulse1, val), 0x4002 => self.apu.write_timer_lo(Channel::Pulse1, val), 0x4003 => self.apu.write_timer_hi(Channel::Pulse1, val), 0x4004 => self.apu.write_ctrl(Channel::Pulse2, val), 0x4005 => self.apu.write_sweep(Channel::Pulse2, val), 0x4006 => self.apu.write_timer_lo(Channel::Pulse2, val), 0x4007 => self.apu.write_timer_hi(Channel::Pulse2, val), 0x4008 => self.apu.write_linear_counter(val), 0x400A => self.apu.write_timer_lo(Channel::Triangle, val), 0x400B => self.apu.write_timer_hi(Channel::Triangle, val), 0x400C => self.apu.write_ctrl(Channel::Noise, val), 0x400E => self.apu.write_timer_lo(Channel::Noise, val), 0x400F => self.apu.write_length(Channel::Noise, val), 0x4010 => self.apu.write_timer_lo(Channel::Dmc, val), 0x4011 => self.apu.write_dmc_output(val), 0x4012 => self.apu.write_dmc_addr(val), 0x4013 => self.apu.write_length(Channel::Dmc, val), 0x4015 => self.apu.write_status(val), 0x4016 => self.input.write(val), 0x4017 => self.apu.write_frame_counter(val), 0x4014 => (), // DMA handled by CPU _ => (), } } } impl Regional for Bus { fn region(&self) -> NesRegion { self.region } fn set_region(&mut self, region: NesRegion) { self.region = region; self.ppu.set_region(region); self.apu.set_region(region); self.input.set_region(region); } } impl Reset for Bus { fn reset(&mut self, kind: ResetKind) { if kind == ResetKind::Hard { self.ram_state.fill(&mut **self.wram); } self.ppu.reset(kind); self.apu.reset(kind); } } impl Sram for Bus { fn save(&self, path: impl AsRef) -> fs::Result<()> { self.ppu.mapper.save(path) } fn load(&mut self, path: impl AsRef) -> fs::Result<()> { self.ppu.mapper.load(path) } } #[cfg(test)] mod test { use super::*; use crate::{ mapper::{Cnrom, Nrom}, mem::Memory, }; #[test] fn load_cart_values() { let mut bus = Bus::default(); #[rustfmt::skip] let rom: [u8; 16] = [ 0x4E, 0x45, 0x53, 0x1A, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; let cart = Cart::from_rom("load_cart_test", &mut rom.as_slice(), RamState::default()) .expect("valid cart"); let expected_mirroring = cart.mirroring(); let expected_region = cart.region(); bus.load_cart(cart); assert_eq!(bus.ppu.region(), expected_region, "ppu region"); assert_eq!(bus.apu.region(), expected_region, "apu region"); assert!( matches!(bus.ppu.mapper, Mapper::Nrom(_)), "mapper is Nrom: {:?}", bus.ppu.mapper ); assert_eq!(bus.ppu.mirroring(), expected_mirroring, "mirroring"); } #[test] fn load_cart_chr_rom() { let mut bus = Bus::default(); let mut cart = Cart::empty(); let mut chr_rom = Memory::new(0x2000); chr_rom.fill(0x66); // Cnrom doesn't provide CHR-RAM cart.mapper = Cnrom::load(&cart, chr_rom, Memory::new(0x4000)).unwrap(); bus.load_cart(cart); bus.write(0x2006, 0x00); bus.write(0x2006, 0x00); bus.read(0x2007); assert_eq!(bus.read(0x2007), 0x66, "chr_rom start"); bus.write(0x2006, 0x1F); bus.write(0x2006, 0xFF); bus.read(0x2007); assert_eq!(bus.read(0x2007), 0x66, "chr_rom end"); // Writes disallowed bus.write(0x2006, 0x00); bus.write(0x2006, 0x10); bus.write(0x2007, 0x77); bus.write(0x2006, 0x00); bus.write(0x2006, 0x10); bus.read(0x2007); assert_eq!(bus.read(0x2007), 0x66, "chr_rom read-only"); } #[test] fn load_cart_chr_ram() { let mut bus = Bus::default(); let mut cart = Cart::empty(); cart.mapper = Nrom::load(&cart, Memory::empty(), Memory::new(cart.prg_rom_size)).unwrap(); if let Mapper::Nrom(nrom) = &mut cart.mapper { nrom.chr.fill(0x66); } bus.load_cart(cart); bus.write(0x2006, 0x00); bus.write(0x2006, 0x00); bus.read(0x2007); assert_eq!(bus.read(0x2007), 0x66, "chr_ram start"); bus.write(0x2006, 0x1F); bus.write(0x2006, 0xFF); bus.read(0x2007); assert_eq!(bus.read(0x2007), 0x66, "chr_ram end"); // Writes allowed bus.write(0x2006, 0x10); bus.write(0x2006, 0x00); // PPU writes to $2006 are delayed by 2 PPU clocks bus.ppu.clock(); bus.ppu.clock(); bus.write(0x2007, 0x77); bus.write(0x2006, 0x10); bus.write(0x2006, 0x00); // PPU writes to $2006 are delayed by 2 PPU clocks bus.ppu.clock(); bus.ppu.clock(); bus.read(0x2007); assert_eq!(bus.read(0x2007), 0x77, "chr_ram write"); } #[test] fn genie_codes() { let mut bus = Bus::default(); let mut cart = Cart::empty(); let mut prg_rom = Memory::new(0x8000); let code = "YYKPOYZZ"; // The Legend of Zelda: New character with 8 Hearts let addr = 0x9F41; let orig_value = 0x22; // 3 Hearts let new_value = 0x77; // 8 Hearts prg_rom[(addr & 0x7FFF) as usize] = orig_value; cart.mapper = Nrom::load(&cart, Memory::new(cart.chr_rom_size), prg_rom).unwrap(); bus.load_cart(cart); bus.add_genie_code(GenieCode::new(code.to_string()).expect("valid genie code")); assert_eq!(bus.peek(addr), new_value, "peek code value"); assert_eq!(bus.read(addr), new_value, "read code value"); bus.remove_genie_code(code); assert_eq!(bus.peek(addr), orig_value, "peek orig value"); assert_eq!(bus.read(addr), orig_value, "read orig value"); } #[test] fn clock() { let mut bus = Bus::default(); bus.ppu.clock_to(12); assert_eq!(bus.ppu.master_clock, 12, "ppu clock"); bus.cpu_clock(); assert_eq!(bus.apu.master_clock, 1, "apu clock"); } #[test] fn read_write_ram() { let mut bus = Bus::default(); bus.write(0x0001, 0x66); assert_eq!(bus.peek(0x0001), 0x66, "peek ram"); assert_eq!(bus.read(0x0001), 0x66, "read ram"); assert_eq!(bus.read(0x0801), 0x66, "peek mirror 1"); assert_eq!(bus.read(0x0801), 0x66, "read mirror 1"); assert_eq!(bus.read(0x1001), 0x66, "peek mirror 2"); assert_eq!(bus.read(0x1001), 0x66, "read mirror 2"); assert_eq!(bus.read(0x1801), 0x66, "peek mirror 3"); assert_eq!(bus.read(0x1801), 0x66, "read mirror 3"); bus.write(0x0802, 0x77); assert_eq!(bus.read(0x0002), 0x77, "write mirror 1"); bus.write(0x1002, 0x88); assert_eq!(bus.read(0x0002), 0x88, "write mirror 2"); bus.write(0x1802, 0x99); assert_eq!(bus.read(0x0002), 0x99, "write mirror 3"); } #[test] #[ignore = "todo"] fn read_write_ppu() { // read: PPUSTATUS, OAMDATA, PPUDATA + Mirrors // peek: PPUSTATUS, OAMDATA, PPUDATA + Mirrors // write: PPUCTRL, PPUMASK, OAMADDR, OAMDATA, PPUSCROLL, PPUADDR, PPUDATA + Mirrors todo!() } #[test] #[ignore = "todo"] fn read_write_apu() { // read: APU_STATUS // write: APU_STATUS, APU_FRAME_COUNTER todo!() } #[test] #[ignore = "todo"] fn write_apu_pulse() { // write: APU_CTRL_PULSE1, APU_SWEEP_PULSE1, APU_TIMER_LO_PULSE1, APU_TIMER_HI_PULSE1 // write: APU_CTRL_PULSE2, APU_SWEEP_PULSE2, APU_TIMER_LO_PULSE2, APU_TIMER_HI_PULSE2 todo!(); } #[test] #[ignore = "todo"] fn write_apu_triangle() { // write: APU_LIN_CTR_TRIANGLE, APU_TIMER_LO_TRIANGLE, APU_TIMER_HI_TRIANGLE todo!(); } #[test] #[ignore = "todo"] fn write_apu_noise() { // write: APU_CTRL_NOISE, APU_TIMER_NOISE, APU_LENGTH_NOISE todo!() } #[test] #[ignore = "todo"] fn write_dmc() { // write: APU_TIMER_DMC, APU_OUTPUT_DMC, APU_ADDR_LOAD_DMC, APU_LENGTH_DMC todo!() } #[test] #[ignore = "todo"] fn read_write_input() { todo!() } #[test] #[ignore = "todo"] fn read_write_mapper() { todo!() } #[test] #[ignore = "todo"] fn reset() { todo!() } } ================================================ FILE: tetanes-core/src/cart.rs ================================================ //! NES cartridge implementation. use crate::{ common::{NesRegion, Regional}, fs, mapper::{ self, Axrom, BandaiFCG, Bf909x, Bnrom, Cnrom, ColorDreams, Exrom, Fxrom, Gxrom, JalecoSs88006, Mapper, Mmc1Revision, Namco163, Nina003006, Nrom, Pxrom, SunsoftFme7, Sxrom, Txrom, Uxrom, Vrc6, m024_m026_vrc6::Revision as Vrc6Revision, m034_nina001::Nina001, }, mem::{Memory, RamState}, ppu::Mirroring, }; use serde::{Deserialize, Serialize}; use std::{ fs::File, io::{BufReader, Read}, path::Path, }; use thiserror::Error; use tracing::{debug, error, info}; const PRG_ROM_BANK_SIZE: usize = 0x4000; const CHR_ROM_BANK_SIZE: usize = 0x2000; pub type Result = std::result::Result; #[derive(Error, Debug)] #[must_use] pub enum Error { #[error("invalid nes header (found: ${value:04X} at byte: {byte}). {message}")] InvalidHeader { byte: u8, value: u8, message: String, }, #[error("mapper: {0}")] InvalidMapper(#[from] mapper::Error), #[error("{context}: {source:?}")] Io { context: String, source: std::io::Error, }, } impl Error { pub fn io(source: std::io::Error, context: impl Into) -> Self { Self::Io { context: context.into(), source, } } } /// An NES cartridge. #[derive(Debug)] #[must_use] pub struct Cart { pub name: String, pub header: NesHeader, pub region: NesRegion, pub ram_state: RamState, pub mapper: Mapper, pub chr_rom: Memory>, // Character ROM pub prg_rom: Memory>, // Program ROM pub chr_rom_size: usize, pub chr_ram_size: usize, pub prg_rom_size: usize, pub prg_ram_size: usize, pub game_info: Option, } impl Default for Cart { fn default() -> Self { Self::empty() } } impl Cart { pub fn empty() -> Self { Self { name: "Empty Cart".to_string(), header: NesHeader::default(), region: NesRegion::default(), ram_state: RamState::default(), mapper: Mapper::none(), chr_rom: Memory::new(CHR_ROM_BANK_SIZE), prg_rom: Memory::new(PRG_ROM_BANK_SIZE), chr_rom_size: CHR_ROM_BANK_SIZE, chr_ram_size: 0, prg_rom_size: PRG_ROM_BANK_SIZE, prg_ram_size: 0, game_info: None, } } /// Load `Cart` from a ROM path. /// /// # Errors /// /// If the NES header is corrupted, the ROM file cannot be read, or the data does not match /// the header, then an error is returned. pub fn from_path>(path: P, ram_state: RamState) -> Result { let path = path.as_ref(); let mut rom = BufReader::new( File::open(path) .map_err(|err| Error::io(err, format!("failed to open rom {path:?}")))?, ); Self::from_rom(path.to_string_lossy(), &mut rom, ram_state) } /// Load `Cart` from ROM data. /// /// # Errors /// /// If the NES header is invalid, or the ROM data does not match the header, then an error is /// returned. pub fn from_rom(name: S, mut rom_data: &mut F, ram_state: RamState) -> Result where S: ToString, F: Read, { let name = name.to_string(); let mut header = NesHeader::load(&mut rom_data)?; debug!("{header:?}"); let prg_rom_size = (header.prg_rom_banks as usize) * PRG_ROM_BANK_SIZE; let mut prg_rom = Memory::new(prg_rom_size); rom_data.read_exact(&mut prg_rom).map_err(|err| { if let std::io::ErrorKind::UnexpectedEof = err.kind() { Error::InvalidHeader { byte: 4, value: header.prg_rom_banks as u8, message: format!( "expected `{}` prg-rom banks ({prg_rom_size} total bytes)", header.prg_rom_banks ), } } else { Error::io(err, "failed to read prg-rom") } })?; let prg_ram_size = Self::calculate_ram_size(header.prg_ram_shift)?; let chr_rom_size = (header.chr_rom_banks as usize) * CHR_ROM_BANK_SIZE; let mut chr_rom = Memory::new(chr_rom_size); if chr_rom_size > 0 { rom_data.read_exact(&mut chr_rom).map_err(|err| { if let std::io::ErrorKind::UnexpectedEof = err.kind() { Error::InvalidHeader { byte: 5, value: header.chr_rom_banks as u8, message: format!( "expected `{}` chr-rom banks ({prg_rom_size} total bytes)", header.chr_rom_banks ), } } else { Error::io(err, "failed to read chr-rom") } })?; } let chr_ram_size = if chr_rom_size > 0 { 0 } else { Self::calculate_ram_size(header.chr_ram_shift)? }; let game_info = Self::lookup_info(&prg_rom, &chr_rom); if let Some(game_info) = &game_info { header.mapper_num = game_info.mapper_num; } let region = if matches!(header.variant, NesVariant::INes | NesVariant::Nes2) { match header.tv_mode { 1 => NesRegion::Pal, 3 => NesRegion::Dendy, _ => game_info .as_ref() .map(|info| info.region) .unwrap_or_default(), } } else { game_info .as_ref() .map(|info| info.region) .unwrap_or_default() }; let mut cart = Self { name, header, region, ram_state, mapper: Mapper::none(), chr_rom: chr_rom.clone(), prg_rom: prg_rom.clone(), chr_rom_size, chr_ram_size, prg_rom_size, prg_ram_size, game_info, }; cart.mapper = match cart.header.mapper_num { 0 => Nrom::load(&cart, chr_rom, prg_rom)?, 1 => Sxrom::load(&cart, chr_rom, prg_rom, Mmc1Revision::BC)?, 2 => Uxrom::load(&cart, chr_rom, prg_rom)?, 3 => Cnrom::load(&cart, chr_rom, prg_rom)?, 4 | 76 | 88 | 95 | 154 | 206 => Txrom::load(&cart, chr_rom, prg_rom)?, 5 => Exrom::load(&cart, chr_rom, prg_rom)?, 7 => Axrom::load(&cart, chr_rom, prg_rom)?, 9 => Pxrom::load(&cart, chr_rom, prg_rom)?, 10 => Fxrom::load(&cart, chr_rom, prg_rom)?, 11 | 144 => ColorDreams::load(&cart, chr_rom, prg_rom)?, 16 | 153 | 157 | 159 => BandaiFCG::load(&cart, chr_rom, prg_rom)?, 18 => JalecoSs88006::load(&cart, chr_rom, prg_rom)?, 19 | 210 => Namco163::load(&cart, chr_rom, prg_rom)?, 24 => Vrc6::load(&cart, chr_rom, prg_rom, Vrc6Revision::A)?, 26 => Vrc6::load(&cart, chr_rom, prg_rom, Vrc6Revision::B)?, 34 => { // ≥ 16K implies NINA-001; ≤ 8K implies BNROM if chr_rom_size >= 0x4000 { Nina001::load(&cart, chr_rom, prg_rom)? } else { Bnrom::load(&cart, chr_rom, prg_rom)? } } 66 => Gxrom::load(&cart, chr_rom, prg_rom)?, 69 => SunsoftFme7::load(&cart, chr_rom, prg_rom)?, 71 => Bf909x::load(&cart, chr_rom, prg_rom)?, 79 | 113 | 146 => Nina003006::load(&cart, chr_rom, prg_rom)?, 155 => Sxrom::load(&cart, chr_rom, prg_rom, Mmc1Revision::A)?, _ => Mapper::none(), }; info!("loaded ROM `{cart}`"); debug!("{cart:?}"); Ok(cart) } #[must_use] #[allow(clippy::missing_const_for_fn)] // false positive on non-const deref coercion pub fn name(&self) -> &str { &self.name } #[must_use] pub const fn is_ines(&self) -> bool { matches!( self.header.variant, NesVariant::ArchaicINes | NesVariant::INes07 | NesVariant::INes ) } #[must_use] pub const fn is_nes2(&self) -> bool { matches!(self.header.variant, NesVariant::Nes2) } /// Returns whether this cartridge has battery-backed Save RAM. #[must_use] pub const fn battery_backed(&self) -> bool { self.header.flags & 0x02 == 0x02 } /// Returns `RamState`. pub const fn ram_state(&self) -> RamState { self.ram_state } /// Returns hardware configured `Mirroring`. pub fn mirroring(&self) -> Mirroring { if self.header.flags & 0x08 == 0x08 { Mirroring::FourScreen } else { match self.header.flags & 0x01 { 0 => Mirroring::Horizontal, 1 => Mirroring::Vertical, _ => unreachable!("impossible mirroring"), } } } /// Returns the Mapper number for this Cart. #[must_use] pub fn mapper_num(&self) -> u16 { self.game_info .as_ref() .map(|info| info.mapper_num) .unwrap_or(self.header.mapper_num) } /// Returns the Sub-Mapper number for this Cart. #[must_use] pub fn submapper_num(&self) -> u8 { self.game_info .as_ref() .map(|info| info.submapper_num) .unwrap_or(self.header.submapper_num) } /// Returns the Mapper and Board name for this Cart. #[must_use] pub fn mapper_board(&self) -> &'static str { NesHeader::mapper_board(self.mapper_num()) } pub fn chr_size(&self) -> usize { match &self.mapper { Mapper::None(_) => 0, Mapper::Nrom(nrom) => nrom.chr.len(), Mapper::Sxrom(sxrom) => sxrom.chr.len(), Mapper::Uxrom(uxrom) => uxrom.chr.len(), Mapper::Cnrom(cnrom) => cnrom.chr_rom.len(), Mapper::Txrom(txrom) => txrom.chr.len(), Mapper::Exrom(exrom) => exrom.chr_rom.len(), Mapper::Axrom(axrom) => axrom.chr.len(), Mapper::Pxrom(pxrom) => pxrom.chr_rom.len(), Mapper::Fxrom(fxrom) => fxrom.chr_rom.len(), Mapper::ColorDreams(color_dreams) => color_dreams.chr_rom.len(), Mapper::BandaiFCG(bandai_fcg) => bandai_fcg.chr.len(), Mapper::JalecoSs88006(jaleco_ss88006) => jaleco_ss88006.chr_rom.len(), Mapper::Namco163(namco163) => namco163.chr_rom.len(), Mapper::Vrc6(vrc6) => vrc6.chr_rom.len(), Mapper::Bnrom(bnrom) => bnrom.chr.len(), Mapper::Gxrom(gxrom) => gxrom.chr_rom.len(), Mapper::Nina001(nina001) => nina001.chr_rom.len(), Mapper::SunsoftFme7(sunsoft_fme7) => sunsoft_fme7.chr_rom.len(), Mapper::Bf909x(bf909x) => bf909x.chr.len(), Mapper::Nina003006(nina003006) => nina003006.chr_rom.len(), } } /// Returns CHR-RAM sized based on the Cart header, or defaults to given size. pub(crate) fn chr_rom_or_ram( &self, chr_rom: Memory>, size: usize, ) -> (Memory>, bool) { if chr_rom.is_empty() { ( Memory::with_ram_state( if self.chr_ram_size > 0 { self.chr_ram_size } else { size }, self.ram_state, ), true, ) } else { (chr_rom, false) } } /// Returns PRG-RAM sized based on the Cart header, or defaults to given size. pub(crate) fn prg_ram_or_default(&self, size: usize) -> Memory> { Memory::with_ram_state( if self.prg_ram_size > 0 { self.prg_ram_size } else { size }, self.ram_state, ) } fn calculate_ram_size(value: u8) -> Result { if value > 0 { 64usize .checked_shl(value.into()) .ok_or_else(|| Error::InvalidHeader { byte: 11, value, message: "header ram size larger than 64".to_string(), }) } else { Ok(0) } } fn lookup_info(prg_rom: &[u8], chr: &[u8]) -> Option { const GAME_DB: &[u8] = include_bytes!("../game_db.dat"); let Ok(games) = fs::load_bytes::>(GAME_DB) else { error!("failed to load `game_regions.dat`"); return None; }; let mut crc32 = fs::compute_crc32(prg_rom); if !chr.is_empty() { crc32 = fs::compute_combine_crc32(crc32, chr); } match games.binary_search_by(|game| game.crc32.cmp(&crc32)) { Ok(index) => { info!( "found game matching crc: {crc32:#010X}. info: {:?}", games[index] ); Some(games[index].clone()) } Err(_) => { info!("no game found matching crc: {crc32:#010X}"); None } } } } impl Regional for Cart { fn region(&self) -> NesRegion { self.region } fn set_region(&mut self, region: NesRegion) { self.region = region; } } impl std::fmt::Display for Cart { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { write!( f, "{} - {}, CHR-ROM: {}K, CHR-RAM: {}K, PRG-ROM: {}K, PRG-RAM: {}K, Mirroring: {:?}, Battery: {}", self.name, self.mapper_board(), self.chr_rom_size / 0x0400, self.chr_ram_size / 0x0400, self.prg_rom_size / 0x0400, self.prg_ram_size / 0x0400, self.mirroring(), self.battery_backed(), ) } } #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct GameInfo { pub crc32: u32, pub region: NesRegion, pub mapper_num: u16, pub submapper_num: u8, } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] #[must_use] pub enum NesVariant { #[default] ArchaicINes, INes07, INes, Nes2, } /// An `iNES` or `NES 2.0` formatted header representing hardware specs of a given NES cartridge. /// /// /// /// (page 28) #[derive(Default, Copy, Clone, PartialEq, Eq)] #[must_use] pub struct NesHeader { pub variant: NesVariant, pub mapper_num: u16, // The primary mapper number pub submapper_num: u8, // NES 2.0 https://wiki.nesdev.org/w/index.php/NES_2.0_submappers pub flags: u8, // Mirroring, Battery, Trainer, VS Unisystem, Playchoice-10, NES 2.0 pub prg_rom_banks: u16, // Number of 16KB PRG-ROM banks (Program ROM) pub chr_rom_banks: u16, // Number of 8KB CHR-ROM banks (Character ROM) pub prg_ram_shift: u8, // NES 2.0 PRG-RAM pub chr_ram_shift: u8, // NES 2.0 CHR-RAM pub tv_mode: u8, // NES 2.0 NTSC/PAL indicator pub vs_data: u8, // NES 2.0 VS System data } impl NesHeader { /// Load `NesHeader` from a ROM path. /// /// # Errors /// /// If the NES header is corrupted, the ROM file cannot be read, or the data does not match /// the header, then an error is returned. pub fn from_path>(path: P) -> Result { let path = path.as_ref(); let mut rom = BufReader::new( File::open(path) .map_err(|err| Error::io(err, format!("failed to open rom {path:?}")))?, ); Self::load(&mut rom) } /// Load `NesHeader` from ROM data. /// /// # Errors /// /// If the NES header is invalid, then an error is returned. pub fn load(rom_data: &mut F) -> Result { let mut header = [0u8; 16]; rom_data.read_exact(&mut header).map_err(|err| { if let std::io::ErrorKind::UnexpectedEof = err.kind() { Error::InvalidHeader { byte: 0, value: 0, message: "expected 16-byte header".to_string(), } } else { Error::io(err, "failed to read nes header") } })?; // Header checks if header[0..4] != *b"NES\x1a" { return Err(Error::InvalidHeader { byte: 0, value: header[0], message: "nes header signature not found".to_string(), }); } if (header[7] & 0x0C) == 0x04 { return Err(Error::InvalidHeader { byte: 7, value: header[7], message: "header is corrupted by `DiskDude!`. repair and try again".to_string(), }); } if (header[7] & 0x0C) == 0x0C { return Err(Error::InvalidHeader { byte: 7, value: header[7], message: "unrecognized header format. repair and try again".to_string(), }); } let mut prg_rom_banks = u16::from(header[4]); let mut chr_rom_banks = u16::from(header[5]); // Upper 4 bits of flags 6 = D0..D3 and 7 = D4..D7 let mut mapper_num = u16::from(((header[6] & 0xF0) >> 4) | (header[7] & 0xF0)); // Lower 4 bits of flag 6 = D0..D3, upper 4 bits of flag 7 = D4..D7 let flags = (header[6] & 0x0F) | ((header[7] & 0x0F) << 4); // NES 2.0 Format let mut submapper_num = 0; let mut prg_ram_shift = 0; let mut chr_ram_shift = 0; let mut tv_mode = 0; let mut vs_data = 0; // If D2..D3 of flag 7 == 2, then NES 2.0 (supports bytes 0-15) let variant = if header[7] & 0x0C == 0x08 { // lower 4 bits of flag 8 = D8..D11 of mapper num mapper_num |= u16::from(header[8] & 0x0F) << 8; // upper 4 bits of flag 8 = D0..D3 of submapper submapper_num = (header[8] & 0xF0) >> 4; // lower 4 bits of flag 9 = D8..D11 of prg_rom_size prg_rom_banks |= u16::from(header[9] & 0x0F) << 8; // upper 4 bits of flag 9 = D8..D11 of chr_rom_size chr_rom_banks |= u16::from(header[9] & 0xF0) << 4; prg_ram_shift = header[10]; chr_ram_shift = header[11]; tv_mode = header[12]; vs_data = header[13]; if prg_ram_shift & 0x0F == 0x0F || prg_ram_shift & 0xF0 == 0xF0 { return Err(Error::InvalidHeader { byte: 10, value: prg_ram_shift, message: "invalid prg-ram size in header".to_string(), }); } if chr_ram_shift & 0x0F == 0x0F || chr_ram_shift & 0xF0 == 0xF0 { return Err(Error::InvalidHeader { byte: 11, value: chr_ram_shift, message: "invalid chr-ram size in header".to_string(), }); } if chr_ram_shift & 0xF0 == 0xF0 { return Err(Error::InvalidHeader { byte: 11, value: chr_ram_shift, message: "battery-backed chr-ram is currently not supported".to_string(), }); } NesVariant::Nes2 } else if header[7] & 0x0C == 0x04 { // If D2..D3 of flag 7 == 1, then archaic iNES (supports bytes 0-7) for (i, value) in header.iter().enumerate().take(16).skip(8) { if *value > 0 { return Err(Error::InvalidHeader { byte: i as u8, value: *value, message: format!( "unrecogonized data found at header byte {i}. repair and try again" ), }); } } NesVariant::ArchaicINes } else if header[7] & 0x0C == 00 && header[12..=15].iter().all(|v| *v == 0) { // If D2..D3 of flag 7 == 0 and bytes 12-15 are all 0, then iNES (supports bytes 0-9) NesVariant::INes } else { // Else iNES 0.7 or archaic iNES (supports mapper high nibble) NesVariant::INes07 }; // Trainer if flags & 0x04 == 0x04 { return Err(Error::InvalidHeader { byte: 6, value: header[6], message: "trained roms are currently not supported.".to_string(), }); } Ok(Self { variant, mapper_num, submapper_num, flags, prg_rom_banks, chr_rom_banks, prg_ram_shift, chr_ram_shift, tv_mode, vs_data, }) } #[must_use] pub const fn mapper_board(mapper_num: u16) -> &'static str { match mapper_num { 0 => "Mapper 000 - NROM", 1 => "Mapper 001 - SxROM/MMC1B/C", 2 => "Mapper 002 - UxROM", 3 => "Mapper 003 - CNROM", 4 => "Mapper 004 - TxROM/MMC3/MMC6", 5 => "Mapper 005 - ExROM/MMC5", 6 => "Mapper 006 - FFE 1M/2M", 7 => "Mapper 007 - AxROM", 8 => "Mapper 008 - FFE 1M/2M", // Also Mapper 006 Submapper 4 9 => "Mapper 009 - PxROM/MMC2", 10 => "Mapper 010 - FxROM/MMC4", 11 => "Mapper 011 - Color Dreams", 12 => "Mapper 012 - Gouder/FFE 4M/MMC3", 13 => "Mapper 013 - CPROM", 14 => "Mapper 014 - UNL SL1632", 15 => "Mapper 015 - K1029/30", 16 => "Mapper 016 - Bandai FCG", 17 => "Mapper 017 - FFE", 18 => "Mapper 018 - Jaleco SS 88006", 19 => "Mapper 019 - Namco 129/163", 20 => "Mapper 020 - FDS", 21 => "Mapper 021 - Vrc4a/Vrc4c", 22 => "Mapper 022 - Vrc2a", 23 => "Mapper 023 - Vrc4e", 24 => "Mapper 024 - Vrc6a", 25 => "Mapper 025 - Vrc4b", 26 => "Mapper 026 - Vrc6b", 27 => "Mapper 027 - Vrc4x", 28 => "Mapper 028 - Action 53", 29 => "Mapper 029 - Sealie Computing", 30 => "Mapper 030 - UNROM 512", 31 => "Mapper 031 - NSF", 32 => "Mapper 032 - Irem G101", 33 => "Mapper 033 - Taito TC0190", 34 => "Mapper 034 - BNROM/NINA-001", 35 => "Mapper 035 - JY Company", 36 => "Mapper 036 - TXC 22000", 37 => "Mapper 037 - MMC3 Multicart", 38 => "Mapper 038 - UNL PCI556", 39 => "Mapper 039 - Subor", 40 => "Mapper 040 - NTDEC 2722", 41 => "Mapper 041 - Caltron 6-in-1", 42 => "Mapper 042", 43 => "Mapper 043 - TONY-I/YS-612", 44 => "Mapper 044 - MMC3 Multicart", 45 => "Mapper 045 - MMC3 Multicart", 46 => "Mapper 046 - Color Dreams", 47 => "Mapper 047 - MMC3 Multicart", 48 => "Mapper 048 - Taito TC0690", 49 => "Mapper 049 - MMC Multicart", 50 => "Mapper 050", 51 => "Mapper 051", 52 => "Mapper 052 - Realtec 8213/MMC Multicaart", 53 => "Mapper 053 - Supervision", 54 => "Mapper 054 - Novel Diamond", 55 => "Mapper 055 - UNIF BTL-MARIO1-MALEE2", 56 => "Mapper 056", 57 => "Mapper 057", 58 => "Mapper 058", 59 => "Mapper 059 - BMC T3H53/D1038", 60 => "Mapper 060", 61 => "Mapper 061", 62 => "Mapper 062", 63 => "Mapper 063", 64 => "Mapper 064 - RAMBO-1", 65 => "Mapper 065 - Irem H3001", 66 => "Mapper 066 - GxROM/MxROM", 67 => "Mapper 067 - Sunsoft-3", 68 => "Mapper 068 - Sunsoft-4", 69 => "Mapper 069 - Sunsoft FME-7", 70 => "Mapper 070 - Bandai", 71 => "Mapper 071 - BF909x", 72 => "Mapper 072 - Jaleco JF-17", 73 => "Mapper 073 - Vrc3", 74 => "Mapper 074", 75 => "Mapper 075 - Vrc1", 76 => "Mapper 076 - NAMCOT-108", 77 => "Mapper 077", 78 => "Mapper 078", 79 => "Mapper 079 - NINA-03/06", 80 => "Mapper 080 - Taito X1005", 81 => "Mapper 081 - NTDEC 715021", 82 => "Mapper 082 - Taito X1017", 83 => "Mapper 083", 84 => "Mapper 084", 85 => "Mapper 085 - Vrc7", 86 => "Mapper 086 - Jaleco JF-13", 87 => "Mapper 087 - Jaleco JF-xx", 88 => "Mapper 088", 89 => "Mapper 089 - Sunsoft", 90 => "Mapper 090 - JY Company", 91 => "Mapper 091", 92 => "Mapper 092", 93 => "Mapper 093 - Sunsoft", 94 => "Mapper 094 - UxROM", 95 => "Mapper 095 - NAMCOT-3425", 96 => "Mapper 096 - Oeka Kids", 97 => "Mapper 097 - Irem TAM-S1", 98 => "Mapper 098", 99 => "Mapper 099 - Vs. System", 100 => "Mapper 100", 101 => "Mapper 101 - Jaleco JF-10", 102 => "Mapper 102", 103 => "Mapper 103", 104 => "Mapper 104 - Golden Five", 105 => "Mapper 105 - MMC1", 106 => "Mapper 106", 107 => "Mapper 107", 108 => "Mapper 108", 109 => "Mapper 109", 110 => "Mapper 110", 111 => "Mapper 111 - GTROM", 112 => "Mapper 112", 113 => "Mapper 113 - NINA-03/06", 114 => "Mapper 114 - MMC3", 115 => "Mapper 115 - MMC3", 116 => "Mapper 116 - SOMARI-P", 117 => "Mapper 117", 118 => "Mapper 118 - TxSROM", 119 => "Mapper 119 - TQROM", 120 => "Mapper 120", 121 => "Mapper 121 - MMC3", 122 => "Mapper 122", 123 => "Mapper 123 - MMC3", 124 => "Mapper 124", 125 => "Mapper 125 - UNL-LH32", 126 => "Mapper 126 - MMC36", 127 => "Mapper 127", 128 => "Mapper 128", 129 => "Mapper 129", 130 => "Mapper 130", 131 => "Mapper 131", 132 => "Mapper 132 - TXC", 133 => "Mapper 133 - Sachen 3009", 134 => "Mapper 134 - MMC3", 135 => "Mapper 135 - Sachen 8259A", 136 => "Mapper 136 - Sachen 3011", 137 => "Mapper 137 - Sachen 8259D", 138 => "Mapper 138 - Sachen 8259B", 139 => "Mapper 139 - Sachen 8259C", 140 => "Mapper 140 - Jaleco JF-11/14", 141 => "Mapper 141 - Sachen 8259A", 142 => "Mapper 142 - Kaiser KS-7032", 143 => "Mapper 143 - NROM", 144 => "Mapper 144 - Color Dreams", 145 => "Mapper 145 - Sachen SA-72007", 146 => "Mapper 146 - NINA-03/06", 147 => "Mapper 147 - Sachen 3018", 148 => "Mapper 148 - Sachen SA-008-A/Tengen 800008", 149 => "Mapper 149 - Sachen SA-0036", 150 => "Mapper 150 - Sach SA-015/630", 151 => "Mapper 151 - Vrc1", 152 => "Mapper 152", 153 => "Mapper 153 - Bandai FCG", 154 => "Mapper 154 - NAMCOT-3453", 155 => "Mapper 155 - SxROM/MMC1A", 156 => "Mapper 156 - Daou", 157 => "Mapper 157 - Bandai FCG", 158 => "Mapper 158 - Tengen 800037", 159 => "Mapper 159 - Bandai FCG", 160 => "Mapper 160", 161 => "Mapper 161", 162 => "Mapper 162 - Wàixīng", 163 => "Mapper 163 - Nánjīng", 164 => "Mapper 164 - Dōngdá/Yànchéng", 165 => "Mapper 165 - MMC3", 166 => "Mapper 166 - Subor", 167 => "Mapper 167 - Subor", 168 => "Mapper 168 - Racermate", 169 => "Mapper 169 - Yuxing", 170 => "Mapper 170", 171 => "Mapper 171 - Kaiser KS-7058", 172 => "Mapper 172", 173 => "Mapper 173", 174 => "Mapper 174", 175 => "Mapper 175 - Kaiser KS-7022", 176 => "Mapper 176 - MMC3", 177 => "Mapper 177 - Hénggé Diànzǐ", 178 => "Mapper 178", 179 => "Mapper 179", 180 => "Mapper 180 - UNROM", 181 => "Mapper 181", 182 => "Mapper 182 - MMC3", 183 => "Mapper 183", 184 => "Mapper 184 - Sunsoft", 185 => "Mapper 185 - CNROM", 186 => "Mapper 186", 187 => "Mapper 187 - Kǎshèng/MMC3", 188 => "Mapper 188 - Bandai Karaoke", 189 => "Mapper 189 - MMC3", 190 => "Mapper 190 -", 191 => "Mapper 191 - MMC3", 192 => "Mapper 192 - Wàixīng", 193 => "Mapper 193 - NTDEC TC-112", 194 => "Mapper 194 - MMC3", 195 => "Mapper 195 - Wàixīng/MMC3", 196 => "Mapper 196 - MMC3", 197 => "Mapper 197 - MMC3", 198 => "Mapper 198 - MMC3", 199 => "Mapper 199 - Wàixīng/MMC3", 200 => "Mapper 200", 201 => "Mapper 201 - NROM", 202 => "Mapper 202", 203 => "Mapper 203", 204 => "Mapper 204", 205 => "Mapper 205 - MMC3", 206 => "Mapper 206 - DxROM", 207 => "Mapper 207 - Taito X1-005", 208 => "Mapper 208 - MMC3", 209 => "Mapper 209 - JY Company", 210 => "Mapper 210 - Namco", 211 => "Mapper 211 - JyCompany", 212 => "Mapper 212", 213 => "Mapper 213", 214 => "Mapper 214", 215 => "Mapper 215 - MMC3", 216 => "Mapper 216", 217 => "Mapper 217 - MMC3", 218 => "Mapper 218", 219 => "Mapper 219 - Kǎshèng/MMC3", 220 => "Mapper 220", 221 => "Mapper 221 - NTDEC N625092", 222 => "Mapper 222", 223 => "Mapper 223", 224 => "Mapper 224 - Jncota/MMC3", 225 => "Mapper 225", 226 => "Mapper 226", 227 => "Mapper 227", 228 => "Mapper 228- Active Enterprises", 229 => "Mapper 229", 230 => "Mapper 230", 231 => "Mapper 231", 232 => "Mapper 232 - BF909x", 233 => "Mapper 233", 234 => "Mapper 234 - Maxi 15 Multicart", 235 => "Mapper 235", 236 => "Mapper 236 - Realtec", 237 => "Mapper 237", 238 => "Mapper 238 - MMC3", 239 => "Mapper 239", 240 => "Mapper 240", 241 => "Mapper 241 - BxROM", 242 => "Mapper 242", 243 => "Mapper 243 - Sachen SA-020A", 244 => "Mapper 244", 245 => "Mapper 245 - Wàixīng/MMC3", 246 => "Mapper 246", 247 => "Mapper 247", 248 => "Mapper 248", 249 => "Mapper 249 - MMC3", 250 => "Mapper 250 - Nitra/MMC3", 251 => "Mapper 251", 252 => "Mapper 252 - Wàixīng", 253 => "Mapper 253 - Wàixīng", 254 => "Mapper 254 - MMC3", 255 => "Mapper 255", _ => "Invalid Mapper", } } } impl std::fmt::Debug for NesHeader { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("NesHeader") .field("version", &self.variant) .field("mapper_num", &format_args!("{:03}", &self.mapper_num)) .field("submapper_num", &self.submapper_num) .field("flags", &format_args!("0b{:08b}", &self.flags)) .field("prg_rom_banks", &self.prg_rom_banks) .field("chr_rom_banks", &self.chr_rom_banks) .field("prg_ram_shift", &self.prg_ram_shift) .field("chr_ram_shift", &self.chr_ram_shift) .field("tv_mode", &self.tv_mode) .field("vs_data", &self.vs_data) .finish() } } #[cfg(test)] mod tests { use super::*; macro_rules! test_headers { ($(($test:ident, $data:expr, $header:expr$(,)?)),*$(,)?) => {$( #[test] fn $test() { let header = NesHeader::load(&mut $data.as_slice()).expect("valid header"); assert_eq!(header, $header); } )*}; } #[rustfmt::skip] test_headers!( ( mapper000_horizontal, [0x4E, 0x45, 0x53, 0x1A, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], NesHeader { variant: NesVariant::INes, mapper_num: 0, flags: 0b0000_0001, prg_rom_banks: 2, chr_rom_banks: 1, ..NesHeader::default() }, ), ( mapper001_vertical, [0x4E, 0x45, 0x53, 0x1A, 0x08, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], NesHeader { variant: NesVariant::INes, mapper_num: 1, flags: 0b0000_0000, prg_rom_banks: 8, chr_rom_banks: 0, ..NesHeader::default() }, ), ); } ================================================ FILE: tetanes-core/src/common.rs ================================================ //! Common traits and constants. use serde::{Deserialize, Serialize}; use std::{fmt::Write, path::Path}; use thiserror::Error; /// Default directory for save states. pub const SAVE_DIR: &str = "save"; /// Default directory for save RAM. pub const SRAM_DIR: &str = "sram"; #[derive(Error, Debug)] #[must_use] #[error("failed to parse `NesRegion`")] pub struct ParseNesRegionError; #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[must_use] pub enum NesRegion { /// Auto-detect region based on ROM headers and a pre-built game database. Auto, /// NTSC, primarily North America. #[default] Ntsc, /// PAL, primarily Japan and Europe. Pal, /// Dendy, primarily Russia. Dendy, } impl NesRegion { pub const fn as_slice() -> &'static [Self] { &[ NesRegion::Auto, NesRegion::Ntsc, NesRegion::Pal, NesRegion::Dendy, ] } #[must_use] pub const fn is_auto(&self) -> bool { matches!(self, Self::Auto) } #[must_use] pub const fn is_ntsc(&self) -> bool { matches!(self, Self::Auto | Self::Ntsc) } #[must_use] pub const fn is_pal(&self) -> bool { matches!(self, Self::Pal) } #[must_use] pub const fn is_dendy(&self) -> bool { matches!(self, Self::Dendy) } #[must_use] pub fn aspect_ratio(&self) -> f32 { // https://www.nesdev.org/wiki/Overscan match self { Self::Auto | Self::Ntsc => 8.0 / 7.0, Self::Pal | Self::Dendy => 18.0 / 13.0, } } #[must_use] pub const fn as_str(&self) -> &'static str { match self { Self::Auto => "auto", Self::Ntsc => "ntsc", Self::Pal => "pal", Self::Dendy => "dendy", } } } impl std::fmt::Display for NesRegion { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = match self { Self::Auto => "Auto", Self::Ntsc => "NTSC", Self::Pal => "PAL", Self::Dendy => "Dendy", }; write!(f, "{s}") } } impl AsRef for NesRegion { fn as_ref(&self) -> &str { self.as_str() } } impl TryFrom<&str> for NesRegion { type Error = ParseNesRegionError; fn try_from(value: &str) -> Result { match value { "auto" => Ok(Self::Auto), "ntsc" => Ok(Self::Ntsc), "pal" => Ok(Self::Pal), "dendy" => Ok(Self::Dendy), _ => Err(ParseNesRegionError), } } } impl TryFrom for NesRegion { type Error = ParseNesRegionError; fn try_from(value: usize) -> Result { match value { 0 => Ok(Self::Auto), 1 => Ok(Self::Ntsc), 2 => Ok(Self::Pal), 3 => Ok(Self::Dendy), _ => Err(ParseNesRegionError), } } } /// Trait for types that have different behavior depending on NES region. // NOTE: enum_dispatch requires absolute paths to types pub trait Regional { /// Return the current region. fn region(&self) -> NesRegion { NesRegion::default() } /// Set the region. fn set_region(&mut self, _region: NesRegion) {} } /// Type of reset for types that have different behavior for reset vs power cycling. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[must_use] pub enum ResetKind { /// Soft reset generally doesn't zero-out most registers or RAM. Soft, /// Hard reset generally zeros-out most registers and RAM. Hard, } /// Trait for types that can can be reset. pub trait Reset { /// Reset the component given the [`ResetKind`]. fn reset(&mut self, _kind: ResetKind) {} } /// Trait for types that can be clocked. pub trait Clock { /// Clock component once. fn clock(&mut self) {} } /// Trait for types that can output `f32` audio samples. pub trait Sample { /// Output a single audio sample. fn output(&self) -> f32 { 0.0 } } /// Trait for types that can save RAM to disk. pub trait Sram { /// Save RAM to a given path. fn save(&self, _path: impl AsRef) -> crate::fs::Result<()> { Ok(()) } /// Load save RAM from a given path. fn load(&mut self, _path: impl AsRef) -> crate::fs::Result<()> { Ok(()) } } /// Prints a hex dump of a given byte array starting at `addr_offset`. #[must_use] pub fn hexdump(data: &[u8], addr_offset: usize) -> Vec { use std::cmp; let mut addr = 0; let len = data.len(); let mut last_line_same = false; let mut output = Vec::new(); let mut last_line = String::with_capacity(80); while addr <= len { let end = cmp::min(addr + 16, len); let line_data = &data[addr..end]; let line_len = line_data.len(); let mut line = String::with_capacity(80); for byte in line_data.iter() { let _ = write!(line, " {byte:02X}"); } if line_len % 16 > 0 { let words_left = (16 - line_len) / 2; for _ in 0..3 * words_left { line.push(' '); } } if line_len > 0 { line.push_str(" |"); for c in line_data { if (*c as char).is_ascii() && !(*c as char).is_control() { let _ = write!(line, "{}", (*c as char)); } else { line.push('.'); } } line.push('|'); } if last_line == line { if !last_line_same { last_line_same = true; output.push("*".to_string()); } } else { last_line_same = false; output.push(format!("{:08x} {}", addr + addr_offset, line)); } last_line = line; addr += 16; } output } #[cfg(test)] pub(crate) mod tests { use crate::{ action::Action, common::{Regional, Reset, ResetKind}, control_deck::{Config, ControlDeck}, input::Player, mem::RamState, ppu::size, video::VideoFilter, }; use anyhow::Context; use image::{ImageBuffer, Rgba}; use serde::{Deserialize, Serialize}; use std::{ collections::hash_map::DefaultHasher, env, fmt::Write, fs::{self, File}, hash::{Hash, Hasher}, io::{BufReader, Read}, path::{Path, PathBuf}, sync::OnceLock, }; use tracing::debug; pub(crate) const RESULT_DIR: &str = "test_results"; static PASS_DIR: OnceLock = OnceLock::new(); static FAIL_DIR: OnceLock = OnceLock::new(); #[macro_export] macro_rules! test_roms { ($mod:ident, $directory:expr, $( $(#[ignore = $reason:expr])? $test:ident ),* $(,)?) => { mod $mod {$( $(#[ignore = $reason])? #[test] fn $test() -> anyhow::Result<()> { $crate::common::tests::test_rom($directory, stringify!($test)) } )*} }; } // TODO: Instead of a bunch of optional fields, it should be an enum: // enum FrameAction { // DeckAction(DeckAction), // FrameHash(u64), // AudioHash(u64), // } #[derive(Default, Debug, Clone, Serialize, Deserialize)] #[serde(default)] #[must_use] struct TestFrame { number: u32, #[serde(skip_serializing_if = "Option::is_none")] name: Option, #[serde(skip_serializing_if = "Option::is_none")] hash: Option, #[serde(skip_serializing_if = "Option::is_none")] action: Option, #[serde(skip_serializing)] audio: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] struct RomTest { name: String, #[serde(skip_serializing, default)] audio: bool, frames: Vec, } fn get_rom_tests(directory: &str) -> anyhow::Result<(PathBuf, Vec)> { let file = PathBuf::from(directory) .join("tests") .with_extension("json"); let mut content = String::with_capacity(1024); File::open(&file) .and_then(|mut file| file.read_to_string(&mut content)) .with_context(|| format!("failed to read rom test data: {file:?}"))?; let tests = serde_json::from_str(&content) .with_context(|| format!("valid rom test data: {file:?}"))?; Ok((file, tests)) } fn load_control_deck>(path: P) -> ControlDeck { let path = path.as_ref(); let mut rom = BufReader::new(File::open(path).expect("failed to open path")); let mut deck = ControlDeck::with_config(Config { ram_state: RamState::AllZeros, filter: VideoFilter::Pixellate, ..Default::default() }); deck.load_rom(path.to_string_lossy(), &mut rom) .expect("failed to load rom"); deck } fn on_frame_action(test_frame: &TestFrame, deck: &mut ControlDeck) { if let Some(action) = test_frame.action { debug!("{:?}", action); match action { Action::Reset(kind) => deck.reset(kind), Action::MapperRevision(rev) => deck.set_mapper_revision(rev), Action::SetVideoFilter(filter) => deck.set_filter(filter), Action::SetNesRegion(format) => deck.set_region(format), Action::Joypad((player, button)) => { let joypad = deck.joypad_mut(player); joypad.set_button(button, true); } Action::ToggleZapperConnected => deck.connect_zapper(!deck.zapper_connected()), Action::ZapperAim((x, y)) => deck.aim_zapper(x, y), Action::ZapperTrigger => deck.trigger_zapper(), Action::LoadState | Action::SaveState | Action::SetSaveSlot(_) | Action::ToggleApuChannel(_) | Action::ZapperAimOffscreen | Action::FourPlayer(_) => (), } } } fn on_snapshot( test: &str, test_frame: &TestFrame, deck: &mut ControlDeck, count: usize, ) -> anyhow::Result> { match test_frame.hash { Some(expected) => { let mut hasher = DefaultHasher::new(); if test_frame.audio { deck.audio_samples() .iter() .for_each(|s| s.to_le_bytes().hash(&mut hasher)); } else { deck.frame_buffer().hash(&mut hasher); } let actual = hasher.finish(); debug!( "frame: {}, matched: {}", test_frame.number, expected == actual ); let base_dir = Path::new(env!("CARGO_MANIFEST_DIR")); let result_dir = if env::var("UPDATE_SNAPSHOT").is_ok() || expected == actual { PASS_DIR.get_or_init(|| { let directory = base_dir.join(PathBuf::from(RESULT_DIR)).join("pass"); if let Err(err) = fs::create_dir_all(&directory) { panic!("created pass test results dir: {directory:?}. {err}",); } directory }) } else { FAIL_DIR.get_or_init(|| { let directory = base_dir.join(PathBuf::from(RESULT_DIR)).join("fail"); if let Err(err) = fs::create_dir_all(&directory) { panic!("created fail test results dir: {directory:?}. {err}",); } directory }) }; let mut filename = test.to_owned(); if let Some(ref name) = test_frame.name { let _ = write!(filename, "_{name}"); } else if count > 0 { let _ = write!(filename, "_{}", count + 1); } let screenshot = result_dir .join(PathBuf::from(filename)) .with_extension("png"); ImageBuffer::, &[u8]>::from_raw( u32::from(size::WIDTH), u32::from(size::HEIGHT), deck.frame_buffer(), ) .expect("valid frame") .save(&screenshot) .with_context(|| format!("failed to save screenshot: {screenshot:?}"))?; Ok(Some((expected, actual, test_frame.number, screenshot))) } None => Ok(None), } } pub(crate) fn test_rom(directory: &str, test_name: &str) -> anyhow::Result<()> { thread_local! { static INIT_TESTS: OnceLock = const { OnceLock::new() }; } let base_dir = Path::new(env!("CARGO_MANIFEST_DIR")); let initialized = INIT_TESTS.with(|init| { *init.get_or_init(|| { use tracing_subscriber::{ filter::Targets, fmt, layer::SubscriberExt, registry, util::SubscriberInitExt, }; let _ = registry() .with( env::var("RUST_LOG") .ok() .and_then(|filter| filter.parse::().ok()) .unwrap_or_default(), ) .with( fmt::layer() .compact() .with_ansi(false) .without_time() .with_line_number(true) .with_thread_ids(true) .with_thread_names(true) .with_writer(std::io::stderr), ) .try_init(); true }) }); if initialized { debug!("Initialized tests"); } let (test_file, mut tests) = get_rom_tests(directory)?; let mut test = tests.iter_mut().find(|test| test.name.eq(test_name)); assert!(test.is_some(), "No test found matching {test_name:?}"); let test = test.as_mut().expect("definitely has a test"); let rom = base_dir .join(directory) .join(PathBuf::from(&test.name)) .with_extension("nes"); assert!(rom.exists(), "No test rom found for {rom:?}"); let mut deck = load_control_deck(&rom); deck.cpu_mut().bus.apu.skip_mixing = !test.audio; let mut results = Vec::new(); assert!(!test.frames.is_empty(), "No test frames found for {rom:?}"); for test_frame in test.frames.iter() { debug!("{} - {:?}", test_frame.number, deck.joypad_mut(Player::One)); while deck.frame_number() < test_frame.number { deck.clock_frame().expect("valid frame clock"); if deck.frame_number() != test_frame.number && !test_frame.audio { deck.clear_audio_samples(); } deck.joypad_mut(Player::One).reset(ResetKind::Soft); deck.joypad_mut(Player::Two).reset(ResetKind::Soft); } on_frame_action(test_frame, &mut deck); if let Ok(Some(result)) = on_snapshot(&test.name, test_frame, &mut deck, results.len()) { results.push(result); } } let mut update_required = false; for (mut expected, actual, frame_number, screenshot) in results { if env::var("UPDATE_SNAPSHOT").is_ok() && expected != actual { expected = actual; update_required = true; if let Some(frame) = &mut test .frames .iter_mut() .find(|frame| frame.number == frame_number) { frame.hash = Some(actual); } } assert!( expected == actual, "mismatched snapshot for {rom:?} -> {screenshot:?} (expected: {expected}, actual: {actual})", ); } if update_required { File::create(&test_file) .context("failed to open rom test file") .and_then(|file| { serde_json::to_writer_pretty(file, &tests) .context("failed to serialize rom data") }) .with_context(|| format!("failed to update snapshot: {test_file:?}"))? } Ok(()) } test_roms!( cpu, "test_roms/cpu", branch_backward, // Tests branches jumping backward branch_basics, // Tests branch instructions, including edge cases branch_forward, // Tests branches jumping forward nestest, // Tests all CPU instructions, including illegal opcodes // Verifies ram and registers are set/cleared correctly after reset ram_after_reset, regs_after_reset, // Tests CPU dummy reads dummy_reads, dummy_writes_oam, dummy_writes_ppumem, // Verifies cpu can execute code from any memory location, incl. I/O exec_space_apu, exec_space_ppuio, flag_concurrency, // Tests CPU several instruction combinations instr_abs, instr_abs_xy, instr_basics, instr_branches, instr_brk, instr_imm, instr_imp, instr_ind_x, instr_ind_y, instr_jmp_jsr, instr_misc, instr_rti, instr_rts, instr_special, instr_stack, instr_timing, instr_zp, instr_zp_xy, // Tests IRQ/NMI timings int_branch_delays_irq, int_cli_latency, int_irq_and_dma, int_nmi_and_brk, int_nmi_and_irq, overclock, // Tests cycle stealing behavior of DMC DMA while running sprite DMAs sprdma_and_dmc_dma, sprdma_and_dmc_dma_512, timing_test, // Tests CPU timing ); test_roms!( ppu, "test_roms/ppu", _240pee, // TODO: Run each test color, // TODO: Test all color combinations ntsc_torture, // Tests PPU NTSC signal artifacts oam_read, // Tests OAM reading ($2004) oam_stress, // Stresses OAM ($2003) reads and writes ($2004) open_bus, // Tests PPU open bus behavior palette, // Tests simple scanline palette changes palette_ram, // Tests palette RAM access read_buffer, // Thoroughly tests PPU read buffer ($2007) scanline, // Tests scanline rendering spr_hit_alignment, // Tests sprite hit alignment spr_hit_basics, // Tests sprite hit basics spr_hit_corners, // Tests sprite hit corners spr_hit_double_height, // Tests sprite hit in x16 height mode spr_hit_edge_timing, // Tests sprite hit edge timing spr_hit_flip, // Tests sprite hit with sprite flip spr_hit_left_clip, // Tests sprite hit with left edge clipped spr_hit_right_edge, // Tests sprite hit right edge spr_hit_screen_bottom, // Tests sprite hit bottom spr_hit_timing_basics, // Tests sprite hit timing spr_hit_timing_order, // Tests sprite hit order spr_overflow_basics, // Tests sprite overflow basics spr_overflow_details, // Tests more thorough sprite overflow spr_overflow_emulator, spr_overflow_obscure, // Tests obscure sprite overflow cases spr_overflow_timing, // Tests sprite overflow timing sprite_ram, // Tests sprite ram tv, // Tests NTSC color and NTSC/PAL aspect ratio vbl_nmi_basics, // Tests vblank NMI basics vbl_nmi_clear_timing, // Tests vblank NMI clear timing vbl_nmi_control, // Tests vblank NMI control vbl_nmi_disable, // Tests vblank NMI disable vbl_nmi_even_odd_frames, // Tests vblank NMI on even/odd frames #[ignore = "clock is skipped too late relative to enabling BG Failed #3"] vbl_nmi_even_odd_timing, // Tests vblank NMI even/odd frame timing vbl_nmi_frame_basics, // Tests vblank NMI frame basics vbl_nmi_off_timing, // Tests vblank NMI off timing vbl_nmi_on_timing, // Tests vblank NMI on timing vbl_nmi_set_time, // Tests vblank NMI set timing vbl_nmi_suppression, // Tests vblank NMI supression vbl_nmi_timing, // Tests vblank NMI timing vbl_timing, // Tests vblank timing vram_access, // Tests video RAM access ); test_roms!( apu, "test_roms/apu", // DMC DMA during $2007 read causes 2-3 extra $2007 // reads before real read. // // Number of extra reads depends in CPU-PPU // synchronization at reset. dmc_dma_2007_read, // DMC DMA during $2007 write has no effect. // Output: // 22 11 22 AA 44 55 66 77 // 22 11 22 AA 44 55 66 77 // 22 11 22 AA 44 55 66 77 // 22 11 22 AA 44 55 66 77 // 22 11 22 AA 44 55 66 77 dmc_dma_2007_write, // DMC DMA during $4016 read causes extra $4016 // read. // Output: // 08 08 07 08 08 dmc_dma_4016_read, // Double read of $2007 sometimes ignores extra // read, and puts odd things into buffer. // // Output (depends on CPU-PPU synchronization): // 22 33 44 55 66 // 22 44 55 66 77 or // 22 33 44 55 66 or // 02 44 55 66 77 or // 32 44 55 66 77 or // 85CFD627 or F018C287 or 440EF923 or E52F41A5 dmc_dma_double_2007_read, // Read of $2007 just before write behaves normally. // // Output: // 33 11 22 33 09 55 66 77 // 33 11 22 33 09 55 66 77 dmc_dma_read_write_2007, // This NES program demonstrates abusing the NTSC NES's sampled sound // playback hardware as a scanline timer to split the screen twice // without needing to use a mapper-generated IRQ. dpcmletterbox, // Blargg's APU tests // // Misc // ---- // - The frame IRQ flag is cleared only when $4015 is read or $4017 is // written with bit 6 set ($40 or $c0). // - The IRQ handler is invoked at minimum 29833 clocks after writing $00 // to $4017 (assuming the frame IRQ flag isn't already set, and nothing // else generates an IRQ during that time). // - After reset or power-up, APU acts as if $4017 were written with $00 // from 9 to 12 clocks before first instruction begins. It is as if this // occurs (this generates a 10 clock delay): // lda #$00 // sta $4017 ; 1 // lda <0 ; 9 delay // nop // nop // nop // reset: // ... // - As shown, the frame irq flag is set three times in a row. Thus when // polling it, always read $4015 an extra time after the flag is found to // be set, to be sure it's clear afterwards, // wait: bit $4015 ; V flag reflects frame IRQ flag // bvc wait // bit $4015 ; be sure irq flag is clear // or better yet, clear it before polling it: // bit $4015 ; clear flag first // wait: bit $4015 ; V flag reflects frame IRQ flag // bvc wait // // See: // // // Tests basic length counter operation // 1) Passed tests // 2) Problem with length counter load or $4015 // 3) Problem with length table, timing, or $4015 // 4) Writing $80 to $4017 should clock length immediately // 5) Writing $00 to $4017 shouldn't clock length immediately // 6) Clearing enable bit in $4015 should clear length counter // 7) When disabled via $4015, length shouldn't allow reloading // 8) Halt bit should suspend length clocking len_ctr, // Tests all length table entries. // 1) Passed // 2) Failed. Prints four bytes $II $ee $cc $02 that indicate the length // load value written (ll), the value that the emulator uses ($ee), and the // correct value ($cc). len_table, // Tests basic operation of frame irq flag. // 1) Tests passed // 2) Flag shouldn't be set in $4017 mode $40 // 3) Flag shouldn't be set in $4017 mode $80 // 4) Flag should be set in $4017 mode $00 // 5) Reading flag clears it // 6) Writing $00 or $80 to $4017 doesn't affect flag // 7) Writing $40 or $c0 to $4017 clears flag irq_flag, // Clock Jitter // ------------ // Changes to the mode by writing to $4017 only occur on *even* internal // APU clocks; if written on an odd clock, the first step of the mode is // delayed by one clock. At power-up and reset, the APU is randomly in an // odd or even cycle with respect to the first clock of the first // instruction executed by the CPU. // ; assume even APU and CPU clocks occur together // lda #$00 // sta $4017 ; mode begins in one clock // sta <0 ; delay 3 clocks // sta $4017 ; mode begins immediately // // Tests for APU clock jitter. Also tests basic timing of frame irq flag // since it's needed to determine jitter. // 1) Passed tests // 2) Frame irq is set too soon // 3) Frame irq is set too late // 4) Even jitter not handled properly // 5) Odd jitter not handled properly clock_jitter, // Mode 0 Timing // ------------- // -5 lda #$00 // -3 sta $4017 // 0 (write occurs here) // 1 // 2 // 3 // ... // Step 1 // 7459 Clock linear // ... // Step 2 // 14915 Clock linear & length // ... // Step 3 // 22373 Clock linear // ... // Step 4 // 29830 Set frame irq // 29831 Clock linear & length and set frame irq // 29832 Set frame irq // ... // Step 1 // 37289 Clock linear // ... // etc. // // Return current jitter in A. Takes an even number of clocks. Tests length // counter timing in mode 0. // 1) Passed tests // 2) First length is clocked too soon // 3) First length is clocked too late // 4) Second length is clocked too soon // 5) Second length is clocked too late // 6) Third length is clocked too soon // 7) Third length is clocked too late len_timing_mode0, // Mode 1 Timing // ------------- // -5 lda #$80 // -3 sta $4017 // 0 (write occurs here) // Step 0 // 1 Clock linear & length // 2 // ... // Step 1 // 7459 Clock linear // ... // Step 2 // 14915 Clock linear & length // ... // Step 3 // 22373 Clock linear // ... // Step 4 // 29829 (do nothing) // ... // Step 0 // 37283 Clock linear & length // ... // etc. // // Tests length counter timing in mode 1. // 1) Passed tests // 2) First length is clocked too soon // 3) First length is clocked too late // 4) Second length is clocked too soon // 5) Second length is clocked too late // 6) Third length is clocked too soon // 7) Third length is clocked too late len_timing_mode1, // Frame interrupt flag is set three times in a row 29831 clocks after // writing $4017 with $00. // 1) Success // 2) Flag first set too soon // 3) Flag first set too late // 4) Flag last set too soon // 5) Flag last set too late irq_flag_timing, // IRQ handler is invoked at minimum 29833 clocks after writing $00 to // $4017. // 1) Passed tests // 2) Too soon // 3) Too late // 4) Never occurred irq_timing, // After reset or power-up, APU acts as if $4017 were written with $00 from // 9 to 12 clocks before first instruction begins. // 1) Success // 2) $4015 didn't read back as $00 at power-up // 3) Fourth step occurs too soon // 4) Fourth step occurs too late reset_timing, // Changes to length counter halt occur after clocking length, not before. // 1) Passed tests // 2) Length shouldn't be clocked when halted at 14914 // 3) Length should be clocked when halted at 14915 // 4) Length should be clocked when unhalted at 14914 // 5) Length shouldn't be clocked when unhalted at 14915 len_halt_timing, // Write to length counter reload should be ignored when made during length // counter clocking and the length counter is not zero. // 1) Passed tests // 2) Reload just before length clock should work normally // 3) Reload just after length clock should work normally // 4) Reload during length clock when ctr = 0 should work normally // 5) Reload during length clock when ctr > 0 should be ignored len_reload_timing, // Verifies timing of length counter clocks in both modes // 2) First length of mode 0 is too soon // 3) First length of mode 0 is too late // 4) Second length of mode 0 is too soon // 5) Second length of mode 0 is too late // 6) Third length of mode 0 is too soon // 7) Third length of mode 0 is too late // 8) First length of mode 1 is too soon // 9) First length of mode 1 is too late // 10) Second length of mode 1 is too soon // 11) Second length of mode 1 is too late // 12) Third length of mode 1 is too soon // 13) Third length of mode 1 is too late len_timing, // Verifies basic DMC operation // 2) DMC isn't working well enough to test further // 3) Starting DMC should reload length from $4013 // 4) Writing $10 to $4015 should restart DMC if previous sample finished // 5) Writing $10 to $4015 should not affect DMC if previous sample is // still playing // 6) Writing $00 to $4015 should stop current sample // 7) Changing $4013 shouldn't affect current sample length // 8) Shouldn't set DMC IRQ flag when flag is disabled // 9) Should set IRQ flag when enabled and sample ends // 10) Reading IRQ flag shouldn't clear it // 11) Writing to $4015 should clear IRQ flag // 12) Disabling IRQ flag should clear it // 13) Looped sample shouldn't end until $00 is written to $4015 // 14) Looped sample shouldn't ever set IRQ flag // 15) Clearing loop flag and then setting again shouldn't stop loop // 16) Clearing loop flag should end sample once it reaches end // 17) Looped sample should reload length from $4013 each time it reaches // end // 18) $4013=0 should give 1-byte sample // 19) There should be a one-byte buffer that's filled immediately if empty dmc_basics, // Verifies the DMC's 16 rates dmc_rates, // Reset // See: // // At power and reset, $4015 is cleared. // 2) At power, $4015 should be cleared // 3) At reset, $4015 should be cleared reset_4015_cleared, // At power, it is as if $00 were written to $4017, // then a 9-12 clock delay, then execution from address // in reset vector. // At reset, same as above, except last value written // to $4017 is written again, rather than $00. // The delay from when $00 was written to $4017 is // printed. Delay after NES being powered off for a // minute is usually 9. // 2) Frame IRQ flag should be set later after power/reset // 3) Frame IRQ flag should be set sooner after power/reset reset_4017_timing, // At power, $4017 = $00. // At reset, $4017 mode is unchanged, but IRQ inhibit // flag is sometimes cleared. // 2) At power, $4017 should be written with $00 // 3) At reset, $4017 should should be rewritten with last value written reset_4017_written, // At power and reset, IRQ flag is clear. // 2) At power, flag should be clear // 3) At reset, flag should be clear reset_irq_flag_cleared, // At power and reset, length counters are enabled. // 2) At power, length counters should be enabled // 3) At reset, length counters should be enabled, triangle unaffected reset_len_ctrs_enabled, // At power and reset, $4017, $4015, and length counters work // immediately. // 2) At power, writes should work immediately // 3) At reset, writes should work immediately reset_works_immediately, // 11 tests that verify a number of behaviors with the APU (including the frame counter) // // See: test_1, test_2, test_3, test_4, test_5, test_6, test_7, test_8, test_9, test_10, // PAL APU tests // // See: // // Tests basic length counter operation // 1) Passed tests // 2) Problem with length counter load or $4015 // 3) Problem with length table, timing, or $4015 // 4) Writing $80 to $4017 should clock length immediately // 5) Writing $00 to $4017 shouldn't clock length immediately // 6) Clearing enable bit in $4015 should clear length counter // 7) When disabled via $4015, length shouldn't allow reloading // 8) Halt bit should suspend length clocking pal_len_ctr, // Tests all length table entries. // 1) Passed // 2) Failed. Prints four bytes $II $ee $cc $02 that indicate the length load // value written (ll), the value that the emulator uses ($ee), and the correct // value ($cc). pal_len_table, // Tests basic operation of frame irq flag. // 1) Tests passed // 2) Flag shouldn't be set in $4017 mode $40 // 3) Flag shouldn't be set in $4017 mode $80 // 4) Flag should be set in $4017 mode $00 // 5) Reading flag clears it // 6) Writing $00 or $80 to $4017 doesn't affect flag // 7) Writing $40 or $c0 to $4017 clears flag pal_irq_flag, // Tests for APU clock jitter. Also tests basic timing of frame irq flag since // it's needed to determine jitter. It's OK if you don't implement jitter, in // which case you'll get error #5, but you can still run later tests without // problem. // 1) Passed tests // 2) Frame irq is set too soon // 3) Frame irq is set too late // 4) Even jitter not handled properly // 5) Odd jitter not handled properly pal_clock_jitter, // Tests length counter timing in mode 0. // 1) Passed tests // 2) First length is clocked too soon // 3) First length is clocked too late // 4) Second length is clocked too soon // 5) Second length is clocked too late // 6) Third length is clocked too soon // 7) Third length is clocked too late pal_len_timing_mode0, // Tests length counter timing in mode 1. // 1) Passed tests // 2) First length is clocked too soon // 3) First length is clocked too late // 4) Second length is clocked too soon // 5) Second length is clocked too late // 6) Third length is clocked too soon // 7) Third length is clocked too late pal_len_timing_mode1, // Frame interrupt flag is set three times in a row 33255 clocks after writing // $4017 with $00. // 1) Success // 2) Flag first set too soon // 3) Flag first set too late // 4) Flag last set too soon // 5) Flag last set too late pal_irq_flag_timing, // IRQ handler is invoked at minimum 33257 clocks after writing $00 to $4017. // 1) Passed tests // 2) Too soon // 3) Too late // 4) Never occurred pal_irq_timing, // Changes to length counter halt occur after clocking length, not before. // 1) Passed tests // 2) Length shouldn't be clocked when halted at 16628 // 3) Length should be clocked when halted at 16629 // 4) Length should be clocked when unhalted at 16628 // 5) Length shouldn't be clocked when unhalted at 16629 pal_len_halt_timing, // Write to length counter reload should be ignored when made during length // counter clocking and the length counter is not zero. // 1) Passed tests // 2) Reload just before length clock should work normally // 3) Reload just after length clock should work normally // 4) Reload during length clock when ctr = 0 should work normally // 5) Reload during length clock when ctr > 0 should be ignored pal_len_reload_timing, #[ignore = "todo: passes, compare output"] apu_env, #[ignore = "todo: passes, check status"] dmc_buffer_retained, #[ignore = "todo: passes, compare output"] dmc_latency, #[ignore = "todo: passes, compare output"] dmc_pitch, #[ignore = "todo: passes, check status"] dmc_status, #[ignore = "todo: passes, check status"] dmc_status_irq, #[ignore = "todo: passes, compare output"] lin_ctr, #[ignore = "todo: passes, compare output"] noise_pitch, // Tests pulse behavior when writing to $4003/$4007 (reset duty but not dividers) #[ignore = "todo: unknown, compare output"] phase_reset, #[ignore = "todo: passes, compare output"] square_pitch, #[ignore = "todo: passes, compare output"] sweep_cutoff, #[ignore = "todo: passes, compare output"] sweep_sub, #[ignore = "todo: passes, compare output"] triangle_pitch, // This program demonstrates the channel balance among implementations // of the NES architecture. // The pattern consists of a set of 12 tones, as close to 1000 Hz as // the NES allows: // 1. Channel 1, 1/8 duty // 2. Channel 1, 1/4 duty // 3. Channel 1, 1/2 duty // 4. Channel 1, 3/4 duty // 5. Channels 1 and 2, 1/8 duty // 6. Channels 1 and 2, 1/4 duty // 7. Channels 1 and 2, 1/2 duty // 8. Channels 1 and 2, 3/4 duty // 9. Channel 3 // 10. Channel 4, long LFSR period // 11. Channel 4, short LFSR period // 12. Channel 5, amplitude 30 // When the user presses A on controller 1, the pattern plays three // times, with channel 5 held steady at 0, 48, and 96. The high point // of tone 12 each time is 30 units above the level for that time, // that is, 30, 78, and 126 respectively. // // See: #[ignore = "todo: unknown, compare output"] volumes, // Mixer // The test status is written to $6000. $80 means the test is running, $81 // means the test needs the reset button pressed, but delayed by at least // 100 msec from now. $00-$7F means the test has completed and given that // result code. // To allow an emulator to know when one of these tests is running and the // data at $6000+ is valid, as opposed to some other NES program, $DE $B0 // $G1 is written to $6001-$6003. // // A byte is reported as a series of tones. The code is in binary, with a // low tone for 0 and a high tone for 1, and with leading zeroes skipped. // The first tone is always a zero. A final code of 0 means passed, 1 means // failure, and 2 or higher indicates a specific reason. See the source // code of the test for more information about the meaning of a test code. // They are found after the set_test macro. For example, the cause of test // code 3 would be found in a line containing set_test 3. Examples: // Tones Binary Decimal Meaning // - - - - - - - - - - - - - - - - - - - - // low 0 0 passed // low high 01 1 failed // low high low 010 2 error 2 // // See #[ignore = "todo: passes, compare $6000 output"] dmc, #[ignore = "todo: passes, compare $6000 output"] noise, #[ignore = "todo: passes, compare $6000 output"] square, #[ignore = "todo: passes, compare $6000 output"] triangle, ); test_roms!( input, "test_roms/input", zapper_flip, zapper_light, #[ignore = "todo"] zapper_stream, #[ignore = "todo"] zapper_trigger, ); test_roms!( m004_txrom, "test_roms/mapper/m004_txrom", a12_clocking, clocking, details, rev_b, scanline_timing, big_chr_ram, rev_a, ); test_roms!(m005_exram, "test_roms/mapper/m005_exrom", exram, basics); } ================================================ FILE: tetanes-core/src/control_deck.rs ================================================ //! Control Deck implementation. The primary entry-point for emulating the NES. use crate::{ apu::{self, Apu, Channel}, bus::Bus, cart::{self, Cart}, common::{Clock, NesRegion, Regional, Reset, ResetKind, Sram}, cpu::Cpu, debug::Debugger, fs, genie::{self, GenieCode}, input::{FourPlayer, Joypad, Player}, mapper::{Bf909Revision, Mapper, MapperRevision, Mmc3Revision}, mem::RamState, ppu::Ppu, video::{Video, VideoFilter}, }; use bitflags::bitflags; use serde::{Deserialize, Serialize}; use std::{ io::Read, path::{Path, PathBuf}, }; use thiserror::Error; use tracing::{error, info}; /// Result returned from [`ControlDeck`] methods. pub type Result = std::result::Result; /// Errors that [`ControlDeck`] can return. #[derive(Error, Debug)] #[must_use] pub enum Error { /// [`Cart`] error when loading a ROM. #[error(transparent)] Cart(#[from] cart::Error), /// Battery-backed RAM error. #[error("sram error: {0:?}")] Sram(fs::Error), /// Save state error. #[error("save state error: {0:?}")] SaveState(fs::Error), /// When trying to load a save state that doesn't exist. #[error("no save state found")] NoSaveStateFound, /// Operational error indicating a ROM must be loaded first. #[error("no rom is loaded")] RomNotLoaded, /// CPU state is corrupted and emulation can't continue. Could be due to a bad ROM image or a /// corrupt save state. #[error("cpu state is corrupted")] CpuCorrupted, /// Invalid Game Genie code error. #[error(transparent)] InvalidGenieCode(#[from] genie::Error), /// Invalid file path. #[error("invalid file path {0:?}")] InvalidFilePath(PathBuf), #[error("unimplemented mapper `{0}`")] UnimplementedMapper(u16), /// Filesystem error. #[error(transparent)] Fs(#[from] fs::Error), /// IO error. #[error("{context}: {source:?}")] Io { context: String, source: std::io::Error, }, } impl Error { pub fn io(source: std::io::Error, context: impl Into) -> Self { Self::Io { context: context.into(), source, } } } bitflags! { /// Headless mode flags to disable audio and video processing, reducing CPU usage. #[derive(Default, Debug, Copy, Clone, PartialEq, Serialize, Deserialize, )] #[must_use] pub struct HeadlessMode: u8 { /// Disable audio mixing. const NO_AUDIO = 0x01; /// Disable pixel rendering. const NO_VIDEO = 0x02; } } /// Set of desired mapper revisions to use when loading a ROM matching the available mapper types. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[must_use] pub struct MapperRevisionsConfig { /// MMC3 mapper revision. pub mmc3: Mmc3Revision, /// BF909 mapper revision. pub bf909: Bf909Revision, } impl MapperRevisionsConfig { /// Set the desired mapper revision to use when loading a ROM matching the available mapper types. pub const fn set(&mut self, rev: MapperRevision) { match rev { MapperRevision::Mmc3(rev) => self.mmc3 = rev, MapperRevision::Bf909(rev) => self.bf909 = rev, } } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(default)] #[must_use] /// Control deck configuration settings. pub struct Config { /// Video filter. pub filter: VideoFilter, /// NES region. pub region: NesRegion, /// RAM initialization state. pub ram_state: RamState, /// Four player adapter. pub four_player: FourPlayer, /// Enable zapper gun. pub zapper: bool, /// Game Genie codes. pub genie_codes: Vec, /// Whether to support concurrent D-Pad input which wasn't possible on the original NES. pub concurrent_dpad: bool, /// Apu channels enabled. pub channels_enabled: [bool; Apu::MAX_CHANNEL_COUNT], /// Headless mode. pub headless_mode: HeadlessMode, /// Data directory for storing battery-backed RAM. pub data_dir: PathBuf, /// Which mapper revisions to emulate for any ROM loaded that uses this mapper. pub mapper_revisions: MapperRevisionsConfig, /// Whether to emulate PPU warmup where writes to certain registers are ignored. Can result in /// some games not working correctly. /// /// See: pub emulate_ppu_warmup: bool, } impl Config { /// Base directory for storing TetaNES data. pub const BASE_DIR: &'static str = "tetanes"; /// Directory for storing battery-backed Cart RAM. pub const SRAM_DIR: &'static str = "sram"; /// File extension for battery-backed Cart RAM. pub const SRAM_EXTENSION: &'static str = "sram"; /// Returns the default directory where TetaNES data is stored. #[inline] #[must_use] pub fn default_data_dir() -> PathBuf { dirs::data_local_dir().map_or_else(|| PathBuf::from("data"), |dir| dir.join(Self::BASE_DIR)) } /// Returns the directory used to store battery-backed Cart RAM. #[inline] #[must_use] pub fn sram_dir(&self) -> PathBuf { self.data_dir.join(Self::SRAM_DIR) } } impl Default for Config { fn default() -> Self { Self { filter: VideoFilter::default(), region: NesRegion::Auto, ram_state: RamState::Random, four_player: FourPlayer::default(), zapper: false, genie_codes: Vec::new(), concurrent_dpad: false, channels_enabled: [true; Apu::MAX_CHANNEL_COUNT], headless_mode: HeadlessMode::empty(), data_dir: Self::default_data_dir(), mapper_revisions: MapperRevisionsConfig::default(), emulate_ppu_warmup: false, } } } /// Represents a loaded ROM [`Cart`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct LoadedRom { /// Name of ROM. pub name: String, /// Whether the loaded Cart is battery-backed. pub battery_backed: bool, /// Auto-detected of the loaded Cart. pub region: NesRegion, } /// Represents an NES Control Deck. Encapsulates the entire emulation state. #[derive(Debug, Clone)] #[must_use] pub struct ControlDeck { /// Whether a ROM is loaded and the emulation is currently running or not. running: bool, /// Video output and filtering. video: Video, /// Last frame number rendered, allowing `frame_buffer` to be cached if called multiple times. last_frame_number: u32, /// The currently loaded ROM [`Cart`], if any. loaded_rom: Option, /// Directory for storing battery-backed Cart RAM if a ROM is loaded. sram_dir: PathBuf, /// Mapper revisions to emulate for any ROM loaded that matches the given mappers. mapper_revisions: MapperRevisionsConfig, /// Whether to auto-detect the region based on the loaded Cart. auto_detect_region: bool, /// Remaining CPU cycles to execute used to clock a given number of seconds. cycles_remaining: f32, /// Emulated frame speed step ranging from 1 (0.25 speed) to 8 (2.0). frame_speed_step: u16, /// Accumulated frame speed to account for slower 1x speeds. frame_accumulator: u16, /// NES CPU. cpu: Cpu, } impl Default for ControlDeck { fn default() -> Self { Self::new() } } impl ControlDeck { /// Create a NES `ControlDeck` with the default configuration. pub fn new() -> Self { Self::with_config(Config::default()) } /// Create a NES `ControlDeck` with a configuration. pub fn with_config(cfg: Config) -> Self { let mut cpu = Cpu::new(Bus::new(cfg.region, cfg.ram_state)); cpu.bus.ppu.skip_rendering = cfg.headless_mode.contains(HeadlessMode::NO_VIDEO); cpu.bus.ppu.emulate_warmup = cfg.emulate_ppu_warmup; cpu.bus.apu.skip_mixing = cfg.headless_mode.contains(HeadlessMode::NO_AUDIO); if cfg.region.is_auto() { cpu.set_region(NesRegion::default()); } else { cpu.set_region(cfg.region); } cpu.bus.input.set_concurrent_dpad(cfg.concurrent_dpad); cpu.bus.input.set_four_player(cfg.four_player); cpu.bus.input.connect_zapper(cfg.zapper); for (i, enabled) in cfg.channels_enabled.iter().enumerate() { match Channel::try_from(i) { Ok(channel) => cpu.bus.apu.set_channel_enabled(channel, *enabled), Err(apu::ParseChannelError) => tracing::error!("invalid APU channel: {i}"), } } for genie_code in cfg.genie_codes.iter().cloned() { cpu.bus.add_genie_code(genie_code); } let video = Video::with_filter(cfg.filter); Self { running: false, video, last_frame_number: 0, loaded_rom: None, sram_dir: cfg.sram_dir(), mapper_revisions: cfg.mapper_revisions, auto_detect_region: cfg.region.is_auto(), cycles_remaining: 0.0, frame_speed_step: 4, frame_accumulator: 0, cpu, } } /// Returns the path to the SRAM save file for a given ROM name which is used to store /// battery-backed Cart RAM. Returns `None` when the current platform doesn't have a /// `data` directory and no custom `data_dir` was configured. pub fn sram_path(&self, name: &str) -> PathBuf { self.sram_dir .join(name) .with_extension(Config::SRAM_EXTENSION) } /// Loads a ROM cartridge into memory /// /// # Errors /// /// If there is any issue loading the ROM, then an error is returned. pub fn load_rom(&mut self, name: S, rom: &mut F) -> Result { let name = name.to_string(); self.unload_rom()?; let cart = Cart::from_rom(&name, rom, self.cpu.bus.ram_state)?; if cart.mapper.is_none() { return Err(Error::UnimplementedMapper(cart.mapper_num())); } let loaded_rom = LoadedRom { name: name.clone(), battery_backed: cart.battery_backed(), region: cart.region(), }; if self.auto_detect_region { self.cpu.set_region(loaded_rom.region); } self.cpu.bus.load_cart(cart); self.loaded_rom = Some(loaded_rom.clone()); self.update_mapper_revisions(); self.reset(ResetKind::Hard); let sram_dir = self.sram_path(&name); if let Err(err) = self.load_sram(sram_dir) { error!("failed to load SRAM: {err:?}"); } Ok(loaded_rom) } /// Loads a ROM cartridge into memory from a path. /// /// # Errors /// /// If there is any issue loading the ROM, then an error is returned. pub fn load_rom_path(&mut self, path: impl AsRef) -> Result { use std::{fs::File, io::BufReader}; let path = path.as_ref(); let filename = fs::filename(path); info!("loading ROM: {filename}"); File::open(path) .map_err(|err| Error::io(err, format!("failed to open rom {path:?}"))) .and_then(|rom| self.load_rom(filename, &mut BufReader::new(rom))) } /// Unloads the currently loaded ROM and saves SRAM to disk if the Cart is battery-backed. /// /// # Errors /// /// If the loaded [`Cart`] is battery-backed and saving fails, then an error is returned. pub fn unload_rom(&mut self) -> Result<()> { if let Some(rom) = &self.loaded_rom { let sram_dir = self.sram_path(&rom.name); if let Err(err) = self.save_sram(sram_dir) { error!("failed to save SRAM: {err:?}"); } } self.loaded_rom = None; self.cpu.bus.unload_cart(); self.running = false; Ok(()) } /// Load a previously saved CPU state. #[inline] pub fn load_cpu(&mut self, cpu: Cpu) { self.cpu.load(cpu); } /// Set the [`MapperRevision`] to emulate for the any ROM loaded that uses this mapper. #[inline] pub const fn set_mapper_revision(&mut self, rev: MapperRevision) { self.mapper_revisions.set(rev); self.update_mapper_revisions(); } /// Set the set of [`MapperRevisionsConfig`] to emulate for the any ROM loaded that uses this /// mapper. #[inline] pub const fn set_mapper_revisions(&mut self, revs: MapperRevisionsConfig) { self.mapper_revisions = revs; self.update_mapper_revisions(); } /// Internal method to update the loaded ROM mapper revision when `mapper_revisions` is /// updated. const fn update_mapper_revisions(&mut self) { match &mut self.cpu.bus.ppu.mapper { Mapper::Txrom(mapper) => { mapper.set_revision(self.mapper_revisions.mmc3); } Mapper::Bf909x(mapper) => { mapper.set_revision(self.mapper_revisions.bf909); } // Remaining mappers all have more concrete detection via ROM headers Mapper::None(_) | Mapper::Nrom(_) | Mapper::Sxrom(_) | Mapper::Uxrom(_) | Mapper::Cnrom(_) | Mapper::Exrom(_) | Mapper::Axrom(_) | Mapper::Pxrom(_) | Mapper::Fxrom(_) | Mapper::ColorDreams(_) | Mapper::BandaiFCG(_) | Mapper::JalecoSs88006(_) | Mapper::Namco163(_) | Mapper::Vrc6(_) | Mapper::Bnrom(_) | Mapper::Nina001(_) | Mapper::Gxrom(_) | Mapper::SunsoftFme7(_) | Mapper::Nina003006(_) => (), } } /// Set whether concurrent D-Pad input is enabled which wasn't possible on the original NES. #[inline] pub fn set_concurrent_dpad(&mut self, enabled: bool) { self.cpu.bus.input.set_concurrent_dpad(enabled); } /// Set emulation RAM initialization state. #[inline] pub const fn set_ram_state(&mut self, ram_state: RamState) { self.cpu.bus.ram_state = ram_state; } /// Set the headless mode which can increase performance when the frame and audio outputs are /// not needed. #[inline] pub const fn set_headless_mode(&mut self, mode: HeadlessMode) { self.cpu.bus.ppu.skip_rendering = mode.contains(HeadlessMode::NO_VIDEO); self.cpu.bus.apu.skip_mixing = mode.contains(HeadlessMode::NO_AUDIO); } /// Set whether to emulate PPU warmup where writes to certain registers are ignored. Can result /// in some games not working correctly. /// /// See: #[inline] pub const fn set_emulate_ppu_warmup(&mut self, enabled: bool) { self.cpu.bus.ppu.emulate_warmup = enabled; } /// Adds a debugger callback to be executed any time the debugger conditions /// match. pub fn add_debugger(&mut self, debugger: Debugger) { match debugger { Debugger::Ppu(debugger) => self.cpu.bus.ppu.debugger = debugger, } } /// Removes a debugger callback. pub fn remove_debugger(&mut self, debugger: Debugger) { match debugger { Debugger::Ppu(_) => self.cpu.bus.ppu.debugger = Default::default(), } } /// Returns the name of the currently loaded ROM [`Cart`]. Returns `None` if no ROM is loaded. #[inline] #[must_use] pub const fn loaded_rom(&self) -> Option<&LoadedRom> { self.loaded_rom.as_ref() } /// Returns the auto-detected [`NesRegion`] for the loaded ROM. Returns `None` if no ROM is /// loaded. #[inline] #[must_use] pub fn cart_region(&self) -> Option { self.loaded_rom.as_ref().map(|rom| rom.region) } /// Returns whether the loaded ROM is battery-backed. Returns `None` if no ROM is loaded. #[inline] #[must_use] pub fn cart_battery_backed(&self) -> Option { self.loaded_rom.as_ref().map(|rom| rom.battery_backed) } /// Returns the NES Work RAM. #[inline] #[must_use] pub fn wram(&self) -> &[u8] { self.cpu.bus.wram() } /// Save battery-backed Save RAM to a file (if cartridge supports it) /// /// # Errors /// /// If the file path is invalid or fails to save, then an error is returned. pub fn save_sram(&self, path: impl AsRef) -> Result<()> { if let Some(true) = self.cart_battery_backed() { let path = path.as_ref(); if path.is_dir() { return Err(Error::InvalidFilePath(path.to_path_buf())); } info!("saving SRAM..."); self.cpu .bus .save(path.with_extension(Config::SRAM_EXTENSION)) .map_err(Error::Sram)?; } Ok(()) } /// Load battery-backed Save RAM from a file (if cartridge supports it) /// /// # Errors /// /// If the file path is invalid or fails to load, then an error is returned. pub fn load_sram(&mut self, path: impl AsRef) -> Result<()> { if let Some(true) = self.cart_battery_backed() { let path = path.as_ref(); if path.is_dir() { return Err(Error::InvalidFilePath(path.to_path_buf())); } if path.is_file() { info!("loading SRAM..."); self.cpu .bus .load(path.with_extension(Config::SRAM_EXTENSION)) .map_err(Error::Sram)?; } } Ok(()) } /// Save the current state of the console into a save file. /// /// # Errors /// /// If there is an issue saving the state, then an error is returned. pub fn save_state(&mut self, path: impl AsRef) -> Result<()> { if self.loaded_rom().is_none() { return Err(Error::RomNotLoaded); }; let path = path.as_ref(); fs::save(path, &self.cpu).map_err(Error::SaveState) } /// Load the console with data saved from a save state, if it exists. /// /// # Errors /// /// If there is an issue loading the save state, then an error is returned. pub fn load_state(&mut self, path: impl AsRef) -> Result<()> { if self.loaded_rom().is_none() { return Err(Error::RomNotLoaded); }; let path = path.as_ref(); if fs::exists(path) { fs::load::(path) .map_err(Error::SaveState) .map(|mut cpu| { cpu.bus.input.clear(); // Discard inputs from save states self.load_cpu(cpu) }) } else { Err(Error::NoSaveStateFound) } } /// Load the raw underlying frame buffer from the PPU for further processing. #[inline] pub fn frame_buffer_raw(&mut self) -> &[u16] { self.cpu.bus.ppu.frame_buffer() } /// Load a frame worth of pixels. #[inline] pub fn frame_buffer(&mut self) -> &[u8] { // Avoid applying filter if the frame number hasn't changed let frame_number = self.cpu.bus.ppu.frame_number(); if self.last_frame_number == frame_number { return &self.video.frame; } self.last_frame_number = frame_number; self.video .apply_filter(self.cpu.bus.ppu.frame_buffer(), frame_number) } /// Load a frame worth of pixels into the given buffer. #[inline] pub fn frame_buffer_into(&self, buffer: &mut [u8]) { self.video.apply_filter_into( self.cpu.bus.ppu.frame_buffer(), self.cpu.bus.ppu.frame_number(), buffer, ); } /// Get the current frame number. #[inline(always)] #[must_use] pub const fn frame_number(&self) -> u32 { self.cpu.bus.ppu.frame_number() } /// Get audio samples. #[inline(always)] #[must_use] pub fn audio_samples(&self) -> &[f32] { self.cpu.bus.audio_samples() } /// Clear audio samples. #[inline] pub fn clear_audio_samples(&mut self) { self.cpu.bus.clear_audio_samples(); } /// CPU clock rate based on currently configured NES region. #[inline] #[must_use] pub const fn clock_rate(&self) -> f32 { self.cpu.clock_rate() } /// Steps the control deck one CPU clock. /// /// # Errors /// /// If CPU encounters an invalid opcode, then an error is returned. pub fn clock_instr(&mut self) -> Result<()> { self.clock(); if self.cpu_corrupted() { self.running = false; return Err(Error::CpuCorrupted); } Ok(()) } /// Steps the control deck the given number of seconds. /// /// # Errors /// /// If CPU encounters an invalid opcode, then an error is returned. pub fn clock_seconds(&mut self, seconds: f32) -> Result { self.cycles_remaining += self.clock_rate() * seconds; let mut total_cycles = 0; while self.cycles_remaining > 0.0 { let start_cycles = self.cpu.cycle; self.clock_instr()?; let cycles = self.cpu.cycle - start_cycles; total_cycles += cycles; self.cycles_remaining -= cycles as f32; } Ok(total_cycles) } /// Steps the control deck the given number of seconds, calling `handle_audito` with audio /// samples and `handle_frame` with the `frame_buffer` if a frame is completed. /// /// # Errors /// /// If CPU encounters an invalid opcode, then an error is returned. pub fn clock_seconds_output( &mut self, seconds: f32, handle_audio: impl FnOnce(&[f32]), handle_frame: impl FnOnce(&[u8]), ) -> Result<()> { let frame = self.frame_number(); self.clock_seconds(seconds)?; let audio = self.cpu.bus.audio_samples(); handle_audio(audio); self.cpu.bus.clear_audio_samples(); if frame != self.frame_number() { let frame = self.video.apply_filter( self.cpu.bus.ppu.frame_buffer(), self.cpu.bus.ppu.frame_number(), ); handle_frame(frame); } Ok(()) } /// Steps the control deck an entire frame. /// /// # Errors /// /// If CPU encounters an invalid opcode, then an error is returned. pub fn clock_frame(&mut self) -> Result<()> { if !self.running { return Err(Error::RomNotLoaded); } // Frames that aren't multiples of the default render 1 more/less frames // every other frame // e.g. a speed of 1.5 will clock # of frames: 1, 2, 1, 2, 1, 2, 1, 2, ... // A speed of 0.5 will clock 0, 1, 0, 1, 0, 1, 0, 1, 0, ... self.frame_accumulator += self.frame_speed_step; let mut frames_to_clock = 0; while self.frame_accumulator >= 4 { self.frame_accumulator -= 4; frames_to_clock += 1; } for _ in 0..frames_to_clock { let frame = self.frame_number(); while frame == self.frame_number() { self.clock_instr()?; } self.cpu.clock_sync(); } Ok(()) } /// Steps the control deck an entire frame, calling `handle_output` with the `frame_buffer` and /// `audio_samples` for that frame. /// /// # Errors /// /// If CPU encounters an invalid opcode, then an error is returned. pub fn clock_frame_output( &mut self, handle_output: impl FnOnce(&[u8], &[f32]) -> T, ) -> Result { self.clock_frame()?; let frame = self.video.apply_filter( self.cpu.bus.ppu.frame_buffer(), self.cpu.bus.ppu.frame_number(), ); let audio = self.cpu.bus.audio_samples(); let res = handle_output(frame, audio); self.cpu.bus.clear_audio_samples(); Ok(res) } /// Steps the control deck an entire frame, copying the `frame_buffer` and /// `audio_samples` for that frame into the provided buffers. /// /// # Errors /// /// If CPU encounteres an invalid opcode, an error is returned. pub fn clock_frame_into( &mut self, frame_buffer: &mut [u8], audio_samples: &mut [f32], ) -> Result<()> { self.clock_frame()?; let frame = self.video.apply_filter( self.cpu.bus.ppu.frame_buffer(), self.cpu.bus.ppu.frame_number(), ); frame_buffer.copy_from_slice(&frame[..frame_buffer.len()]); let audio = self.cpu.bus.audio_samples(); audio_samples.copy_from_slice(&audio[..audio_samples.len()]); self.clear_audio_samples(); Ok(()) } /// Steps the control deck an entire frame with run-ahead frames to reduce input lag. /// /// # Errors /// /// If CPU encounters an invalid opcode, then an error is returned. pub fn clock_frame_ahead( &mut self, run_ahead: usize, handle_output: impl FnOnce(&[u8], &[f32]) -> T, ) -> Result { if run_ahead == 0 { return self.clock_frame_output(handle_output); } // Clock current frame and save state so we can rewind self.clock_frame()?; let frame = std::mem::take(&mut self.cpu.bus.ppu.frame.buffer); // Save state so we can rewind let config = bincode::config::legacy(); let state = bincode::serde::encode_to_vec(&self.cpu, config) .map_err(|err| fs::Error::SerializationFailed(err.to_string()))?; // Clock additional frames and discard video/audio self.cpu.bus.ppu.skip_rendering = true; for _ in 1..run_ahead { self.clock_frame()?; } self.cpu.bus.ppu.skip_rendering = false; // Output the future frame video/audio self.clear_audio_samples(); let result = self.clock_frame_output(handle_output)?; // Restore back to current frame let (mut state, _) = bincode::serde::decode_from_slice::(&state, config) .map_err(|err| fs::Error::DeserializationFailed(err.to_string()))?; state.bus.ppu.frame.buffer = frame; self.load_cpu(state); Ok(result) } /// Steps the control deck an entire frame with run-ahead frames to reduce input lag. /// /// # Errors /// /// If CPU encounters an invalid opcode, then an error is returned. pub fn clock_frame_ahead_into( &mut self, run_ahead: usize, frame_buffer: &mut [u8], audio_samples: &mut [f32], ) -> Result<()> { if run_ahead == 0 { return self.clock_frame_into(frame_buffer, audio_samples); } // Clock current frame and save state so we can rewind self.clock_frame()?; let frame = std::mem::take(&mut self.cpu.bus.ppu.frame.buffer); // Save state so we can rewind let config = bincode::config::legacy(); let state = bincode::serde::encode_to_vec(&self.cpu, config) .map_err(|err| fs::Error::SerializationFailed(err.to_string()))?; // Clock additional frames and discard video/audio for _ in 1..run_ahead { self.clock_frame()?; } // Output the future frame/audio self.clear_audio_samples(); self.clock_frame_into(frame_buffer, audio_samples)?; // Restore back to current frame let (mut state, _) = bincode::serde::decode_from_slice::(&state, config) .map_err(|err| fs::Error::DeserializationFailed(err.to_string()))?; state.bus.ppu.frame.buffer = frame; self.load_cpu(state); Ok(()) } /// Steps the control deck a single scanline. /// /// # Errors /// /// If CPU encounters an invalid opcode, then an error is returned. pub fn clock_scanline(&mut self) -> Result<()> { if !self.running { return Err(Error::RomNotLoaded); } let current_scanline = self.cpu.bus.ppu.scanline; while current_scanline == self.cpu.bus.ppu.scanline { self.clock_instr()?; } Ok(()) } /// Returns whether the CPU is corrupted or not which means it encounted an invalid/unhandled /// opcode and can't proceed executing the current ROM. #[cold] #[inline(always)] #[must_use] pub const fn cpu_corrupted(&self) -> bool { self.cpu.corrupted } /// Returns the current [`Cpu`] state. #[inline] pub const fn cpu(&self) -> &Cpu { &self.cpu } /// Returns a mutable reference to the current [`Cpu`] state. #[inline] pub const fn cpu_mut(&mut self) -> &mut Cpu { &mut self.cpu } /// Returns the current [`Ppu`] state. #[inline] pub const fn ppu(&self) -> &Ppu { &self.cpu.bus.ppu } /// Returns a mutable reference to the current [`Ppu`] state. #[inline] pub const fn ppu_mut(&mut self) -> &mut Ppu { &mut self.cpu.bus.ppu } /// Retu[ns the current [`Bus`] state. #[inline] pub const fn bus(&self) -> &Bus { &self.cpu.bus } /// Returns a mutable reference to the current [`Bus`] state. #[inline] pub const fn bus_mut(&mut self) -> &mut Bus { &mut self.cpu.bus } /// Returns the current [`Apu`] state. #[inline] pub const fn apu(&self) -> &Apu { &self.cpu.bus.apu } /// Returns a mutable reference to the current [`Apu`] state. #[inline] pub const fn apu_mut(&mut self) -> &Apu { &mut self.cpu.bus.apu } /// Returns the current [`Mapper`] state. #[inline] pub const fn mapper(&self) -> &Mapper { &self.cpu.bus.ppu.mapper } /// Returns a mutable reference to the current [`Mapper`] state. #[inline] pub const fn mapper_mut(&mut self) -> &mut Mapper { &mut self.cpu.bus.ppu.mapper } /// Returns the current four player mode. #[inline] pub const fn four_player(&self) -> FourPlayer { self.cpu.bus.input.four_player } /// Enable/Disable Four Score for 4-player controllers. #[inline] pub fn set_four_player(&mut self, four_player: FourPlayer) { self.cpu.bus.input.set_four_player(four_player); } /// Returns the current [`Joypad`] state for a given controller slot. #[inline] pub const fn joypad(&mut self, slot: Player) -> &Joypad { self.cpu.bus.input.joypad(slot) } /// Returns a mutable reference to the current [`Joypad`] state for a given controller slot. #[inline] pub const fn joypad_mut(&mut self, slot: Player) -> &mut Joypad { self.cpu.bus.input.joypad_mut(slot) } /// Returns whether the [`Zapper`](crate::input::Zapper) gun is connected. #[inline] pub const fn zapper_connected(&self) -> bool { self.cpu.bus.input.zapper.connected } /// Enable [`Zapper`](crate::input::Zapper) gun. #[inline] pub const fn connect_zapper(&mut self, enabled: bool) { self.cpu.bus.input.connect_zapper(enabled); } /// Returns the current [`Zapper`](crate::input::Zapper) aim position. #[inline] #[must_use] pub const fn zapper_pos(&self) -> (u16, u16) { let zapper = self.cpu.bus.input.zapper; (zapper.x(), zapper.y()) } /// Trigger [`Zapper`](crate::input::Zapper) gun. #[inline] pub fn trigger_zapper(&mut self) { self.cpu.bus.input.zapper.trigger(); } /// Aim [`Zapper`](crate::input::Zapper) gun. #[inline] pub const fn aim_zapper(&mut self, x: u16, y: u16) { self.cpu.bus.input.zapper.aim(x, y); } /// Set the video filter for frame buffer output when calling [`ControlDeck::frame_buffer`]. #[inline] pub const fn set_filter(&mut self, filter: VideoFilter) { self.video.filter = filter; } /// Set the [`Apu`] sample rate. #[inline] pub fn set_sample_rate(&mut self, sample_rate: f32) { self.cpu.bus.apu.set_sample_rate(sample_rate); } /// Set the emulation speed. #[inline] pub fn set_frame_speed(&mut self, speed: f32) { self.frame_speed_step = (speed * 4.0) as u16; self.cpu.bus.apu.set_frame_speed(speed); } /// Add a NES Game Genie code. /// /// # Errors /// /// If the genie code is invalid, an error is returned. #[inline] pub fn add_genie_code(&mut self, genie_code: String) -> Result<()> { self.cpu.bus.add_genie_code(GenieCode::new(genie_code)?); Ok(()) } /// Remove a NES Game Genie code. #[inline] pub fn remove_genie_code(&mut self, genie_code: &str) { self.cpu.bus.remove_genie_code(genie_code); } /// Remove all NES Game Genie codes. #[inline] pub fn clear_genie_codes(&mut self) { self.cpu.bus.clear_genie_codes(); } /// Returns whether a given [`Apu`] [`Channel`] is enabled. #[inline] #[must_use] pub const fn channel_enabled(&self, channel: Channel) -> bool { self.cpu.bus.apu.channel_enabled(channel) } /// Enable or disable a given [`Apu`] [`Channel`]. #[inline] pub const fn set_apu_channel_enabled(&mut self, channel: Channel, enabled: bool) { self.cpu.bus.apu.set_channel_enabled(channel, enabled); } /// Toggle a given [`Apu`] [`Channel`]. #[inline] pub const fn toggle_apu_channel(&mut self, channel: Channel) { self.cpu.bus.apu.toggle_channel(channel); } /// Returns whether the control deck is currently running. #[inline] #[must_use] pub const fn is_running(&self) -> bool { self.running } } impl Clock for ControlDeck { /// Steps the control deck a single clock cycle. #[inline(always)] fn clock(&mut self) { self.cpu.clock() } } impl Regional for ControlDeck { /// Get the NES format for the emulation. fn region(&self) -> NesRegion { self.cpu.region() } /// Set the NES format for the emulation. fn set_region(&mut self, region: NesRegion) { self.auto_detect_region = region.is_auto(); if self.auto_detect_region { self.cpu.set_region(self.cart_region().unwrap_or_default()); } else { self.cpu.set_region(region); } } } impl Reset for ControlDeck { /// Resets the console. fn reset(&mut self, kind: ResetKind) { self.cpu.reset(kind); if self.loaded_rom.is_some() { self.running = true; } } } ================================================ FILE: tetanes-core/src/cpu/instr.rs ================================================ //! CPU Asddressing cmps and Operations use crate::{ cpu::{Cpu, IrqFlags, Status}, mem::{Read, Write}, }; use serde::{Deserialize, Serialize}; /// List of all CPU official and unofficial operations. /// /// # References /// /// - /// - #[rustfmt::skip] #[allow(clippy::upper_case_acronyms)] #[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[must_use] pub enum Instr { ADC, AND, ASL, BCC, BCS, BEQ, BIT, BMI, BNE, BPL, BRK, BVC, BVS, CLC, CLD, CLI, CLV, CMP, CPX, CPY, DEC, DEX, DEY, EOR, INC, INX, INY, JMP, JSR, LDA, LDX, LDY, LSR, NOP, ORA, PHA, PHP, PLA, PLP, ROL, ROR, RTI, RTS, SBC, SEC, SED, SEI, STA, STX, STY, TAX, TAY, TSX, TXA, TXS, TYA, // "Unofficial" opcodes ISB, DCP, AXS, LAS, LAX, AHX, SAX, XAA, SXA, RRA, TAS, SYA, ARR, SRE, ALR, RLA, ANC, SHAZ, ATX, SHAA, SLO, #[default] HLT } /// CPU Addressing mode. #[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[allow(clippy::upper_case_acronyms)] #[rustfmt::skip] #[must_use] pub enum AddrMode { // Accumulator and Implied ACC, IMP, // Immediate and relative #[default] IMM, REL, // Zero Page ZP0, ABS, ZPX, ZPY, // Indirect, with read/write variants IND, IDX, IDY, IDYW, // Absolute, with read/write variants ABX, ABXW, ABY, ABYW, // Special address mode, handled separately OTH } /// CPU Opcode. #[derive(Debug, Copy, Clone)] #[must_use] pub struct Op { f: fn(&mut Cpu), addr_mode: AddrMode, } impl Op { #[inline(always)] pub fn run(&self, cpu: &mut Cpu) { (self.f)(cpu) } #[inline(always)] pub const fn addr_mode(&self) -> AddrMode { self.addr_mode } } macro_rules! op { ($f:ident, $addr_mode:ident) => { Op { f: Cpu::$f, addr_mode: AddrMode::$addr_mode, } }; } /// CPU Instruction Reference. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[must_use] pub struct InstrRef { pub opcode: u8, pub instr: Instr, pub addr_mode: AddrMode, pub cycles: u8, } impl std::fmt::Display for InstrRef { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { let instr = self.instr; let unofficial = match instr { Instr::HLT | Instr::ISB | Instr::DCP | Instr::AXS | Instr::LAS | Instr::LAX | Instr::AHX | Instr::SAX | Instr::XAA | Instr::SXA | Instr::RRA | Instr::TAS | Instr::SYA | Instr::ARR | Instr::SRE | Instr::ALR | Instr::RLA | Instr::ANC | Instr::SLO => "*", Instr::NOP if self.opcode != 0xEA => "*", // 0xEA is the only official NOP Instr::SBC if self.opcode == 0xEB => "*", _ => "", }; write!(f, "{unofficial:1}{instr:?}") } } macro_rules! instr { ($opcode:expr, $instr:ident, $addr_mode:ident, $cycles:expr) => { InstrRef { opcode: $opcode, instr: Instr::$instr, addr_mode: AddrMode::$addr_mode, cycles: $cycles, } }; } /// CPU Addressing Modes /// /// The 6502 can address 64KB from 0x0000 - 0xFFFF. The high byte is usually the page and the /// low byte the offset into the page. There are 256 total pages of 256 bytes. impl Cpu { /// 16x16 grid of 6502 opcode operations. Matches datasheet matrix for easy lookup #[rustfmt::skip] pub const OPS: [Op; 256] = [ // 0 1 2 3 4 5 6 7 8 9 A B C D E F /* 0 */ op!(brk, IMM), op!(ora, IDX), op!(hlt, IMP), op!(slo, IDX), op!(nop, ZP0), op!(ora, ZP0), op!(aslm, ZP0), op!(slo, ZP0), op!(php, IMP), op!(ora, IMM), op!(asla, ACC), op!(anc, IMM), op!(nop, ABS), op!(ora, ABS), op!(aslm, ABS), op!(slo, ABS), /* 1 */ op!(bpl, REL), op!(ora, IDY), op!(hlt, IMP), op!(slo, IDYW), op!(nop, ZPX), op!(ora, ZPX), op!(aslm, ZPX), op!(slo, ZPX), op!(clc, IMP), op!(ora, ABY), op!(nop, IMP), op!(slo, ABYW), op!(nop, ABX), op!(ora, ABX), op!(aslm, ABXW), op!(slo, ABXW), /* 2 */ op!(jsr, OTH), op!(and, IDX), op!(hlt, IMP), op!(rla, IDX), op!(bit, ZP0), op!(and, ZP0), op!(rolm, ZP0), op!(rla, ZP0), op!(plp, IMP), op!(and, IMM), op!(rola, ACC), op!(anc, IMM), op!(bit, ABS), op!(and, ABS), op!(rolm, ABS), op!(rla, ABS), /* 3 */ op!(bmi, REL), op!(and, IDY), op!(hlt, IMP), op!(rla, IDYW), op!(nop, ZPX), op!(and, ZPX), op!(rolm, ZPX), op!(rla, ZPX), op!(sec, IMP), op!(and, ABY), op!(nop, IMP), op!(rla, ABYW), op!(nop, ABX), op!(and, ABX), op!(rolm, ABXW), op!(rla, ABXW), /* 4 */ op!(rti, IMP), op!(eor, IDX), op!(hlt, IMP), op!(sre, IDX), op!(nop, ZP0), op!(eor, ZP0), op!(lsrm, ZP0), op!(sre, ZP0), op!(pha, IMP), op!(eor, IMM), op!(lsra, ACC), op!(alr, IMM), op!(jmpa, ABS), op!(eor, ABS), op!(lsrm, ABS), op!(sre, ABS), /* 5 */ op!(bvc, REL), op!(eor, IDY), op!(hlt, IMP), op!(sre, IDYW), op!(nop, ZPX), op!(eor, ZPX), op!(lsrm, ZPX), op!(sre, ZPX), op!(cli, IMP), op!(eor, ABY), op!(nop, IMP), op!(sre, ABYW), op!(nop, ABX), op!(eor, ABX), op!(lsrm, ABXW), op!(sre, ABXW), /* 6 */ op!(rts, IMP), op!(adc, IDX), op!(hlt, IMP), op!(rra, IDX), op!(nop, ZP0), op!(adc, ZP0), op!(rorm, ZP0), op!(rra, ZP0), op!(pla, IMP), op!(adc, IMM), op!(rora, ACC), op!(arr, IMM), op!(jmpi, IND), op!(adc, ABS), op!(rorm, ABS), op!(rra, ABS), /* 7 */ op!(bvs, REL), op!(adc, IDY), op!(hlt, IMP), op!(rra, IDYW), op!(nop, ZPX), op!(adc, ZPX), op!(rorm, ZPX), op!(rra, ZPX), op!(sei, IMP), op!(adc, ABY), op!(nop, IMP), op!(rra, ABYW), op!(nop, ABX), op!(adc, ABX), op!(rorm, ABXW), op!(rra, ABXW), /* 8 */ op!(nop, IMM), op!(sta, IDX), op!(nop, IMM), op!(sax, IDX), op!(sty, ZP0), op!(sta, ZP0), op!(stx, ZP0), op!(sax, ZP0), op!(dey, IMP), op!(nop, IMM), op!(txa, IMP), op!(xaa, IMM), op!(sty, ABS), op!(sta, ABS), op!(stx, ABS), op!(sax, ABS), /* 9 */ op!(bcc, REL), op!(sta, IDYW), op!(hlt, IMP), op!(shaz, OTH), op!(sty, ZPX), op!(sta, ZPX), op!(stx, ZPY), op!(sax, ZPY), op!(tya, IMP), op!(sta, ABYW), op!(txs, IMP), op!(tas, OTH), op!(sya, OTH), op!(sta, ABXW), op!(sxa, OTH), op!(shaa, OTH), /* A */ op!(ldy, IMM), op!(lda, IDX), op!(ldx, IMM), op!(lax, IDX), op!(ldy, ZP0), op!(lda, ZP0), op!(ldx, ZP0), op!(lax, ZP0), op!(tay, IMP), op!(lda, IMM), op!(tax, IMP), op!(atx, IMM), op!(ldy, ABS), op!(lda, ABS), op!(ldx, ABS), op!(lax, ABS), /* B */ op!(bcs, REL), op!(lda, IDY), op!(hlt, IMP), op!(lax, IDY), op!(ldy, ZPX), op!(lda, ZPX), op!(ldx, ZPY), op!(lax, ZPY), op!(clv, IMP), op!(lda, ABY), op!(tsx, IMP), op!(las, ABY), op!(ldy, ABX), op!(lda, ABX), op!(ldx, ABY), op!(lax, ABY), /* C */ op!(cpy, IMM), op!(cpa, IDX), op!(nop, IMM), op!(dcp, IDX), op!(cpy, ZP0), op!(cpa, ZP0), op!(dec, ZP0), op!(dcp, ZP0), op!(iny, IMP), op!(cpa, IMM), op!(dex, IMP), op!(axs, IMM), op!(cpy, ABS), op!(cpa, ABS), op!(dec, ABS), op!(dcp, ABS), /* D */ op!(bne, REL), op!(cpa, IDY), op!(hlt, IMP), op!(dcp, IDYW), op!(nop, ZPX), op!(cpa, ZPX), op!(dec, ZPX), op!(dcp, ZPX), op!(cld, IMP), op!(cpa, ABY), op!(nop, IMP), op!(dcp, ABYW), op!(nop, ABX), op!(cpa, ABX), op!(dec, ABXW), op!(dcp, ABXW), /* E */ op!(cpx, IMM), op!(sbc, IDX), op!(nop, IMM), op!(isb, IDX), op!(cpx, ZP0), op!(sbc, ZP0), op!(inc, ZP0), op!(isb, ZP0), op!(inx, IMP), op!(sbc, IMM), op!(nop, IMP), op!(sbc, IMM), op!(cpx, ABS), op!(sbc, ABS), op!(inc, ABS), op!(isb, ABS), /* F */ op!(beq, REL), op!(sbc, IDY), op!(hlt, IMP), op!(isb, IDYW), op!(nop, ZPX), op!(sbc, ZPX), op!(inc, ZPX), op!(isb, ZPX), op!(sed, IMP), op!(sbc, ABY), op!(nop, IMP), op!(isb, ABYW), op!(nop, ABX), op!(sbc, ABX), op!(inc, ABXW), op!(isb, ABXW), ]; /// 16x16 grid of 6502 opcode instructions. Matches datasheet matrix for easy lookup #[rustfmt::skip] pub const INSTR_REF: [InstrRef; 256] = [ instr!(0x00, BRK, IMM, 7), instr!(0x01, ORA, IDX, 6), instr!(0x02, HLT, IMP, 2), instr!(0x03, SLO, IDX, 8), instr!(0x04, NOP, ZP0, 3), instr!(0x05, ORA, ZP0, 3), instr!(0x06, ASL, ZP0, 5), instr!(0x07, SLO, ZP0, 5), instr!(0x08, PHP, IMP, 3), instr!(0x09, ORA, IMM, 2), instr!(0x0A, ASL, ACC, 2), instr!(0x0B, ANC, IMM, 2), instr!(0x0C, NOP, ABS, 4), instr!(0x0D, ORA, ABS, 4), instr!(0x0E, ASL, ABS, 6), instr!(0x0F, SLO, ABS, 6), instr!(0x10, BPL, REL, 2), instr!(0x11, ORA, IDY, 5), instr!(0x12, HLT, IMP, 2), instr!(0x13, SLO, IDYW, 8), instr!(0x14, NOP, ZPX, 4), instr!(0x15, ORA, ZPX, 4), instr!(0x16, ASL, ZPX, 6), instr!(0x17, SLO, ZPX, 6), instr!(0x18, CLC, IMP, 2), instr!(0x19, ORA, ABY, 4), instr!(0x1A, NOP, IMP, 2), instr!(0x1B, SLO, ABYW, 7), instr!(0x1C, NOP, ABX, 4), instr!(0x1D, ORA, ABX, 4), instr!(0x1E, ASL, ABXW, 7), instr!(0x1F, SLO, ABXW, 7), instr!(0x20, JSR, OTH, 6), instr!(0x21, AND, IDX, 6), instr!(0x22, HLT, IMP, 2), instr!(0x23, RLA, IDX, 8), instr!(0x24, BIT, ZP0, 3), instr!(0x25, AND, ZP0, 3), instr!(0x26, ROL, ZP0, 5), instr!(0x27, RLA, ZP0, 5), instr!(0x28, PLP, IMP, 4), instr!(0x29, AND, IMM, 2), instr!(0x2A, ROL, ACC, 2), instr!(0x2B, ANC, IMM, 2), instr!(0x2C, BIT, ABS, 4), instr!(0x2D, AND, ABS, 4), instr!(0x2E, ROL, ABS, 6), instr!(0x2F, RLA, ABS, 6), instr!(0x30, BMI, REL, 2), instr!(0x31, AND, IDY, 5), instr!(0x32, HLT, IMP, 2), instr!(0x33, RLA, IDYW, 8), instr!(0x34, NOP, ZPX, 4), instr!(0x35, AND, ZPX, 4), instr!(0x36, ROL, ZPX, 6), instr!(0x37, RLA, ZPX, 6), instr!(0x38, SEC, IMP, 2), instr!(0x39, AND, ABY, 4), instr!(0x3A, NOP, IMP, 2), instr!(0x3B, RLA, ABYW, 7), instr!(0x3C, NOP, ABX, 4), instr!(0x3D, AND, ABX, 4), instr!(0x3E, ROL, ABXW, 7), instr!(0x3F, RLA, ABXW, 7), instr!(0x40, RTI, IMP, 6), instr!(0x41, EOR, IDX, 6), instr!(0x42, HLT, IMP, 2), instr!(0x43, SRE, IDX, 8), instr!(0x44, NOP, ZP0, 3), instr!(0x45, EOR, ZP0, 3), instr!(0x46, LSR, ZP0, 5), instr!(0x47, SRE, ZP0, 5), instr!(0x48, PHA, IMP, 3), instr!(0x49, EOR, IMM, 2), instr!(0x4A, LSR, ACC, 2), instr!(0x4B, ALR, IMM, 2), instr!(0x4C, JMP, ABS, 3), instr!(0x4D, EOR, ABS, 4), instr!(0x4E, LSR, ABS, 6), instr!(0x4F, SRE, ABS, 6), instr!(0x50, BVC, REL, 2), instr!(0x51, EOR, IDY, 5), instr!(0x52, HLT, IMP, 2), instr!(0x53, SRE, IDYW, 8), instr!(0x54, NOP, ZPX, 4), instr!(0x55, EOR, ZPX, 4), instr!(0x56, LSR, ZPX, 6), instr!(0x57, SRE, ZPX, 6), instr!(0x58, CLI, IMP, 2), instr!(0x59, EOR, ABY, 4), instr!(0x5A, NOP, IMP, 2), instr!(0x5B, SRE, ABYW, 7), instr!(0x5C, NOP, ABX, 4), instr!(0x5D, EOR, ABX, 4), instr!(0x5E, LSR, ABXW, 7), instr!(0x5F, SRE, ABXW, 7), instr!(0x60, RTS, IMP, 6), instr!(0x61, ADC, IDX, 6), instr!(0x62, HLT, IMP, 2), instr!(0x63, RRA, IDX, 8), instr!(0x64, NOP, ZP0, 3), instr!(0x65, ADC, ZP0, 3), instr!(0x66, ROR, ZP0, 5), instr!(0x67, RRA, ZP0, 5), instr!(0x68, PLA, IMP, 4), instr!(0x69, ADC, IMM, 2), instr!(0x6A, ROR, ACC, 2), instr!(0x6B, ARR, IMM, 2), instr!(0x6C, JMP, IND, 5), instr!(0x6D, ADC, ABS, 4), instr!(0x6E, ROR, ABS, 6), instr!(0x6F, RRA, ABS, 6), instr!(0x70, BVS, REL, 2), instr!(0x71, ADC, IDY, 5), instr!(0x72, HLT, IMP, 2), instr!(0x73, RRA, IDYW, 8), instr!(0x74, NOP, ZPX, 4), instr!(0x75, ADC, ZPX, 4), instr!(0x76, ROR, ZPX, 6), instr!(0x77, RRA, ZPX, 6), instr!(0x78, SEI, IMP, 2), instr!(0x79, ADC, ABY, 4), instr!(0x7A, NOP, IMP, 2), instr!(0x7B, RRA, ABYW, 7), instr!(0x7C, NOP, ABX, 4), instr!(0x7D, ADC, ABX, 4), instr!(0x7E, ROR, ABXW, 7), instr!(0x7F, RRA, ABXW, 7), instr!(0x80, NOP, IMM, 2), instr!(0x81, STA, IDX, 6), instr!(0x82, NOP, IMM, 2), instr!(0x83, SAX, IDX, 6), instr!(0x84, STY, ZP0, 3), instr!(0x85, STA, ZP0, 3), instr!(0x86, STX, ZP0, 3), instr!(0x87, SAX, ZP0, 3), instr!(0x88, DEY, IMP, 2), instr!(0x89, NOP, IMM, 2), instr!(0x8A, TXA, IMP, 2), instr!(0x8B, XAA, IMM, 2), instr!(0x8C, STY, ABS, 4), instr!(0x8D, STA, ABS, 4), instr!(0x8E, STX, ABS, 4), instr!(0x8F, SAX, ABS , 4), instr!(0x90, BCC, REL, 2), instr!(0x91, STA, IDYW, 6), instr!(0x92, HLT, IMP, 2), instr!(0x93, AHX, OTH, 6), instr!(0x94, STY, ZPX, 4), instr!(0x95, STA, ZPX, 4), instr!(0x96, STX, ZPY, 4), instr!(0x97, SAX, ZPY, 4), instr!(0x98, TYA, IMP, 2), instr!(0x99, STA, ABYW, 5), instr!(0x9A, TXS, IMP, 2), instr!(0x9B, TAS, OTH, 5), instr!(0x9C, SYA, OTH, 5), instr!(0x9D, STA, ABXW, 5), instr!(0x9E, SXA, OTH, 5), instr!(0x9F, SHAA, OTH, 5), instr!(0xA0, LDY, IMM, 2), instr!(0xA1, LDA, IDX, 6), instr!(0xA2, LDX, IMM, 2), instr!(0xA3, LAX, IDX, 6), instr!(0xA4, LDY, ZP0, 3), instr!(0xA5, LDA, ZP0, 3), instr!(0xA6, LDX, ZP0, 3), instr!(0xA7, LAX, ZP0, 3), instr!(0xA8, TAY, IMP, 2), instr!(0xA9, LDA, IMM, 2), instr!(0xAA, TAX, IMP, 2), instr!(0xAB, ATX, IMM, 2), instr!(0xAC, LDY, ABS, 4), instr!(0xAD, LDA, ABS, 4), instr!(0xAE, LDX, ABS, 4), instr!(0xAF, LAX, ABS, 4), instr!(0xB0, BCS, REL, 2), instr!(0xB1, LDA, IDY, 5), instr!(0xB2, HLT, IMP, 2), instr!(0xB3, LAX, IDY, 5), instr!(0xB4, LDY, ZPX, 4), instr!(0xB5, LDA, ZPX, 4), instr!(0xB6, LDX, ZPY, 4), instr!(0xB7, LAX, ZPY, 4), instr!(0xB8, CLV, IMP, 2), instr!(0xB9, LDA, ABY, 4), instr!(0xBA, TSX, IMP, 2), instr!(0xBB, LAS, ABY, 4), instr!(0xBC, LDY, ABX, 4), instr!(0xBD, LDA, ABX, 4), instr!(0xBE, LDX, ABY, 4), instr!(0xBF, LAX, ABY, 4), instr!(0xC0, CPY, IMM, 2), instr!(0xC1, CMP, IDX, 6), instr!(0xC2, NOP, IMM, 2), instr!(0xC3, DCP, IDX, 8), instr!(0xC4, CPY, ZP0, 3), instr!(0xC5, CMP, ZP0, 3), instr!(0xC6, DEC, ZP0, 5), instr!(0xC7, DCP, ZP0, 5), instr!(0xC8, INY, IMP, 2), instr!(0xC9, CMP, IMM, 2), instr!(0xCA, DEX, IMP, 2), instr!(0xCB, AXS, IMM, 2), instr!(0xCC, CPY, ABS, 4), instr!(0xCD, CMP, ABS, 4), instr!(0xCE, DEC, ABS, 6), instr!(0xCF, DCP, ABS, 6), instr!(0xD0, BNE, REL, 2), instr!(0xD1, CMP, IDY, 5), instr!(0xD2, HLT, IMP, 2), instr!(0xD3, DCP, IDYW, 8), instr!(0xD4, NOP, ZPX, 4), instr!(0xD5, CMP, ZPX, 4), instr!(0xD6, DEC, ZPX, 6), instr!(0xD7, DCP, ZPX, 6), instr!(0xD8, CLD, IMP, 2), instr!(0xD9, CMP, ABY, 4), instr!(0xDA, NOP, IMP, 2), instr!(0xDB, DCP, ABYW, 7), instr!(0xDC, NOP, ABX, 4), instr!(0xDD, CMP, ABX, 4), instr!(0xDE, DEC, ABXW, 7), instr!(0xDF, DCP, ABXW, 7), instr!(0xE0, CPX, IMM, 2), instr!(0xE1, SBC, IDX, 6), instr!(0xE2, NOP, IMM, 2), instr!(0xE3, ISB, IDX, 8), instr!(0xE4, CPX, ZP0, 3), instr!(0xE5, SBC, ZP0, 3), instr!(0xE6, INC, ZP0, 5), instr!(0xE7, ISB, ZP0, 5), instr!(0xE8, INX, IMP, 2), instr!(0xE9, SBC, IMM, 2), instr!(0xEA, NOP, IMP, 2), instr!(0xEB, SBC, IMM, 2), instr!(0xEC, CPX, ABS, 4), instr!(0xED, SBC, ABS, 4), instr!(0xEE, INC, ABS, 6), instr!(0xEF, ISB, ABS, 6), instr!(0xF0, BEQ, REL, 2), instr!(0xF1, SBC, IDY, 5), instr!(0xF2, HLT, IMP, 2), instr!(0xF3, ISB, IDYW, 8), instr!(0xF4, NOP, ZPX, 4), instr!(0xF5, SBC, ZPX, 4), instr!(0xF6, INC, ZPX, 6), instr!(0xF7, ISB, ZPX, 6), instr!(0xF8, SED, IMP, 2), instr!(0xF9, SBC, ABY, 4), instr!(0xFA, NOP, IMP, 2), instr!(0xFB, ISB, ABYW, 7), instr!(0xFC, NOP, ABX, 4), instr!(0xFD, SBC, ABX, 4), instr!(0xFE, INC, ABXW, 7), instr!(0xFF, ISB, ABXW, 7), ]; /// Accumulator Addressing. /// /// No additional data is required, but the default target will be the accumulator. /// /// # Instructions /// /// ASL, ROL, LSR, ROR /// /// ```text /// # address R/W description /// --- ------- --- ----------------------------------------------- /// 1 PC R fetch opcode, increment PC /// 2 PC R read next instruction byte (and throw it away) /// ``` /// /// Implied Addressing. /// /// No additional data is required, but the default target will be the accumulator. /// /// ```text /// # address R/W description /// --- ------- --- ----------------------------------------------- /// 1 PC R fetch opcode, increment PC /// 2 PC R read next instruction byte (and throw it away) /// ``` #[inline(always)] pub fn acc_imp(&mut self) -> u16 { self.read(self.pc); // Cycle 2, dummy read 0 } /// Immediate Addressing. /// /// Uses the next byte as the value. /// /// ```text /// # address R/W description /// --- ------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch value, increment PC /// ``` /// /// Relative Addressing. /// /// This mode is only used by branching instructions. The address must be between -128 and +127, /// allowing the branching instruction to move backward or forward relative to the current /// program counter. /// /// # Notes /// /// The opcode fetch of the next instruction is included to this diagram for illustration /// purposes. When determining real execution times, remember to subtract the last cycle. /// /// ```text /// # address R/W description /// --- --------- --- --------------------------------------------- /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch fetched_data, increment PC /// 3 PC R Fetch opcode of next instruction, /// If branch is taken, add fetched_data to PCL. /// Otherwise increment PC. /// 4+ PC* R Fetch opcode of next instruction. /// Fix PCH. If it did not change, increment PC. /// 5! PC R Fetch opcode of next instruction, /// increment PC. /// /// * The high byte of Program Counter (PCH) may be invalid /// at this time, i.e. it may be smaller or bigger by $100. /// + If branch is taken, this cycle will be executed. /// ! If branch occurs to different page, this cycle will be /// executed. /// ``` /// /// Zero Page Addressing. /// /// Accesses the first 0xFF bytes of the address range, so this only requires one extra byte /// instead of the usual two. /// /// # Read instructions /// /// LDA, LDX, LDY, EOR, AND, ORA, ADC, SBC, CMP, BIT, LAX, NOP /// /// ```text /// # address R/W description /// --- ------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch address, increment PC /// 3 address R read from effective address /// ``` /// /// # Read-Modify-Write instructions /// /// ASL, LSR, ROL, ROR, INC, DEC, SLO, SRE, RLA, RRA, ISB, DCP /// /// ```text /// # address R/W description /// --- ------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch address, increment PC /// 3 address R read from effective address /// 4 address W write the value back to effective address, /// and do the operation on it /// 5 address W write the new value to effective address /// ``` /// /// # Write instructions /// /// STA, STX, STY, SAX /// /// ```text /// # address R/W description /// --- ------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch address, increment PC /// 3 address W write register to effective address /// ``` #[inline(always)] pub fn imm_rel_zp(&mut self) -> u16 { u16::from(self.fetch_byte()) // Cycle 2 } /// Zero Page Addressing w/ X offset. /// /// Same as Zero Page, but is offset by adding the x register. /// /// # Read instructions /// /// LDA, LDX, LDY, EOR, AND, ORA, ADC, SBC, CMP, BIT, LAX, NOP /// /// ```text /// # address R/W description /// --- --------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch address, increment PC /// 3 address R read from address, add index register to it /// 4 address+X* R read from effective address /// /// * The high byte of the effective address is always zero, /// i.e. page boundary crossings are not handled. /// ``` /// /// # Read-Modify-Write instructions /// /// ASL, LSR, ROL, ROR, INC, DEC, SLO, SRE, RLA, RRA, ISB, DCP /// /// ```text /// # address R/W description /// --- --------- --- --------------------------------------------- /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch address, increment PC /// 3 address R read from address, add index register X to it /// 4 address+X* R read from effective address /// 5 address+X* W write the value back to effective address, /// and do the operation on it /// 6 address+X* W write the new value to effective address /// /// * The high byte of the effective address is always zero, /// i.e. page boundary crossings are not handled. /// ``` /// /// # Write instructions /// /// STA, STX, STY, SAX /// /// ```text /// # address R/W description /// --- --------- --- ------------------------------------------- /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch address, increment PC /// 3 address R read from address, add index register to it /// 4 address+X* W write to effective address /// /// * The high byte of the effective address is always zero, /// i.e. page boundary crossings are not handled. /// ``` #[inline(always)] pub fn zpx(&mut self) -> u16 { let addr = u16::from(self.fetch_byte()); // Cycle 2 self.read(addr); // Cycle 3, dummy read // High byte is always zero addr.wrapping_add(u16::from(self.x)) & 0x00FF } /// Zero Page Addressing w/ Y offset. /// /// Same as Zero Page, but is offset by adding the y register. /// /// # Read instructions /// /// LDX, LAX /// /// ```text /// # address R/W description /// --- --------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch address, increment PC /// 3 address R read from address, add index register to it /// 4 address+Y* R read from effective address /// /// * The high byte of the effective address is always zero, /// i.e. page boundary crossings are not handled. /// ``` /// /// # Write instructions /// /// STX, SAX /// /// ```text /// # address R/W description /// --- --------- --- ------------------------------------------- /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch address, increment PC /// 3 address R read from address, add index register to it /// 4 address+Y* W write to effective address /// /// * The high byte of the effective address is always zero, /// i.e. page boundary crossings are not handled. /// ``` #[inline(always)] pub fn zpy(&mut self) -> u16 { let addr = u16::from(self.fetch_byte()); // Cycle 2 self.read(addr); // Cycle 3, dummy read // High byte is always zero addr.wrapping_add(u16::from(self.y)) & 0x00FF } /// Absolute Addressing. /// /// Uses a full 16-bit address as the next value. /// /// # Read instructions /// /// LDA, LDX, LDY, EOR, AND, ORA, ADC, SBC, CMP, BIT, LAX, NOP /// /// ```text /// # address R/W description /// --- ------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch low byte of address, increment PC /// 3 PC R fetch high byte of address, increment PC /// 4 address R read from effective address /// ``` /// /// # Read-Modify-Write instructions /// /// ASL, LSR, ROL, ROR, INC, DEC, SLO, SRE, RLA, RRA, ISB, DCP /// /// ```text /// # address R/W description /// --- ------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch low byte of address, increment PC /// 3 PC R fetch high byte of address, increment PC /// 4 address R read from effective address /// 5 address W write the value back to effective address, /// and do the operation on it /// 6 address W write the new value to effective address /// ``` /// /// # Write instructions /// /// STA, STX, STY, SAX /// /// ```text /// # address R/W description /// --- ------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch low byte of address, increment PC /// 3 PC R fetch high byte of address, increment PC /// 4 address W write register to effective address /// ``` #[inline(always)] pub fn abs(&mut self) -> u16 { self.fetch_word() // Cycles 2-3 } /// Absolute Address w/ X offset. /// /// Same as Absolute, but is offset by adding the x register. If a page boundary is crossed, an /// additional clock is required. /// /// # Read instructions /// /// LDA, LDX, LDY, EOR, AND, ORA, ADC, SBC, CMP, BIT, LAX, LAE, SHS, NOP /// /// ```text /// # address R/W description /// --- --------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch low byte of address, increment PC /// 3 PC R fetch high byte of address, /// add index register to low address byte, /// increment PC /// 4 address+X* R read from effective address, /// fix the high byte of effective address /// 5+ address+X R re-read from effective address /// /// * The high byte of the effective address may be invalid /// at this time, i.e. it may be smaller by $100. /// + This cycle will be executed only if the effective address /// was invalid during cycle #4, i.e. page boundary was crossed. /// ``` /// /// # Read-Modify-Write instructions /// /// ASL, LSR, ROL, ROR, INC, DEC, SLO, SRE, RLA, RRA, ISB, DCP /// /// ```text /// # address R/W description /// -- --------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch low byte of address, increment PC /// 3 PC R fetch high byte of address, /// add index register X to low address byte, /// increment PC /// 4 address+X* R read from effective address, /// fix the high byte of effective address /// 5 address+X R re-read from effective address /// 6 address+X W write the value back to effective address, /// and do the operation on it /// 7 address+X W write the new value to effective address /// /// * The high byte of the effective address may be invalid /// at this time, i.e. it may be smaller by $100. /// ``` /// /// # Write instructions /// /// STA, STX, STY, SHA, SHX, SHY /// /// ```text /// # address R/W description /// -- --------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch low byte of address, increment PC /// 3 PC R fetch high byte of address, /// add index register to low address byte, /// increment PC /// 4 address+X* R read from effective address, /// fix the high byte of effective address /// 5 address+X W write to effective address /// /// * The high byte of the effective address may be invalid /// at this time, i.e. it may be smaller by $100. Because /// the processor cannot undo a write to an invalid /// address, it always reads from the address first. /// ``` #[inline(always)] pub fn abx(&mut self, dummy_read: bool) -> u16 { let base_addr = self.fetch_word(); // Cycles 2-3 let addr = base_addr.wrapping_add(u16::from(self.x)); if Cpu::pages_differ(base_addr, addr) || dummy_read { // Cycle 4 dummy read with fixed high byte self.read((base_addr & 0xFF00) | (addr & 0x00FF)); } addr } /// Absolute Address w/ Y offset. /// /// Same as Absolute, but is offset by adding the y register. If a page boundary is crossed, an /// additional clock is required. /// /// # Read instructions /// /// LDA, LDX, LDY, EOR, AND, ORA, ADC, SBC, CMP, BIT, LAX, LAE, SHS, NOP /// /// ```text /// # address R/W description /// --- --------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch low byte of address, increment PC /// 3 PC R fetch high byte of address, /// add index register to low address byte, /// increment PC /// 4 address+Y* R read from effective address, /// fix the high byte of effective address /// 5+ address+Y R re-read from effective address /// /// * The high byte of the effective address may be invalid /// at this time, i.e. it may be smaller by $100. /// + This cycle will be executed only if the effective address /// was invalid during cycle #4, i.e. page boundary was crossed. /// ``` /// /// # Read-Modify-Write instructions /// /// ASL, LSR, ROL, ROR, INC, DEC, SLO, SRE, RLA, RRA, ISB, DCP /// /// ```text /// # address R/W description /// --- --------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch low byte of address, increment PC /// 3 PC R fetch high byte of address, /// add index register Y to low address byte, /// increment PC /// 4 address+Y* R read from effective address, /// fix the high byte of effective address /// 5 address+Y R re-read from effective address /// 6 address+Y W write the value back to effective address, /// and do the operation on it /// 7 address+Y W write the new value to effective address /// /// * The high byte of the effective address may be invalid /// at this time, i.e. it may be smaller by $100. /// ``` /// /// # Write instructions /// /// STA, STX, STY, SHA, SHX, SHY /// /// ```text /// # address R/W description /// --- --------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch low byte of address, increment PC /// 3 PC R fetch high byte of address, /// add index register to low address byte, /// increment PC /// 4 address+Y* R read from effective address, /// fix the high byte of effective address /// 5 address+Y W write to effective address /// /// * The high byte of the effective address may be invalid /// at this time, i.e. it may be smaller by $100. Because /// the processor cannot undo a write to an invalid /// address, it always reads from the address first. /// ``` #[inline(always)] pub fn aby(&mut self, dummy_read: bool) -> u16 { let base_addr = self.fetch_word(); // Cycles 2 & 3 let addr = base_addr.wrapping_add(u16::from(self.y)); if Cpu::pages_differ(base_addr, addr) || dummy_read { // Cycle 4 dummy read with fixed high byte self.read((base_addr & 0xFF00) | (addr & 0x00FF)); } addr } /// Indirect Addressing. /// /// The next 16-bit address is used to get the actual 16-bit address. This instruction has /// a bug in the original hardware. If the lo byte is 0xFF, the hi byte would cross a page /// boundary. However, this doesn't work correctly on the original hardware and instead /// wraps back around to 0. /// /// # Instructions /// /// JMP /// /// ```text /// # address R/W description /// --- --------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch pointer address low, increment PC /// 3 PC R fetch pointer address high, increment PC /// 4 pointer R fetch low address to latch /// 5 pointer+1* R fetch PCH, copy latch to PCL /// /// * The PCH will always be fetched from the same page /// than PCL, i.e. page boundary crossing is not handled. /// ``` #[inline(always)] pub fn ind(&mut self) -> u16 { self.fetch_word() } /// Indirect X Addressing. /// /// The next 8-bit address is offset by the X register to get the actual 16-bit address from /// page 0x00. /// /// # Read instructions /// /// LDA, ORA, EOR, AND, ADC, CMP, SBC, LAX /// /// ```text /// # address R/W description /// --- ----------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch pointer address, increment PC /// 3 pointer R read from the address, add X to it /// 4 pointer+X* R fetch effective address low /// 5 pointer+X+1* R fetch effective address high /// 6 address R read from effective address /// /// * The effective address is always fetched from zero page, /// i.e. the zero page boundary crossing is not handled. /// ``` /// /// # Read-Modify-Write instructions /// /// SLO, SRE, RLA, RRA, ISB, DCP /// /// ```text /// # address R/W description /// --- ----------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch pointer address, increment PC /// 3 pointer R read from the address, add X to it /// 4 pointer+X* R fetch effective address low /// 5 pointer+X+1* R fetch effective address high /// 6 address R read from effective address /// 7 address W write the value back to effective address, /// and do the operation on it /// 8 address W write the new value to effective address /// /// * The effective address is always fetched from zero page, /// i.e. the zero page boundary crossing is not handled. /// ``` /// /// # Write instructions /// /// STA, SAX /// /// ```text /// # address R/W description /// --- ----------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch pointer address, increment PC /// 3 pointer R read from the address, add X to it /// 4 pointer+X* R fetch effective address low /// 5 pointer+X+1* R fetch effective address high /// 6 address W write to effective address /// /// * The effective address is always fetched from zero page, /// i.e. the zero page boundary crossing is not handled. /// ``` #[inline(always)] pub fn idx(&mut self) -> u16 { let mut zero_addr = self.fetch_byte(); // Cycle 2 self.read(u16::from(zero_addr)); // Cycle 3 dummy read zero_addr = zero_addr.wrapping_add(self.x); let lo = self.read(u16::from(zero_addr)); // Cycle 4 let hi = self.read(u16::from(zero_addr.wrapping_add(1))); // Cycle 5 u16::from_le_bytes([lo, hi]) } /// Indirect Y Addressing. /// /// The next 8-bit address is read to get a 16-bit address from page 0x00, which is then offset /// by the Y register. If a page boundary is crossed, add a clock cycle. /// /// # Read instructions /// /// LDA, EOR, AND, ORA, ADC, SBC, CMP /// /// ```text /// # address R/W description /// --- ----------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch pointer address, increment PC /// 3 pointer R fetch effective address low /// 4 pointer+1* R fetch effective address high, /// add Y to low byte of effective address /// 5 address+Y+ R read from effective address, /// fix high byte of effective address /// 6! address+Y R read from effective address /// /// * The effective address is always fetched from zero page, /// i.e. the zero page boundary crossing is not handled. /// + The high byte of the effective address may be invalid /// at this time, i.e. it may be smaller by $100. /// ! This cycle will be executed only if the effective address /// was invalid during cycle #5, i.e. page boundary was crossed. /// ``` /// /// # Read-Modify-Write instructions /// /// SLO, SRE, RLA, RRA, ISB, DCP /// /// ```text /// # address R/W description /// --- ----------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch pointer address, increment PC /// 3 pointer R fetch effective address low /// 4 pointer+1* R fetch effective address high, /// add Y to low byte of effective address /// 5 address+Y+ R read from effective address, /// fix high byte of effective address /// 6 address+Y R re-read from effective address /// 7 address+Y W write the value back to effective address, /// and do the operation on it /// 8 address+Y W write the new value to effective address /// /// * The effective address is always fetched from zero page, /// i.e. the zero page boundary crossing is not handled. /// + The high byte of the effective address may be invalid /// at this time, i.e. it may be smaller by $100. /// ``` /// /// # Write instructions /// /// STA, SHA /// /// ```text /// # address R/W description /// --- ----------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch pointer address, increment PC /// 3 pointer R fetch effective address low /// 4 pointer+1* R fetch effective address high, /// add Y to low byte of effective address /// 5 address+Y+ R read from effective address, /// fix high byte of effective address /// 6 address+Y W write to effective address /// /// * The effective address is always fetched from zero page, /// i.e. the zero page boundary crossing is not handled. /// + The high byte of the effective address may be invalid /// at this time, i.e. it may be smaller by $100. /// ``` #[inline(always)] pub fn idy(&mut self, dummy_read: bool) -> u16 { let zero_addr = self.fetch_byte(); // Cycle 2 let base_addr = { let lo = self.read(u16::from(zero_addr)); // Cycle 4 let hi = self.read(u16::from(zero_addr.wrapping_add(1))); // Cycle 5 u16::from_le_bytes([lo, hi]) }; let addr = base_addr.wrapping_add(u16::from(self.y)); if Cpu::pages_differ(base_addr, addr) || dummy_read { // Cycle 5 dummy read with fixed high byte self.read((base_addr & 0xFF00) | (addr & 0x00FF)); } addr } } /// CPU instructions impl Cpu { // Storage opcodes /// LDA: Load A with M #[inline(always)] pub fn lda(&mut self) { let val = self.read_operand(); self.set_acc(val); } /// LDX: Load X with M #[inline(always)] pub fn ldx(&mut self) { let val = self.read_operand(); self.set_x(val); } /// LDY: Load Y with M #[inline(always)] pub fn ldy(&mut self) { let val = self.read_operand(); self.set_y(val); } /// STA: Store A into M #[inline(always)] pub fn sta(&mut self) { self.write(self.operand, self.acc); } /// STX: Store X into M #[inline(always)] pub fn stx(&mut self) { self.write(self.operand, self.x); } /// STY: Store Y into M #[inline(always)] pub fn sty(&mut self) { self.write(self.operand, self.y); } /// TAX: Transfer A to X #[inline(always)] pub fn tax(&mut self) { self.set_x(self.acc); } /// TAY: Transfer A to Y #[inline(always)] pub fn tay(&mut self) { self.set_y(self.acc); } /// TSX: Transfer Stack Pointer to X #[inline(always)] pub fn tsx(&mut self) { self.set_x(self.sp); } /// TXA: Transfer X to A #[inline(always)] pub fn txa(&mut self) { self.set_acc(self.x); } /// TXS: Transfer X to Stack Pointer #[inline(always)] pub const fn txs(&mut self) { self.set_sp(self.x); } /// TYA: Transfer Y to A #[inline(always)] pub fn tya(&mut self) { self.set_acc(self.y); } // Arithmetic opcodes /// ADC: Add M to A with Carry #[inline(always)] pub fn adc(&mut self) { let val = self.read_operand(); self.add(val); } /// SBC: Subtract M from A with Carry #[inline(always)] pub fn sbc(&mut self) { let val = self.read_operand(); self.add(val ^ 0xFF); } /// Utility function used by all add instructions #[inline(always)] fn add(&mut self, val: u8) { let a = u16::from(self.acc); let val = u16::from(val); let carry = u16::from(self.status_bit(Status::C)); let res = a + val + carry; self.set_zn_status(res as u8); self.status .set(Status::V, (a ^ val) & 0x80 == 0 && (a ^ res) & 0x80 != 0); self.status.set(Status::C, res > 0xFF); self.set_acc(res as u8); } /// INC: Increment M by One #[inline(always)] pub fn inc(&mut self) { let addr = self.operand; let val = self.read(addr); self.write(addr, val); // Dummy write let res = val.wrapping_add(1); self.write(addr, res); self.set_zn_status(res); } /// DEC: Decrement M by One #[inline(always)] pub fn dec(&mut self) { let addr = self.operand; let val = self.read(addr); self.write(addr, val); // Dummy write let res = val.wrapping_sub(1); self.write(addr, res); self.set_zn_status(res); } /// INX: Increment X by One #[inline(always)] pub fn inx(&mut self) { self.set_x(self.x.wrapping_add(1)); } /// INY: Increment Y by One #[inline(always)] pub fn iny(&mut self) { self.set_y(self.y.wrapping_add(1)); } /// DEX: Decrement X by One #[inline(always)] pub fn dex(&mut self) { self.set_x(self.x.wrapping_sub(1)); } /// DEY: Decrement Y by One #[inline(always)] pub fn dey(&mut self) { self.set_y(self.y.wrapping_sub(1)); } // Bitwise opcodes /// AND: "And" M with A #[inline(always)] pub fn and(&mut self) { let val = self.read_operand(); self.set_acc(self.acc & val); } /// EOR: "Exclusive-Or" M with A #[inline(always)] pub fn eor(&mut self) { let val = self.read_operand(); self.set_acc(self.acc ^ val); } /// ORA: "OR" M with A #[inline(always)] pub fn ora(&mut self) { let val = self.read_operand(); self.set_acc(self.acc | val); } /// ASL: Shift Left One Bit (A) #[inline(always)] fn asla(&mut self) { let val = self.asl(self.acc); self.set_acc(val); } /// ASL: Shift Left One Bit (M) #[inline(always)] fn aslm(&mut self) { let addr = self.operand; let val = self.read(addr); self.write(addr, val); // Dummy write let res = self.asl(val); self.write(addr, res); } /// Utility function used by all ASL instructions #[inline(always)] fn asl(&mut self, val: u8) -> u8 { self.status.set(Status::C, (val & 0x80) > 0); let res = val.wrapping_shl(1); self.set_zn_status(res); res } /// LSR: Shift Right One Bit (A) #[inline(always)] pub fn lsra(&mut self) { let res = self.lsr(self.acc); self.set_acc(res); } /// LSR: Shift Right One Bit (M) #[inline(always)] pub fn lsrm(&mut self) { let addr = self.operand; let val = self.read(addr); self.write(addr, val); // Dummy write let res = self.lsr(val); self.write(addr, res); } /// Utility function used by all LSR instructions #[inline(always)] fn lsr(&mut self, val: u8) -> u8 { self.status.set(Status::C, (val & 1) > 0); let res = val.wrapping_shr(1); self.set_zn_status(res); res } /// ROL: Rotate One Bit Left (A) #[inline(always)] pub fn rola(&mut self) { let val = self.rol(self.acc); self.set_acc(val); } /// ROL: Rotate One Bit Left (M) #[inline(always)] pub fn rolm(&mut self) { let addr = self.operand; let val = self.read(addr); self.write(addr, val); // Dummy write let val = self.rol(val); self.write(addr, val); } /// Utility function used by all ROL instructions #[inline(always)] pub fn rol(&mut self, val: u8) -> u8 { let carry = self.status_bit(Status::C); self.status.set(Status::C, (val & 0x80) > 0); let res = (val << 1) | carry; self.set_zn_status(res); res } /// ROR: Rotate One Bit Right (A) #[inline(always)] pub fn rora(&mut self) { let val = self.ror(self.acc); self.set_acc(val); } /// ROR: Rotate One Bit Right (M) #[inline(always)] pub fn rorm(&mut self) { let addr = self.operand; let val = self.read(addr); self.write(addr, val); // Dummy write let val = self.ror(val); self.write(addr, val); } /// Utility function used by all ROR instructions #[inline(always)] fn ror(&mut self, val: u8) -> u8 { let carry = self.status_bit(Status::C); self.status.set(Status::C, (val & 1) > 0); let res = (val >> 1) | (carry << 7); self.set_zn_status(res); res } /// BIT: Test Bits in M with A #[inline(always)] pub fn bit(&mut self) { let val = self.read_operand(); self.status.set(Status::Z, (self.acc & val) == 0); self.status.set(Status::N, (val & 0x80) > 0); self.status.set(Status::V, (val & 0x40) > 0); } // Branch opcodes /// BCC: Branch on Carry Clear #[inline(always)] pub fn bcc(&mut self) { self.branch(!self.status.contains(Status::C)); } /// BCS: Branch on Carry Set #[inline(always)] pub fn bcs(&mut self) { self.branch(self.status.contains(Status::C)); } /// BEQ: Branch on Result Zero #[inline(always)] pub fn beq(&mut self) { self.branch(self.status.contains(Status::Z)); } /// BMI: Branch on Result Negative #[inline(always)] pub fn bmi(&mut self) { self.branch(self.status.contains(Status::N)); } /// BNE: Branch on Result Not Zero #[inline(always)] pub fn bne(&mut self) { self.branch(!self.status.contains(Status::Z)); } /// BPL: Branch on Result Positive #[inline(always)] pub fn bpl(&mut self) { self.branch(!self.status.contains(Status::N)); } /// BVC: Branch on Overflow Clear #[inline(always)] pub fn bvc(&mut self) { self.branch(!self.status.contains(Status::V)); } /// BVS: Branch on Overflow Set #[inline(always)] pub fn bvs(&mut self) { self.branch(self.status.contains(Status::V)); } /// Utility function used by all branch instructions. #[inline(always)] fn branch(&mut self, branch: bool) { if !branch { return; } // If an interrupt occurs during the final cycle of a non-pagecrossing branch // then it will be ignored until the next instruction completes let run_irq = self.irq_flags.contains(IrqFlags::RUN_IRQ); let prev_run_irq = self.irq_flags.contains(IrqFlags::PREV_RUN_IRQ); if run_irq && !prev_run_irq { self.irq_flags.remove(IrqFlags::RUN_IRQ); } self.read(self.pc); // Dummy read let offset = i16::from(self.operand as i8); if Self::page_crossed(self.pc, offset) { self.read(self.pc); // Dummy read } self.pc = (self.pc as i16).wrapping_add(offset) as u16; } // Jump opcodes /// JMP: Jump to Location (absolute) /// /// ```text /// # address R/W description /// --- ------- --- ------------------------------------------------- /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch low address byte, increment PC /// 3 PC R copy low address byte to PCL, copy high address /// byte to PCH /// ``` #[inline(always)] pub const fn jmpa(&mut self) { self.pc = self.operand; } /// JMP: Jump to Location (indirect) /// ```text /// # address R/W description /// --- --------- --- ------------------------------------------ /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch pointer address low, increment PC /// 3 PC R fetch pointer address high, increment PC /// 4 pointer R fetch low address to latch /// 5 pointer+1* R fetch PCH, copy latch to PCL /// /// * The PCH will always be fetched from the same page /// than PCL, i.e. page boundary crossing is not handled. /// ``` #[inline(always)] pub fn jmpi(&mut self) { let addr = self.operand; self.pc = if (addr & 0xFF) == 0xFF { let lo = self.read(addr); let hi = self.read(addr - 0xFF); u16::from_le_bytes([lo, hi]) } else { self.read_word(addr) }; } /// JSR: Jump to Location Save Return addr /// /// ```text /// # address R/W description /// --- ------- --- ------------------------------------------------- /// 1 PC R fetch opcode, increment PC /// 2 PC R fetch low address byte, increment PC /// 3 $0100,S R internal operation (predecrement S?) /// 4 $0100,S W push PCH on stack, decrement S /// 5 $0100,S W push PCL on stack, decrement S /// 6 PC R copy low address byte to PCL, copy high address /// byte to PCH /// ``` #[inline(always)] pub fn jsr(&mut self) { let lo = self.fetch_byte(); self.read(self.pc); // Dummy read self.push_word(self.pc); let hi = self.fetch_byte(); let addr = u16::from_le_bytes([lo, hi]); self.pc = addr; } /// RTI: Return from Interrupt /// /// ```text /// # address R/W description /// --- ------- --- ----------------------------------------------- /// 1 PC R fetch opcode, increment PC /// 2 PC R read next instruction byte (and throw it away) /// 3 $0100,S R increment S /// 4 $0100,S R pull P from stack, increment S /// 5 $0100,S R pull PCL from stack, increment S /// 6 $0100,S R pull PCH from stack /// ``` #[inline(always)] pub fn rti(&mut self) { self.read(self.pc); // Dummy read let status = Status::from_bits_truncate(self.pop_byte()); self.set_status(status); self.pc = self.pop_word(); } /// RTS: Return from Subroutine /// /// ```text /// # address R/W description /// --- ------- --- ----------------------------------------------- /// 1 PC R fetch opcode, increment PC /// 2 PC R read next instruction byte (and throw it away) /// 3 $0100,S R increment S /// 4 $0100,S R pull PCL from stack, increment S /// 5 $0100,S R pull PCH from stack /// 6 PC R increment PC /// ``` #[inline(always)] pub fn rts(&mut self) { self.read(self.pc); // Dummy read let addr = self.pop_word(); self.read(self.pc); // Dummy read self.pc = addr.wrapping_add(1); } // Register opcodes /// CLC: Clear Carry Flag #[inline(always)] pub fn clc(&mut self) { self.status.set(Status::C, false); } /// SEC: Set Carry Flag #[inline(always)] pub fn sec(&mut self) { self.status.set(Status::C, true); } /// CLD: Clear Decimal Mode #[inline(always)] pub fn cld(&mut self) { self.status.set(Status::D, false); } /// SED: Set Decimal Mode #[inline(always)] pub fn sed(&mut self) { self.status.set(Status::D, true); } /// CLI: Clear Interrupt Disable Bit #[inline(always)] pub fn cli(&mut self) { self.status.set(Status::I, false); } /// SEI: Set Interrupt Disable Status #[inline(always)] pub fn sei(&mut self) { self.status.set(Status::I, true); } /// CLV: Clear Overflow Flag #[inline(always)] pub fn clv(&mut self) { self.status.set(Status::V, false); } // Compare opcodes /// CMP: Compare M and A #[inline(always)] pub fn cpa(&mut self) { let val = self.read_operand(); self.cmp(self.acc, val); } /// CPX: Compare M and X #[inline(always)] pub fn cpx(&mut self) { let val = self.read_operand(); self.cmp(self.x, val); } /// CPY: Compare M and Y #[inline(always)] pub fn cpy(&mut self) { let val = self.read_operand(); self.cmp(self.y, val); } /// Utility function used by all compare instructions #[inline(always)] fn cmp(&mut self, reg: u8, val: u8) { let result = reg.wrapping_sub(val); self.status.set(Status::C, reg >= val); self.set_zn_status(result); } // Stack opcodes /// PHP: Push Processor Status on Stack /// /// ```text /// # address R/W description /// --- ------- --- ----------------------------------------------- /// 1 PC R fetch opcode, increment PC /// 2 PC R read next instruction byte (and throw it away) /// 3 $0100,S W push register on stack, decrement S /// ``` #[inline(always)] pub fn php(&mut self) { // Set U and B when pushing during PHP and BRK self.push_byte((self.status | Status::U | Status::B).bits()); } /// PLP: Pull Processor Status from Stack /// /// ```text /// # address R/W description /// --- ------- --- ----------------------------------------------- /// 1 PC R fetch opcode, increment PC /// 2 PC R read next instruction byte (and throw it away) /// 3 $0100,S R increment S /// 4 $0100,S R pull register from stack /// ``` #[inline(always)] pub fn plp(&mut self) { self.read(self.pc); // Dummy read let status = Status::from_bits_truncate(self.pop_byte()); self.set_status(status); } /// PHA: Push A on Stack /// /// ```text /// # address R/W description /// --- ------- --- ----------------------------------------------- /// 1 PC R fetch opcode, increment PC /// 2 PC R read next instruction byte (and throw it away) /// 3 $0100,S W push register on stack, decrement S /// ``` #[inline(always)] pub fn pha(&mut self) { self.push_byte(self.acc); // Cycle 3 } /// PLA: Pull A from Stack /// /// ```text /// # address R/W description /// --- ------- --- ----------------------------------------------- /// 1 PC R fetch opcode, increment PC /// 2 PC R read next instruction byte (and throw it away) /// 3 $0100,S R increment S /// 4 $0100,S R pull register from stack /// ``` #[inline(always)] pub fn pla(&mut self) { self.read(Self::SP_BASE | u16::from(self.sp)); // Dummy read self.acc = self.pop_byte(); // Cycle 4 self.set_zn_status(self.acc); } // System opcodes /// BRK: Force Break Interrupt /// /// ```text /// # address R/W description /// --- ------- --- ----------------------------------------------- /// 1 PC R fetch opcode, increment PC /// 2 PC R read next instruction byte (and throw it away), /// increment PC /// 3 $0100,S W push PCH on stack (with B flag set), decrement S /// 4 $0100,S W push PCL on stack, decrement S /// 5 $0100,S W push P on stack, decrement S /// 6 $FFFE R fetch PCL /// 7 $FFFF R fetch PCH /// ``` #[inline(always)] pub fn brk(&mut self) { self.push_word(self.pc); // Pushing status to the stack has to happen after checking NMI since it can hijack the BRK // IRQ when it occurs between cycles 4 and 5. // https://www.nesdev.org/wiki/CPU_interrupts#Interrupt_hijacking // // Set U and B when pushing during PHP and BRK let status = (self.status | Status::U | Status::B).bits(); let nmi = self.irq_flags.contains(IrqFlags::NMI); self.push_byte(status); // Cycle 5 self.status.set(Status::I, true); if nmi { self.irq_flags.remove(IrqFlags::NMI); self.pc = self.read_word(Self::NMI_VECTOR); // Cycles 6-7 tracing::trace!( "NMI - PPU:{:3},{:3} CYC:{}", self.bus.ppu.cycle, self.bus.ppu.scanline, self.cycle ); } else { self.pc = self.read_word(Self::IRQ_VECTOR); // Cycles 6-7 tracing::trace!( "IRQ - PPU:{:3},{:3} CYC:{}", self.bus.ppu.cycle, self.bus.ppu.scanline, self.cycle ); } // Prevent NMI from triggering immediately after BRK tracing::trace!( "Suppress NMI after BRK - PPU:{:3},{:3} CYC:{}, prev_nmi:{}", self.bus.ppu.cycle, self.bus.ppu.scanline, self.cycle, self.irq_flags.contains(IrqFlags::PREV_NMI) ); self.irq_flags.remove(IrqFlags::PREV_NMI); } /// NOP: No Operation #[inline(always)] pub fn nop(&mut self) { let _ = self.read_operand(); } // Unofficial opcodes /// HLT: Captures all unimplemented opcodes and halts CPU #[inline(always)] pub fn hlt(&mut self) { // Freezes CPU by rewiding and re-executing the bad opcode. self.pc = self.pc.wrapping_sub(1); // Prevent IRQ/NMI self.clear_irq_flags(IrqFlags::PREV_RUN_IRQ | IrqFlags::PREV_NMI); self.corrupted = true; let opcode = usize::from(self.peek(self.pc.wrapping_sub(1))); let instr = Cpu::INSTR_REF[opcode]; tracing::error!( "Invalid opcode ${opcode:02X} {:?} #{:?} encountered!", instr.instr, instr.addr_mode, ); } /// ISC/ISB: Shortcut for INC then SBC #[inline(always)] pub fn isb(&mut self) { let val = self.read_operand(); let addr = self.operand; // INC self.write(addr, val); // Dummy write let val = val.wrapping_add(1); // SBC self.add(val ^ 0xFF); self.write(addr, val); } /// DCP: Shortcut for DEC then CMP #[inline(always)] pub fn dcp(&mut self) { let val = self.read_operand(); let addr = self.operand; // DEC self.write(addr, val); // Dummy write let val = val.wrapping_sub(1); // CMP self.cmp(self.acc, val); self.write(addr, val); } /// ATX: Shortcut for LDA & TAX #[inline(always)] pub fn atx(&mut self) { let val = self.read_operand(); self.set_acc(val); // LDA self.set_x(self.acc); // TAX } /// AXS: A & X into X #[inline(always)] pub fn axs(&mut self) { let val = self.read_operand(); // CMP & DEX let res = (self.acc & self.x).wrapping_sub(val); self.status.set(Status::C, (self.acc & self.x) >= val); self.set_x(res); } /// LAS: Shortcut for LDA then TSX, but ANDs memory stack pointer #[inline(always)] pub fn las(&mut self) { let val = self.read_operand(); self.set_acc(val & self.sp); self.set_x(self.acc); self.set_sp(self.acc); } /// LAX: Shortcut for LDA then TAX #[inline(always)] pub fn lax(&mut self) { let val = self.read_operand(); self.set_x(val); self.set_acc(val); } /// SYA/A11/SHY/SAY/TEY: Combinations of STA/STX/STY /// AND Y register with the high byte of the target address of the argument + 1. Store the /// result in memory. #[inline(always)] pub fn sya(&mut self) { let base_addr = self.fetch_word(); self.sya_sxa_axa(base_addr, self.x, self.y); } /// SXA/SHX/XAS: AND X with the high byte of the target address + 1 #[inline(always)] pub fn sxa(&mut self) { let base_addr = self.fetch_word(); self.sya_sxa_axa(base_addr, self.y, self.x); } /// SHA/AXA: AND X with A then AND with 7, then store in memory #[inline(always)] pub fn shaa(&mut self) { let base_addr = self.fetch_word(); self.sya_sxa_axa(base_addr, self.y, self.x & self.acc); } /// AHX: And X with A stores A&X&H into {adr} #[inline(always)] pub fn shaz(&mut self) { let zero_addr = self.fetch_byte(); let base_addr = { let lo = self.read(u16::from(zero_addr)); let hi = self.read(u16::from(zero_addr.wrapping_add(1))); u16::from_le_bytes([lo, hi]) }; self.sya_sxa_axa(base_addr, self.y, self.x & self.acc); } fn sya_sxa_axa(&mut self, base_addr: u16, index_reg: u8, val_reg: u8) { let addr = base_addr.wrapping_add(u16::from(index_reg)); let page_crossed = Cpu::pages_differ(base_addr, addr); let start_cycles = self.cycle; // Dummy read with fixed high byte self.read((base_addr & 0xFF00) | (addr & 0x00FF)); // Dummy read took more than 1 cycle, so it was interrupted by a DMA let had_dma = (self.cycle - start_cycles) > 1; let mut hi = (addr >> 8) as u8; let lo = (addr & 0xFF) as u8; if page_crossed { hi &= val_reg; } let val = if had_dma { val_reg } else { val_reg & ((base_addr >> 8) + 1) as u8 }; self.write(u16::from_le_bytes([lo, hi]), val); } /// SAX: AND A with X #[inline(always)] pub fn sax(&mut self) { self.write(self.operand, self.acc & self.x); } /// XXA: Shortcutr for TXA with AND #[inline(always)] pub fn xaa(&mut self) { let val = self.read_operand(); self.set_acc((self.acc | 0xEE) & self.x & val); } /// RRA: Shortcut for ROR then ADC #[inline(always)] pub fn rra(&mut self) { let val = self.read_operand(); let addr = self.operand; // ROR self.write(addr, val); // Dummy write let shifted_val = self.ror(val); // ADC self.add(shifted_val); self.write(addr, shifted_val); } /// TAS: Shortcut for STA then TXS, Same as SHA but sets SP = A & X #[inline(always)] pub fn tas(&mut self) { self.shaa(); // TXS self.set_sp(self.x & self.acc); } /// ARR: Shortcut for AND #imm then ROR, but sets flags differently /// C is bit 6 and V is bit 6 xor bit 5 #[inline(always)] pub fn arr(&mut self) { let val = self.read_operand(); let carry = self.status_bit(Status::C); self.set_acc(((self.acc & val) >> 1) | (carry << 7)); self.status.set(Status::C, (self.acc & 0x40) > 0); self.status.set( Status::V, (self.status_bit(Status::C) ^ (self.acc >> 5) & 0x01) > 0, ); } /// SRA: Shortcut for LSR then EOR #[inline(always)] pub fn sre(&mut self) { let val = self.read_operand(); let addr = self.operand; // LSR self.write(addr, val); // Dummy write let shifted_val = self.lsr(val); // EOR self.set_acc(self.acc ^ shifted_val); self.write(addr, shifted_val); } /// ALR/ASR: Shortcut for AND #imm then LSR #[inline(always)] pub fn alr(&mut self) { let val = self.read_operand(); self.set_acc(self.acc & val); self.status.set(Status::C, (self.acc & 0x01) > 0); self.set_acc(self.acc >> 1); } /// RLA: Shortcut for ROL then AND #[inline(always)] pub fn rla(&mut self) { let val = self.read_operand(); let addr = self.operand; // ROL self.write(addr, val); // Dummy write let shifted_val = self.rol(val); // AND self.set_acc(self.acc & shifted_val); self.write(addr, shifted_val); } /// ANC/AAC: AND #imm but puts bit 7 into carry as if ASL was executed #[inline(always)] pub fn anc(&mut self) { let val = self.read_operand(); self.set_acc(self.acc & val); self.status.set(Status::C, self.status.contains(Status::N)); } /// SLO: Shortcut for ASL then ORA #[inline(always)] pub fn slo(&mut self) { let val = self.read_operand(); let addr = self.operand; // ASL self.write(addr, val); // Dummy write let shifted_val = self.asl(val); // ORA self.set_acc(self.acc | shifted_val); self.write(addr, shifted_val); } } ================================================ FILE: tetanes-core/src/cpu.rs ================================================ //! 6502 Central Processing Unit (CPU) implementation. //! //! use crate::{ bus::Bus, common::{Clock, NesRegion, Regional, Reset, ResetKind}, mem::{Read, Write}, }; use crate::{ cpu::instr::{ AddrMode, Instr::{JMP, JSR}, InstrRef, }, mapper::Map, }; use bitflags::bitflags; use serde::{Deserialize, Serialize}; use std::fmt::{self}; use tracing::trace; pub mod instr; bitflags! { #[derive(Default, Serialize, Deserialize, Debug, Copy, Clone)] #[must_use] pub struct IrqFlags: u8 { const NMI = 1 << 0; const PREV_NMI = 1 << 1; const PREV_NMI_PENDING = 1 << 2; const RUN_IRQ = 1 << 3; const PREV_RUN_IRQ = 1 << 4; const DMA_DMC = 1 << 5; const DMA_HALT = 1 << 6; const DMA_DUMMY_READ = 1 << 7; } } // Status Registers // https://wiki.nesdev.org/w/index.php/Status_flags // 7654 3210 // NVUB DIZC // |||| |||| // |||| |||+- Carry // |||| ||+-- Zero // |||| |+--- Interrupt Disable // |||| +---- Decimal Mode - Not used in the NES but still has to function // |||+------ Break - 1 when pushed to stack from PHP/BRK, 0 from IRQ/NMI // ||+------- Unused - always set to 1 when pushed to stack // |+-------- Overflow // +--------- Negative bitflags! { /// CPU Status Registers. #[derive(Default, Serialize, Deserialize, Debug, Copy, Clone)] #[must_use] pub struct Status: u8 { const C = 1; // Carry const Z = 1 << 1; // Zero const I = 1 << 2; // Disable Interrupt const D = 1 << 3; // Decimal Mode const B = 1 << 4; // Break const U = 1 << 5; // Unused const V = 1 << 6; // Overflow const N = 1 << 7; // Negative } } /// The Central Processing Unit status and registers #[derive(Default, Clone, Serialize, Deserialize)] #[must_use] #[repr(C)] pub struct Cpu { pub cycle: u32, // total number of cycles ran pub master_clock: u32, // start/end cycle counts for reads/writes pub start_cycles: u8, pub end_cycles: u8, pub pc: u16, // program counter pub operand: u16, // opcode operand pub addr_mode: AddrMode, // Addressing mode pub sp: u8, // stack pointer - stack is at $0100-$01FF pub acc: u8, // accumulator pub x: u8, // x register pub y: u8, // y register pub status: Status, // Status Registers pub irq_flags: IrqFlags, pub dma_oam_addr: Option, pub bus: Bus, #[serde(skip)] pub corrupted: bool, // Encountering an invalid opcode corrupts CPU processing #[serde(skip)] pub disasm: String, } impl Cpu { const NTSC_MASTER_CLOCK_RATE: f32 = 21_477_272.0; const NTSC_CPU_CLOCK_RATE: f32 = Self::NTSC_MASTER_CLOCK_RATE / 12.0; const PAL_MASTER_CLOCK_RATE: f32 = 26_601_712.0; const PAL_CPU_CLOCK_RATE: f32 = Self::PAL_MASTER_CLOCK_RATE / 16.0; const DENDY_CPU_CLOCK_RATE: f32 = Self::PAL_MASTER_CLOCK_RATE / 15.0; // Represents CPU/PPU alignment and would range from 1..=ppu.clock_divider-1 // if random PPU alignment was emulated // See: https://www.nesdev.org/wiki/PPU_frame_timing#CPU-PPU_Clock_Alignment const PPU_OFFSET: u32 = 1; const NMI_VECTOR: u16 = 0xFFFA; // NMI Vector address const IRQ_VECTOR: u16 = 0xFFFE; // IRQ Vector address const RESET_VECTOR: u16 = 0xFFFC; // Vector address at reset const POWER_ON_STATUS: Status = Status::U.union(Status::I); const POWER_ON_SP: u8 = 0xFD; const SP_BASE: u16 = 0x0100; // Stack-pointer starting address /// Create a new CPU with the given bus. pub fn new(bus: Bus) -> Self { let mut cpu = Self { cycle: 0, master_clock: 0, start_cycles: 6, end_cycles: 6, pc: 0x0000, operand: 0, addr_mode: AddrMode::default(), sp: 0x00, acc: 0x00, x: 0x00, y: 0x00, status: Self::POWER_ON_STATUS, irq_flags: IrqFlags::default(), dma_oam_addr: None, bus, corrupted: false, disasm: String::new(), }; cpu.set_region(cpu.bus.region); cpu } /// Load a CPU state. pub fn load(&mut self, mut cpu: Self) { // Doesn't make sense to load a debugger from a previous state cpu.bus.ppu.debugger = std::mem::take(&mut self.bus.ppu.debugger); *self = cpu; } /// Returns the CPU clock rate based on [`NesRegion`]. #[inline] #[must_use] pub const fn region_clock_rate(region: NesRegion) -> f32 { match region { NesRegion::Auto | NesRegion::Ntsc => Self::NTSC_CPU_CLOCK_RATE, NesRegion::Pal => Self::PAL_CPU_CLOCK_RATE, NesRegion::Dendy => Self::DENDY_CPU_CLOCK_RATE, } } /// Clock rate based on currently configured NES region. #[inline] #[must_use] pub const fn clock_rate(&self) -> f32 { Self::region_clock_rate(self.bus.region) } /// Peek at the next instruction. #[inline] pub fn next_instr(&self) -> InstrRef { let opcode = self.peek(self.pc); Cpu::INSTR_REF[usize::from(opcode)] } /// Start OAM DMA. #[inline] pub fn start_oam_dma(&mut self, addr: u16) { self.irq_flags.insert(IrqFlags::DMA_HALT); self.dma_oam_addr = Some(addr); } /// Process an interrupted request. /// /// /// # address R/W description /// --- ------- --- ----------------------------------------------- /// 1 PC R fetch PCH /// 2 PC R fetch PCL /// 3 $0100,S W push PCH to stack, decrement S /// 4 $0100,S W push PCL to stack, decrement S /// 5 $0100,S W push P to stack, decrement S /// 6 PC R fetch low byte of interrupt vector /// 7 PC R fetch high byte of interrupt vector #[cold] #[inline(never)] pub fn irq(&mut self) { if self.irq_flags(IrqFlags::DMA_HALT) && self.region() == NesRegion::Pal { // Check for DMA on PAL self.handle_dma(self.pc); } self.read(self.pc); // Dummy read self.read(self.pc); // Dummy read self.push_word(self.pc); // Pushing status to the stack has to happen after checking NMI since it can hijack the BRK // IRQ when it occurs between cycles 4 and 5. // https://www.nesdev.org/wiki/CPU_interrupts#Interrupt_hijacking // // Set U and !B during push let status = ((self.status | Status::U) & !Status::B).bits(); let nmi = self.irq_flags(IrqFlags::NMI); self.push_byte(status); self.status.set(Status::I, true); if nmi { self.clear_irq_flags(IrqFlags::NMI); self.pc = self.read_word(Self::NMI_VECTOR); self.clock_sync(); trace!( "NMI - PPU:{:3},{:3} CYC:{}", self.bus.ppu.cycle, self.bus.ppu.scanline, self.cycle ); } else { self.pc = self.read_word(Self::IRQ_VECTOR); trace!( "IRQ - PPU:{:3},{:3} CYC:{}", self.bus.ppu.cycle, self.bus.ppu.scanline, self.cycle ); } } /// Handle CPU interrupt requests, if any are pending. #[inline(always)] fn handle_interrupts(&mut self) { let irq_pending_mapper = self.bus.ppu.mapper.irq_pending(); let dma_pending_mapper = self.bus.ppu.mapper.dma_pending(); let nmi_pending = self.bus.ppu.nmi_pending; let irq_pending_apu = self.bus.apu.irq_pending(); let dma_pending_apu = self.bus.apu.dma_pending(); if dma_pending_apu { self.bus.apu.clear_dma_pending(); self.irq_flags .insert(IrqFlags::DMA_DMC | IrqFlags::DMA_HALT | IrqFlags::DMA_DUMMY_READ); } else if dma_pending_mapper { self.bus.ppu.mapper.clear_dma_pending(); self.irq_flags .insert(IrqFlags::DMA_DMC | IrqFlags::DMA_HALT | IrqFlags::DMA_DUMMY_READ); } let flags = &mut self.irq_flags; // https://www.nesdev.org/wiki/CPU_interrupts // // The internal signal goes high during φ1 of the cycle that follows the one where // the edge is detected, and stays high until the NMI has been handled. NMI is handled only // when `prev_nmi` is true. flags.set(IrqFlags::PREV_NMI, flags.contains(IrqFlags::NMI)); // This edge detector polls the status of the NMI line during φ2 of each CPU cycle (i.e., // during the second half of each cycle, hence here in `end_cycle`) and raises an internal // signal if the input goes from being high during one cycle to being low during the // next. let prev_nmi_pending = flags.contains(IrqFlags::PREV_NMI_PENDING); if !prev_nmi_pending & nmi_pending { flags.insert(IrqFlags::NMI); } flags.set(IrqFlags::PREV_NMI_PENDING, nmi_pending); // The IRQ status at the end of the second-to-last cycle is what matters, // so keep the second-to-last status. flags.set(IrqFlags::PREV_RUN_IRQ, flags.contains(IrqFlags::RUN_IRQ)); let run_irq = (irq_pending_mapper | irq_pending_apu) & !self.status.intersects(Status::I); flags.set(IrqFlags::RUN_IRQ, run_irq); #[cfg(feature = "trace")] if !flags.contains(IrqFlags::PREV_NMI_PENDING) && flags.contains(IrqFlags::RUN_IRQ) { trace!( "IRQ: {} - CYC:{}", irq_pending_mapper | irq_pending_apu, self.cycle ); } } /// Start a CPU cycle. #[inline(always)] fn start_cycle(&mut self, increment: u8) { self.master_clock = self.master_clock.wrapping_add(u32::from(increment)); self.cycle = self.cycle.wrapping_add(1); self.bus.ppu.clock_to(self.master_clock - Self::PPU_OFFSET); self.bus.cpu_clock(); } /// End a CPU cycle. #[inline(always)] fn end_cycle(&mut self, increment: u8) { self.master_clock = self.master_clock.wrapping_add(u32::from(increment)); self.bus.ppu.clock_to(self.master_clock - Self::PPU_OFFSET); self.handle_interrupts(); } /// Start a direct-memory access (DMA) cycle. #[inline(always)] fn start_dma_cycle(&mut self) { // OAM DMA cycles count as halt/dummy reads for DMC DMA when both run at the same time if self.irq_flags(IrqFlags::DMA_HALT) { self.clear_irq_flags(IrqFlags::DMA_HALT); } else { self.clear_irq_flags(IrqFlags::DMA_DUMMY_READ); } self.start_cycle(self.start_cycles - 1); } /// Handle a direct-memory access (DMA) request. #[cold] #[inline(never)] fn handle_dma(&mut self, addr: u16) { trace!("Starting DMA - CYC:{}", self.cycle); self.start_cycle(self.start_cycles - 1); self.bus.read(addr); self.end_cycle(self.start_cycles + 1); self.clear_irq_flags(IrqFlags::DMA_HALT); let skip_dummy_reads = addr == 0x4016 || addr == 0x4017; let mut oam_offset = 0; let mut oam_dma_count = 0; let mut read_val = 0; loop { let dma_dmc = self.irq_flags(IrqFlags::DMA_DMC); let dma_oam_addr = self.dma_oam_addr; if !dma_dmc & dma_oam_addr.is_none() { break; } if self.cycle & 0x01 == 0x00 { if dma_dmc & !self.irq_flags(IrqFlags::DMA_HALT) & !self.irq_flags(IrqFlags::DMA_DUMMY_READ) { // DMC DMA ready to read a byte (halt and dummy read done before) self.start_dma_cycle(); let dma_addr = self.bus.apu.dmc.dma_addr(); read_val = self.bus.read(dma_addr); trace!( "Loaded DMC DMA byte. ${dma_addr:04X}: {read_val} - CYC:{}", self.cycle ); self.end_cycle(self.start_cycles + 1); self.bus.apu.dmc.load_buffer(read_val); self.clear_irq_flags(IrqFlags::DMA_DMC); } else if let Some(oam_addr) = dma_oam_addr { // DMC DMA not running or ready, run OAM DMA self.start_dma_cycle(); read_val = self.bus.read(oam_addr + oam_offset); self.end_cycle(self.start_cycles + 1); oam_offset += 1; oam_dma_count += 1; } else { // DMC DMA running, but not ready yet (needs to halt, or dummy read) and OAM // DMA isn't running debug_assert!( self.irq_flags(IrqFlags::DMA_HALT) | self.irq_flags(IrqFlags::DMA_DUMMY_READ) ); self.start_dma_cycle(); if !skip_dummy_reads { self.bus.read(addr); // throw away } self.end_cycle(self.start_cycles + 1); } } else if dma_oam_addr.is_some() & (oam_dma_count & 0x01 == 0x01) { // OAM DMA write cycle, done on odd cycles after a read on even cycles self.start_dma_cycle(); self.bus.write(0x2004, read_val); self.end_cycle(self.start_cycles + 1); oam_dma_count += 1; if oam_dma_count == 0x200 { self.dma_oam_addr.take(); } } else { // Align to read cycle before starting OAM DMA (or align to perform DMC read) self.start_dma_cycle(); if !skip_dummy_reads { self.bus.read(addr); // throw away } self.end_cycle(self.start_cycles + 1); } } } // Interrupt flag functions /// Clear [`IrqFlags`] flags for the given bits. #[inline(always)] fn clear_irq_flags(&mut self, flags: IrqFlags) { self.irq_flags &= !flags; } /// Returns `true` if the [`IrqFlags`] register is set. #[inline(always)] fn irq_flags(&self, flags: IrqFlags) -> bool { (self.irq_flags & flags).bits() == flags.bits() } // Status Register functions /// Set [`Status`] flags for the given bits. #[inline(always)] fn set_status(&mut self, status: Status) { self.status = status & !Status::U & !Status::B; } /// Returns the [`Status`] register as a byte. #[inline(always)] const fn status_bit(&self, reg: Status) -> u8 { self.status.intersection(reg).bits() } /// Set accumulator and update [`Status`] flags based on value. #[inline(always)] fn set_acc(&mut self, val: u8) { self.set_zn_status(val); self.acc = val; } /// Set x and update [`Status`] flags based on value. #[inline(always)] fn set_x(&mut self, val: u8) { self.set_zn_status(val); self.x = val; } /// Set y and update [`Status`] flags based on value. #[inline(always)] fn set_y(&mut self, val: u8) { self.set_zn_status(val); self.y = val; } /// Set stack pointer. #[inline(always)] const fn set_sp(&mut self, val: u8) { self.sp = val; } /// Set both [`Status::Z`] and [`Status::N`] flags based on value. #[inline(always)] fn set_zn_status(&mut self, val: u8) { self.status.set(Status::Z, val == 0x00); self.status.set(Status::N, val & 0x80 > 0); } // Stack Functions /// Push a byte to the stack. #[inline(always)] fn push_byte(&mut self, val: u8) { self.write(Self::SP_BASE | u16::from(self.sp), val); self.sp = self.sp.wrapping_sub(1); } /// Pull a byte from the stack. #[inline(always)] #[must_use] fn pop_byte(&mut self) -> u8 { self.sp = self.sp.wrapping_add(1); self.read(Self::SP_BASE | u16::from(self.sp)) } /// Peek byte at the top of the stack. #[inline] #[must_use] pub fn peek_stack(&self) -> u8 { self.peek(Self::SP_BASE | u16::from(self.sp.wrapping_add(1))) } /// Peek at the top of the stack. #[inline] #[must_use] pub fn peek_stack_u16(&self) -> u16 { let lo = self.peek(Self::SP_BASE | u16::from(self.sp)); let hi = self.peek(Self::SP_BASE | u16::from(self.sp.wrapping_add(1))); u16::from_le_bytes([lo, hi]) } /// Push a word (two bytes) to the stack #[inline(always)] fn push_word(&mut self, val: u16) { let [lo, hi] = val.to_le_bytes(); self.push_byte(hi); self.push_byte(lo); } /// Pull a word (two bytes) from the stack #[inline(always)] fn pop_word(&mut self) -> u16 { let lo = self.pop_byte(); let hi = self.pop_byte(); u16::from_le_bytes([lo, hi]) } // Memory accesses /// Fetch a byte and increments PC by 1. #[inline(always)] #[must_use] fn fetch_byte(&mut self) -> u8 { let val = self.read(self.pc); self.pc = self.pc.wrapping_add(1); val } /// Fetch opcode operand based on addressing mode. #[inline(always)] #[must_use] fn fetch_operand(&mut self) -> u16 { match self.addr_mode { AddrMode::ACC | AddrMode::IMP => self.acc_imp(), AddrMode::IMM | AddrMode::REL | AddrMode::ZP0 => self.imm_rel_zp(), AddrMode::ZPX => self.zpx(), AddrMode::ZPY => self.zpy(), AddrMode::IND => self.ind(), AddrMode::IDX => self.idx(), AddrMode::IDY => self.idy(false), AddrMode::IDYW => self.idy(true), AddrMode::ABS => self.abs(), AddrMode::ABX => self.abx(false), AddrMode::ABXW => self.abx(true), AddrMode::ABY => self.aby(false), AddrMode::ABYW => self.aby(true), AddrMode::OTH => 0, } } /// Fetch a 16-bit word and increments PC by 2. #[inline(always)] #[must_use] fn fetch_word(&mut self) -> u16 { let lo = self.fetch_byte(); let hi = self.fetch_byte(); u16::from_le_bytes([lo, hi]) } /// Read operand value. #[inline(always)] #[must_use] fn read_operand(&mut self) -> u8 { if matches!( self.addr_mode, AddrMode::ACC | AddrMode::IMP | AddrMode::IMM | AddrMode::REL ) { self.operand as u8 } else { self.read(self.operand) } } /// Read a 16-bit word. #[inline(always)] #[must_use] pub fn read_word(&mut self, addr: u16) -> u16 { let lo = self.read(addr); let hi = self.read(addr.wrapping_add(1)); u16::from_le_bytes([lo, hi]) } /// Peek a 16-bit word without side effects. #[inline] #[must_use] pub fn peek_word(&self, addr: u16) -> u16 { let lo = self.peek(addr); let hi = self.peek(addr.wrapping_add(1)); u16::from_le_bytes([lo, hi]) } /// Disassemble the instruction at the given program counter. pub fn disassemble(&mut self, pc: &mut u16) -> &str { use fmt::Write; self.disasm.clear(); let addr = { *pc }; let opcode = { let byte = self.peek(*pc); *pc = pc.wrapping_add(1); byte }; let _ = write!(self.disasm, "${addr:04X} ${opcode:02X} "); let mut peek_byte = || { let byte = self.peek(*pc); *pc = pc.wrapping_add(1); byte }; let mut peek_word = || { let lo = peek_byte(); let hi = peek_byte(); (lo, hi, u16::from_le_bytes([lo, hi])) }; let instr_ref = Cpu::INSTR_REF[usize::from(opcode)]; match instr_ref.addr_mode { AddrMode::ACC | AddrMode::IMP => { let _ = write!(self.disasm, " {instr_ref}"); } AddrMode::IMM => { let byte = peek_byte(); let _ = write!(self.disasm, "${byte:02X} {instr_ref} #${byte:02X}"); } AddrMode::REL => { let byte = peek_byte(); let addr = (*pc as i16).wrapping_add(i16::from(byte as i8)) as u16; let _ = write!(self.disasm, "${byte:02X} {instr_ref} ${addr:04X}"); } AddrMode::ZP0 => { let byte = peek_byte(); let val = self.peek(byte.into()); let _ = write!( self.disasm, "${byte:02X} {instr_ref} ${byte:02X} = #${val:02X}" ); } AddrMode::ZPX => { let byte = peek_byte(); let addr = byte.wrapping_add(self.x); let val = self.peek(addr.into()); let _ = write!( self.disasm, "${byte:02X} {instr_ref} ${byte:02X},X @ ${addr:02X} = #${val:02X}" ); } AddrMode::ZPY => { let byte = peek_byte(); let addr = byte.wrapping_add(self.y); let val = self.peek(addr.into()); let _ = write!( self.disasm, "${byte:02X} {instr_ref} ${byte:02X},Y @ ${addr:02X} = #${val:02X}" ); } AddrMode::IND => { let (byte1, byte2, base_addr) = peek_word(); let val = if (base_addr & 0xFF) == 0xFF { let lo = self.peek(base_addr); let hi = self.peek(base_addr - 0xFF); u16::from_le_bytes([lo, hi]) } else { self.peek_word(base_addr) }; let _ = write!( self.disasm, "${byte1:02X} ${byte2:02X} {instr_ref} (${base_addr:04X}) = ${val:04X}" ); } AddrMode::IDX => { let byte = peek_byte(); let zero_addr = byte.wrapping_add(self.x); let lo = self.peek(u16::from(zero_addr)); let hi = self.peek(u16::from(zero_addr.wrapping_add(1))); let addr = u16::from_le_bytes([lo, hi]); let val = self.peek(addr); let _ = write!( self.disasm, "${byte:02X} {instr_ref} (${byte:02X},X) @ ${addr:04X} = #${val:02X}" ); } AddrMode::IDY | AddrMode::IDYW => { let byte = peek_byte(); let base_addr = { let lo = self.peek(u16::from(byte)); let hi = self.peek(u16::from(byte.wrapping_add(1))); u16::from_le_bytes([lo, hi]) }; let addr = base_addr.wrapping_add(u16::from(self.y)); let val = self.peek(addr); let _ = write!( self.disasm, "${byte:02X} {instr_ref} (${byte:02X}),Y @ ${addr:04X} = #${val:02X}" ); } AddrMode::ABS => { let (byte1, byte2, addr) = peek_word(); if instr_ref.instr == JMP { let _ = write!( self.disasm, "${byte1:02X} ${byte2:02X} {instr_ref} ${addr:04X}" ); } else { let val = self.peek(addr); let _ = write!( self.disasm, "${byte1:02X} ${byte2:02X} {instr_ref} ${addr:04X} = #${val:02X}" ); } } AddrMode::ABX | AddrMode::ABXW => { let (byte1, byte2, base_addr) = peek_word(); let addr = base_addr.wrapping_add(self.x.into()); let val = self.peek(addr); let _ = write!( self.disasm, "${byte1:02X} ${byte2:02X} {instr_ref} ${base_addr:04X},X @ ${addr:04X} = #${val:02X}" ); } AddrMode::ABY | AddrMode::ABYW => { let (byte1, byte2, base_addr) = peek_word(); let addr = base_addr.wrapping_add(self.y.into()); let val = self.peek(addr); let _ = write!( self.disasm, "${byte1:02X} ${byte2:02X} {instr_ref} ${base_addr:04X},Y @ ${addr:04X} = #${val:02X}" ); } AddrMode::OTH => { let (byte1, byte2, addr) = peek_word(); if instr_ref.instr == JSR { let _ = write!( self.disasm, "${byte1:02X} ${byte2:02X} {instr_ref} ${addr:04X}" ); } else { let val = self.peek(addr); let _ = write!( self.disasm, "${byte1:02X} ${byte2:02X} {instr_ref} ${addr:04X} = #${val:02X}" ); } } }; &self.disasm } /// Logs the disassembled instruction being executed. #[cold] #[inline(never)] pub fn trace_instr(&mut self) { if !tracing::enabled!(tracing::Level::TRACE) { return; } let mut pc = self.pc; let status = self.status; let acc = self.acc; let x = self.x; let y = self.y; let sp = self.sp; let ppu_cycle = self.bus.ppu.cycle; let ppu_scanline = self.bus.ppu.scanline; let cycle = self.cycle; let n = if status.contains(Status::N) { 'N' } else { 'n' }; let v = if status.contains(Status::V) { 'V' } else { 'v' }; let i = if status.contains(Status::I) { 'I' } else { 'i' }; let z = if status.contains(Status::Z) { 'Z' } else { 'z' }; let c = if status.contains(Status::C) { 'C' } else { 'c' }; println!( "{:<50} A:{acc:02X} X:{x:02X} Y:{y:02X} P:{n}{v}--d{i}{z}{c} SP:{sp:02X} PPU:{ppu_cycle:3},{ppu_scanline:3} CYC:{cycle}", self.disassemble(&mut pc), ); } // Utilities /// Returns whether two addresses are on different memory pages. #[inline(always)] #[must_use] const fn pages_differ(addr1: u16, addr2: u16) -> bool { (addr1 & 0xFF00) != (addr2 & 0xFF00) } /// Returns whether a memory page is crossed using relative address. #[inline(always)] #[must_use] const fn page_crossed(addr: u16, offset: i16) -> bool { ((addr as i16 + offset) as u16 & 0xFF00) != (addr & 0xFF00) } /// Runs all componnets up to master clock, synchronizing them. #[inline(always)] pub fn clock_sync(&mut self) { self.bus.ppu.clock_to(self.master_clock); self.master_clock = self.master_clock.saturating_sub(self.bus.ppu.master_clock); self.bus.ppu.master_clock = 0; self.bus.apu.clock_sync(); } } impl Clock for Cpu { /// Runs the CPU one instruction. #[inline(always)] fn clock(&mut self) { #[cfg(feature = "trace")] self.trace_instr(); let opcode = self.fetch_byte(); // Cycle 1 let op = Cpu::OPS[usize::from(opcode)]; self.addr_mode = op.addr_mode(); self.operand = self.fetch_operand(); op.run(self); if self .irq_flags .intersects(IrqFlags::PREV_RUN_IRQ | IrqFlags::PREV_NMI) { self.irq(); } } } impl Read for Cpu { #[inline(always)] fn read(&mut self, addr: u16) -> u8 { if self.irq_flags(IrqFlags::DMA_HALT) { self.handle_dma(addr); } self.start_cycle(self.start_cycles - 1); let val = self.bus.read(addr); self.end_cycle(self.end_cycles + 1); val } fn peek(&self, addr: u16) -> u8 { self.bus.peek(addr) } } impl Write for Cpu { #[inline(always)] fn write(&mut self, addr: u16, val: u8) { self.start_cycle(self.start_cycles + 1); if addr == 0x4014 { self.start_oam_dma(u16::from(val) << 8); } else { self.bus.write(addr, val); } self.end_cycle(self.end_cycles - 1); } } impl Regional for Cpu { #[inline(always)] fn region(&self) -> NesRegion { self.bus.region } fn set_region(&mut self, region: NesRegion) { let (start_cycles, end_cycles) = match region { NesRegion::Auto | NesRegion::Ntsc => (6, 6), // NTSC_MASTER_CLOCK_DIVIDER / 2 NesRegion::Pal => (8, 8), // PAL_MASTER_CLOCK_DIVIDER / 2 NesRegion::Dendy => (7, 8), // DENDY_MASTER_CLOCK_DIVIDER / 2 }; self.start_cycles = start_cycles; self.end_cycles = end_cycles; self.bus.set_region(region); self.clock_sync(); } } impl Reset for Cpu { /// Resets the CPU /// /// Updates the PC, SP, and Status values to defined constants. /// /// These operations take the CPU 7 cycles. fn reset(&mut self, kind: ResetKind) { trace!("{:?} RESET", kind); match kind { ResetKind::Soft => { self.status.set(Status::I, true); // Reset pushes to the stack similar to IRQ, but since the read bit is set, nothing is // written except the SP being decremented self.sp = self.sp.wrapping_sub(0x03); } ResetKind::Hard => { self.acc = 0x00; self.x = 0x00; self.y = 0x00; self.status = Self::POWER_ON_STATUS; self.sp = Self::POWER_ON_SP; } } self.bus.reset(kind); self.cycle = 0; self.master_clock = 0; self.irq_flags = IrqFlags::default(); self.corrupted = false; // Read directly from bus so as to not clock other components during reset let lo = self.bus.read(Self::RESET_VECTOR); let hi = self.bus.read(Self::RESET_VECTOR + 1); self.pc = u16::from_le_bytes([lo, hi]); // The CPU takes 7 cycles to reset/power on // See: // * // * for _ in 0..7 { self.start_cycle(self.start_cycles - 1); self.end_cycle(self.start_cycles + 1); } } } impl fmt::Debug for Cpu { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::result::Result<(), fmt::Error> { f.debug_struct("Cpu") .field("cycle", &self.cycle) .field("pc", &format_args!("${:04X}", self.pc)) .field("sp", &format_args!("${:02X}", self.sp)) .field("acc", &format_args!("${:02X}", self.acc)) .field("x", &format_args!("${:02X}", self.x)) .field("y", &format_args!("${:02X}", self.y)) .field("status", &self.status) .field("bus", &self.bus) .field("interrupt_flags", &self.irq_flags) .finish() } } #[cfg(test)] mod tests { use crate::{cart::Cart, cpu::instr::Instr::*, mapper::Nrom, mem::Memory}; #[test] fn cycle_timing() { use super::*; let mut cpu = Cpu::new(Bus::default()); let mut cart = Cart::empty(); cart.mapper = Nrom::load( &cart, Memory::new(cart.chr_rom_size), Memory::new(cart.prg_rom_size), ) .unwrap(); cpu.bus.load_cart(cart); cpu.reset(ResetKind::Hard); cpu.clock(); assert_eq!(cpu.cycle, 14, "cpu after power + one clock"); for instr_ref in Cpu::INSTR_REF.iter() { let extra_cycle = match instr_ref.instr { BCC | BNE | BPL | BVC => 1, _ => 0, }; // Ignore invalid opcodes if instr_ref.instr == HLT { continue; } cpu.reset(ResetKind::Hard); cpu.bus.write(0x0000, instr_ref.opcode); cpu.clock(); let cpu_cyc = u32::from(7 + instr_ref.cycles + extra_cycle); assert_eq!( cpu.cycle, cpu_cyc, "cpu ${:02X} {:?} #{:?}", instr_ref.opcode, instr_ref.instr, instr_ref.addr_mode ); } } } ================================================ FILE: tetanes-core/src/debug.rs ================================================ use crate::ppu::Ppu; use std::sync::Arc; #[derive(Debug, Clone, PartialEq)] #[must_use] pub enum Debugger { Ppu(PpuDebugger), } impl From for Debugger { fn from(debugger: PpuDebugger) -> Self { Self::Ppu(debugger) } } #[derive(Clone)] #[must_use] pub struct PpuDebugger { pub cycle: u16, pub scanline: u16, pub callback: Arc, } impl Default for PpuDebugger { fn default() -> Self { Self { cycle: u16::MAX, scanline: u16::MAX, callback: Arc::new(|_| {}), } } } impl PartialEq for PpuDebugger { fn eq(&self, other: &Self) -> bool { self.cycle == other.cycle && self.scanline == other.scanline } } impl std::fmt::Debug for PpuDebugger { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("PpuDebugger") .field("cycle", &self.cycle) .field("scanline", &self.scanline) .finish_non_exhaustive() } } ================================================ FILE: tetanes-core/src/error.rs ================================================ //! Error handling. use std::path::PathBuf; use thiserror::Error; pub type Result = std::result::Result; #[derive(Error, Debug)] #[must_use] pub enum Error { #[error("invalid save version (expected {expected:?}, found: {found:?})")] InvalidSaveVersion { expected: &'static str, found: String, }, #[error("invalid tetanes header (path: {path:?}. {error}")] InvalidSaveHeader { path: PathBuf, error: String }, #[error("invalid configuration {value:?} for {field:?}")] InvalidConfig { field: &'static str, value: String }, #[error("{context}: {source:?}")] Io { context: String, source: std::io::Error, }, #[error("{0}")] Unknown(String), } impl Error { pub fn io(source: std::io::Error, context: impl Into) -> Self { Self::Io { context: context.into(), source, } } } ================================================ FILE: tetanes-core/src/fs.rs ================================================ //! Filesystem utilities for save state and compression. use crate::sys::fs; use flate2::{Compression, read::DeflateDecoder, write::DeflateEncoder}; use serde::{Serialize, de::DeserializeOwned}; use std::{ io::{Cursor, Read, Write}, path::{Path, PathBuf}, }; use thiserror::Error; use tracing::warn; const SAVE_FILE_MAGIC_LEN: usize = 8; const SAVE_FILE_MAGIC: [u8; SAVE_FILE_MAGIC_LEN] = *b"TETANES\x1a"; // Keep this separate from Semver because breaking API changes may not invalidate the save format. const SAVE_VERSION: &str = "1"; pub type Result = std::result::Result; #[derive(Error, Debug)] #[must_use] pub enum Error { #[error("invalid tetanes header: {0}")] InvalidHeader(String), #[error("failed to write tetanes header: {0:?}")] WriteHeaderFailed(std::io::Error), #[error("failed to encode data: {0:?}")] EncodingFailed(std::io::Error), #[error("failed to decode data: {0:?}")] DecodingFailed(std::io::Error), #[error("failed to serialize data: {0:?}")] SerializationFailed(String), #[error("failed to deserialize data: {0:?}")] DeserializationFailed(String), #[error("invalid path: {0:?}")] InvalidPath(PathBuf), #[error("{context}: {source:?}")] Io { source: std::io::Error, context: String, }, #[error("{0}")] Custom(String), } impl Error { pub fn io(source: std::io::Error, context: impl Into) -> Self { Self::Io { source, context: context.into(), } } pub fn custom(error: impl Into) -> Self { Self::Custom(error.into()) } } /// Writes a header including a magic string and a version /// /// # Errors /// /// If the header fails to write to disk, then an error is returned. pub(crate) fn write_header(f: &mut impl Write) -> std::io::Result<()> { f.write_all(&SAVE_FILE_MAGIC)?; f.write_all(SAVE_VERSION.as_bytes()) } /// Verifies a `TetaNES` saved state header. /// /// # Errors /// /// If the header fails to validate, then an error is returned. pub(crate) fn validate_header(f: &mut impl Read) -> Result<()> { let mut magic = [0u8; SAVE_FILE_MAGIC_LEN]; f.read_exact(&mut magic) .map_err(|s| Error::InvalidHeader(s.to_string()))?; if magic != SAVE_FILE_MAGIC { return Err(Error::InvalidHeader(format!( "invalid magic (expected {SAVE_FILE_MAGIC:?}, found: {magic:?}", ))); } let mut version = [0u8]; f.read_exact(&mut version) .map_err(|s| Error::InvalidHeader(s.to_string()))?; if version == SAVE_VERSION.as_bytes() { Ok(()) } else { Err(Error::InvalidHeader(format!( "invalid version (expected {SAVE_VERSION:?}, found: {version:?}", ))) } } pub fn encode(mut writer: &mut impl Write, data: &[u8]) -> std::io::Result<()> { let mut encoder = DeflateEncoder::new(&mut writer, Compression::default()); encoder.write_all(data)?; encoder.finish()?; Ok(()) } pub fn decode(data: impl Read) -> std::io::Result> { let mut decoded = vec![]; let mut decoder = DeflateDecoder::new(data); decoder.read_to_end(&mut decoded)?; Ok(decoded) } pub fn save(path: impl AsRef, value: &T) -> Result<()> where T: ?Sized + Serialize, { let config = bincode::config::legacy(); let data = bincode::serde::encode_to_vec(value, config) .map_err(|err| Error::SerializationFailed(err.to_string()))?; let mut writer = fs::writer_impl(path)?; write_header(&mut writer).map_err(Error::WriteHeaderFailed)?; encode(&mut writer, &data).map_err(Error::EncodingFailed)?; writer .flush() .map_err(|err| Error::io(err, "failed to save data"))?; Ok(()) } pub fn save_raw(path: impl AsRef, value: &[u8]) -> Result<()> { let mut writer = fs::writer_impl(path)?; writer .write_all(value) .map_err(|err| Error::io(err, "failed to save data"))?; writer .flush() .map_err(|err| Error::io(err, "failed to save data"))?; Ok(()) } pub fn load(path: impl AsRef) -> Result where T: DeserializeOwned, { let mut reader = fs::reader_impl(path)?; validate_header(&mut reader)?; let data = decode(&mut reader).map_err(Error::DecodingFailed)?; let config = bincode::config::legacy(); let (res, _) = bincode::serde::decode_from_slice(&data, config) .map_err(|err| Error::DeserializationFailed(err.to_string()))?; Ok(res) } pub fn load_bytes(bytes: &[u8]) -> Result where T: DeserializeOwned, { let mut reader = Cursor::new(bytes); validate_header(&mut reader)?; let data = decode(&mut reader).map_err(Error::DecodingFailed)?; let config = bincode::config::legacy(); let (res, _) = bincode::serde::decode_from_slice(&data, config) .map_err(|err| Error::SerializationFailed(err.to_string()))?; Ok(res) } pub fn load_raw(path: impl AsRef) -> Result> { let mut reader = fs::reader_impl(path)?; let mut data = vec![]; reader .read_to_end(&mut data) .map_err(|err| Error::io(err, "failed to load data"))?; Ok(data) } pub fn clear_dir(path: impl AsRef) -> Result<()> { fs::clear_dir_impl(path) } pub fn exists(path: &Path) -> bool { fs::exists_impl(path) } pub fn filename(path: &Path) -> &str { path.file_name() .and_then(std::ffi::OsStr::to_str) .unwrap_or_else(|| { warn!("invalid path without file_name: {path:?}"); "??" }) } pub fn compute_crc32(data: &[u8]) -> u32 { compute_combine_crc32(0, data) } pub fn compute_combine_crc32(crc32: u32, data: &[u8]) -> u32 { const BUFFER_SIZE: usize = 0x2000; data.chunks(BUFFER_SIZE).fold(crc32, compute_crc32_buffer) } fn compute_crc32_buffer(crc32: u32, buffer: &[u8]) -> u32 { buffer.iter().fold(crc32 ^ 0xFFFFFFFF, |crc32, byte| { (crc32 >> 8) ^ CRC_TABLE[((crc32 ^ *byte as u32) & 0xFF) as usize] }) ^ 0xFFFFFFFF } const CRC_TABLE: [u32; 256] = [ 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D, ]; #[cfg(test)] mod tests { use super::*; #[test] fn save_header() { let mut file = Vec::new(); assert!(write_header(&mut file).is_ok(), "write header"); assert!( validate_header(&mut file.as_slice()).is_ok(), "validate header" ); } #[test] fn crc32() { let s = "Lorem ipsum dolor sit amet, consectetur adipisicing elit"; assert_eq!(compute_crc32(s.as_bytes()), 0xb9b4cbd5); } } ================================================ FILE: tetanes-core/src/genie.rs ================================================ //! Game Genie code parsing. use serde::{Deserialize, Serialize}; use std::{collections::HashMap, sync::OnceLock}; use thiserror::Error; static GENIE_MAP: OnceLock> = OnceLock::new(); pub type Result = std::result::Result; #[derive(Error, Debug)] #[error("invalid genie code {code:?}. {kind}")] pub struct Error { code: String, kind: ErrorKind, } impl Error { fn new(code: impl Into, kind: ErrorKind) -> Self { Self { code: code.into(), kind, } } pub const fn kind(&self) -> ErrorKind { self.kind } } #[derive(Error, Debug, Copy, Clone)] #[must_use] pub enum ErrorKind { #[error("length must be 6 or 8 characters. found `{0}`")] InvalidLength(usize), #[error("invalid character: `{0}`")] InvalidCharacter(char), } /// Game Genie Code #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct GenieCode { code: String, addr: u16, data: u8, compare: Option, } impl GenieCode { /// Creates a new `GenieCode` instance. /// /// # Errors /// /// This function will return an error if the given code is not the correct format. pub fn new(code: String) -> Result { let hex = Self::parse(&code)?; Ok(Self::from_raw(code, &hex)) } /// Creates a new `GenieCode` instance from raw hex values. `GenieCode` may not be valid if /// `hex` is not the correct length. Use `GenieCode::parse` to validate the code. pub fn from_raw(code: String, hex: &[u8]) -> Self { let addr = 0x8000 + (((u16::from(hex[3]) & 7) << 12) | ((u16::from(hex[5]) & 7) << 8) | ((u16::from(hex[4]) & 8) << 8) | ((u16::from(hex[2]) & 7) << 4) | ((u16::from(hex[1]) & 8) << 4) | (u16::from(hex[4]) & 7) | (u16::from(hex[3]) & 8)); let data = if hex.len() == 6 { ((hex[1] & 7) << 4) | ((hex[0] & 8) << 4) | (hex[0] & 7) | (hex[5] & 8) } else { ((hex[1] & 7) << 4) | ((hex[0] & 8) << 4) | (hex[0] & 7) | (hex[7] & 8) }; let compare = if hex.len() == 8 { Some(((hex[7] & 7) << 4) | ((hex[6] & 8) << 4) | (hex[6] & 7) | (hex[5] & 8)) } else { None }; Self { code: code.to_ascii_uppercase(), addr, data, compare, } } fn generate_genie_map() -> HashMap { // Game genie maps these letters to binary representations as a form of code obfuscation HashMap::from([ ('A', 0x0), ('P', 0x1), ('Z', 0x2), ('L', 0x3), ('G', 0x4), ('I', 0x5), ('T', 0x6), ('Y', 0x7), ('E', 0x8), ('O', 0x9), ('X', 0xA), ('U', 0xB), ('K', 0xC), ('S', 0xD), ('V', 0xE), ('N', 0xF), ]) } pub fn parse(code: &str) -> Result> { if code.len() != 6 && code.len() != 8 { return Err(Error::new(code, ErrorKind::InvalidLength(code.len()))); } let mut hex = Vec::with_capacity(code.len()); for s in code.chars() { if let Some(h) = GENIE_MAP .get_or_init(Self::generate_genie_map) .get(&s.to_ascii_uppercase()) { hex.push(*h); } else { return Err(Error::new(code, ErrorKind::InvalidCharacter(s))); } } Ok(hex.into()) } #[must_use] #[allow(clippy::missing_const_for_fn)] // false positive on non-const deref coercion pub fn code(&self) -> &str { &self.code } #[must_use] pub const fn addr(&self) -> u16 { self.addr } #[must_use] pub const fn read(&self, val: u8) -> u8 { if let Some(compare) = self.compare { if val == compare { self.data } else { val } } else { self.data } } } impl std::fmt::Display for GenieCode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", &self.code) } } ================================================ FILE: tetanes-core/src/input.rs ================================================ //! [`Joypad`] and [`Zapper`] implementation. use crate::{ common::{Clock, NesRegion, Reset, ResetKind}, cpu::Cpu, ppu::{Ppu, size}, }; use bitflags::bitflags; use serde::{Deserialize, Serialize}; use std::str::FromStr; use thiserror::Error; use tracing::trace; #[derive(Error, Debug)] #[must_use] #[error("failed to parse `Player`")] pub struct ParsePlayerError; #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[must_use] pub enum Player { #[default] One, Two, Three, Four, } impl std::fmt::Display for Player { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = match self { Self::One => "One", Self::Two => "Two", Self::Three => "Three", Self::Four => "Four", }; write!(f, "{s}") } } impl AsRef for Player { fn as_ref(&self) -> &str { match self { Self::One => "one", Self::Two => "two", Self::Three => "three", Self::Four => "four", } } } impl TryFrom for Player { type Error = ParsePlayerError; fn try_from(value: usize) -> Result { match value { 0 => Ok(Self::One), 1 => Ok(Self::Two), 2 => Ok(Self::Three), 3 => Ok(Self::Four), _ => Err(ParsePlayerError), } } } pub trait InputRegisters { fn read(&mut self, player: Player, ppu: &Ppu) -> u8; fn peek(&self, player: Player, ppu: &Ppu) -> u8; fn write(&mut self, val: u8); } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[must_use] pub enum FourPlayer { #[default] Disabled, FourScore, Satellite, } impl FourPlayer { pub const fn as_slice() -> &'static [Self] { &[Self::Disabled, Self::FourScore, Self::Satellite] } pub const fn as_str(&self) -> &'static str { match self { Self::Disabled => "disabled", Self::FourScore => "four-score", Self::Satellite => "satellite", } } } impl AsRef for FourPlayer { fn as_ref(&self) -> &str { self.as_str() } } impl std::fmt::Display for FourPlayer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = match self { Self::Disabled => "Disabled", Self::FourScore => "FourScore", Self::Satellite => "Satellite", }; write!(f, "{s}") } } impl FromStr for FourPlayer { type Err = &'static str; fn from_str(s: &str) -> Result { match s { "disabled" => Ok(Self::Disabled), "four-score" => Ok(Self::FourScore), "satellite" => Ok(Self::Satellite), _ => Err( "invalid FourPlayer value. valid options: `disabled`, `four-score`, or `satellite`", ), } } } #[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct Input { pub joypads: [Joypad; 4], pub signatures: [Joypad; 2], pub zapper: Zapper, pub turbo_timer: u32, pub four_player: FourPlayer, } impl Input { pub fn new(region: NesRegion) -> Self { Self { joypads: [Joypad::new(); 4], // Signature bits are reversed so they can shift right signatures: [ Joypad::from_bytes(0b0000_1000), Joypad::from_bytes(0b0000_0100), ], zapper: Zapper::new(region), turbo_timer: 30, four_player: FourPlayer::default(), } } pub const fn joypad(&self, player: Player) -> &Joypad { &self.joypads[player as usize] } pub const fn joypad_mut(&mut self, player: Player) -> &mut Joypad { &mut self.joypads[player as usize] } pub fn set_region(&mut self, region: NesRegion) { self.zapper.trigger_release_delay = Cpu::region_clock_rate(region) / 10.0; } pub fn set_concurrent_dpad(&mut self, enabled: bool) { self.joypads .iter_mut() .for_each(|pad| pad.concurrent_dpad = enabled); } pub const fn connect_zapper(&mut self, connected: bool) { self.zapper.connected = connected; } pub fn set_four_player(&mut self, four_player: FourPlayer) { self.four_player = four_player; self.reset(ResetKind::Hard); } pub fn clear(&mut self) { for pad in &mut self.joypads { pad.clear(); } self.zapper.clear(); } } impl InputRegisters for Input { fn read(&mut self, player: Player, ppu: &Ppu) -> u8 { // Read $4016/$4017 D0 8x for controller #1/#2. // Read $4016/$4017 D0 8x for controller #3/#4. // Read $4016/$4017 D0 8x for signature: 0b00010000/0b00100000 let zapper = if player == Player::Two { self.zapper.read(ppu) } else { 0x00 }; let player = player as usize; assert!(player < 4); let val = match self.four_player { FourPlayer::Disabled => self.joypads[player].read(), FourPlayer::FourScore => { if self.joypads[player].index() < 8 { self.joypads[player].read() } else if self.joypads[player + 2].index() < 8 { self.joypads[player + 2].read() } else if self.signatures[player].index() < 8 { self.signatures[player].read() } else { 0x01 } } FourPlayer::Satellite => { self.joypads[player].read() | (self.joypads[player + 2].read() << 1) } }; zapper | val | 0x40 } fn peek(&self, player: Player, ppu: &Ppu) -> u8 { // Read $4016/$4017 D0 8x for controller #1/#2. // Read $4016/$4017 D0 8x for controller #3/#4. // Read $4016/$4017 D0 8x for signature: 0b00010000/0b00100000 let zapper = if player == Player::Two { self.zapper.read(ppu) } else { 0x00 }; let player = player as usize; assert!(player < 4); let val = match self.four_player { FourPlayer::Disabled => self.joypads[player].peek(), FourPlayer::FourScore => { if self.joypads[player].index() < 8 { self.joypads[player].peek() } else if self.joypads[player + 2].index() < 8 { self.joypads[player + 2].peek() } else if self.signatures[player].index() < 8 { self.signatures[player].peek() } else { 0x01 } } FourPlayer::Satellite => { self.joypads[player].peek() | (self.joypads[player + 2].peek() << 1) } }; zapper | val | 0x40 } fn write(&mut self, val: u8) { for pad in &mut self.joypads { pad.write(val); } for sig in &mut self.signatures { sig.write(val); } } } impl Clock for Input { fn clock(&mut self) { self.zapper.clock(); if self.turbo_timer > 0 { self.turbo_timer -= 1; } if self.turbo_timer == 0 { // Roughly 20Hz self.turbo_timer += 89500; for pad in &mut self.joypads { if pad.button(JoypadBtnState::TURBO_A) { let pressed = pad.button(JoypadBtnState::A); pad.set_button(JoypadBtnState::A, !pressed); } if pad.button(JoypadBtnState::TURBO_B) { let pressed = pad.button(JoypadBtnState::B); pad.set_button(JoypadBtnState::B, !pressed); } } } } } impl Reset for Input { fn reset(&mut self, kind: ResetKind) { for pad in &mut self.joypads { pad.reset(kind); } self.signatures[0] = Joypad::from_bytes(0b0000_1000); self.signatures[1] = Joypad::from_bytes(0b0000_0100); self.zapper.reset(kind); } } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum JoypadBtn { /// Left D-Pad. Left, /// Right D-Pad. Right, /// Up D-Pad. Up, /// Down D-Pad. Down, /// A Button. A, /// B Button. B, /// A Button (Turbo). TurboA, /// B Button (Turbo). TurboB, /// Select Button. Select, /// Start Button. Start, } impl AsRef for JoypadBtn { fn as_ref(&self) -> &str { match *self { JoypadBtn::A => "A", JoypadBtn::B => "B", JoypadBtn::Select => "Select", JoypadBtn::Start => "Start", JoypadBtn::Up => "Up", JoypadBtn::Down => "Down", JoypadBtn::Left => "Left", JoypadBtn::Right => "Right", JoypadBtn::TurboA => "A (Turbo)", JoypadBtn::TurboB => "B (Turbo)", } } } bitflags! { #[derive(Default, Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)] #[must_use] pub struct JoypadBtnState: u16 { const A = 0x01; const B = 0x02; const SELECT = 0x04; const START = 0x08; const UP = 0x10; const DOWN = 0x20; const LEFT = 0x40; const RIGHT = 0x80; const TURBO_A = 0x100; const TURBO_B = 0x200; } } impl From for JoypadBtnState { fn from(button: JoypadBtn) -> Self { match button { JoypadBtn::A => Self::A, JoypadBtn::B => Self::B, JoypadBtn::Select => Self::SELECT, JoypadBtn::Start => Self::START, JoypadBtn::Up => Self::UP, JoypadBtn::Down => Self::DOWN, JoypadBtn::Left => Self::LEFT, JoypadBtn::Right => Self::RIGHT, JoypadBtn::TurboA => Self::TURBO_A, JoypadBtn::TurboB => Self::TURBO_B, } } } #[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct Joypad { pub buttons: JoypadBtnState, pub concurrent_dpad: bool, pub index: u8, pub strobe: bool, } impl Joypad { pub const fn new() -> Self { Self { buttons: JoypadBtnState::empty(), concurrent_dpad: false, index: 0, strobe: false, } } #[must_use] pub const fn button(&self, button: JoypadBtnState) -> bool { self.buttons.contains(button) } pub fn set_button(&mut self, button: impl Into, pressed: bool) { let button = button.into(); let prevent_concurrent_dpad = pressed && !self.concurrent_dpad; if let Some(button) = match button { JoypadBtnState::LEFT if prevent_concurrent_dpad => Some(JoypadBtnState::RIGHT), JoypadBtnState::RIGHT if prevent_concurrent_dpad => Some(JoypadBtnState::LEFT), JoypadBtnState::UP if prevent_concurrent_dpad => Some(JoypadBtnState::DOWN), JoypadBtnState::DOWN if prevent_concurrent_dpad => Some(JoypadBtnState::UP), JoypadBtnState::TURBO_A if !pressed => Some(JoypadBtnState::A), JoypadBtnState::TURBO_B if !pressed => Some(JoypadBtnState::B), _ => None, } { self.buttons.set(button, false); } self.buttons.set(button, pressed); } pub const fn from_bytes(val: u16) -> Self { Self { buttons: JoypadBtnState::from_bits_truncate(val), concurrent_dpad: false, index: 0, strobe: false, } } #[must_use] pub const fn read(&mut self) -> u8 { let val = self.peek(); if !self.strobe && self.index < 8 { self.index += 1; } val } #[must_use] pub const fn peek(&self) -> u8 { if self.index < 8 { ((self.buttons.bits() as u8) & (1 << self.index)) >> self.index } else { 0x01 } } pub const fn write(&mut self, val: u8) { let prev_strobe = self.strobe; self.strobe = val & 0x01 == 0x01; if prev_strobe && !self.strobe { self.index = 0; } } #[must_use] pub const fn index(&self) -> u8 { self.index } pub const fn clear(&mut self) { self.buttons = JoypadBtnState::empty(); } } impl Reset for Joypad { fn reset(&mut self, _kind: ResetKind) { self.buttons = JoypadBtnState::empty(); self.index = 0; self.strobe = false; } } #[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct Zapper { #[serde(skip)] // Don't save triggered state pub triggered: f32, pub trigger_release_delay: f32, #[serde(skip)] // Don't save zapper position pub x: u16, #[serde(skip)] // Don't save zapper position pub y: u16, pub radius: u16, pub connected: bool, } impl Zapper { #[inline(always)] #[must_use] pub const fn x(&self) -> u16 { self.x } #[inline(always)] #[must_use] pub const fn y(&self) -> u16 { self.y } #[inline(always)] pub fn trigger(&mut self) { if self.triggered <= 0.0 { self.triggered = self.trigger_release_delay; } } #[inline(always)] pub const fn aim(&mut self, x: u16, y: u16) { self.x = x; self.y = y; } pub const fn clear(&mut self) { self.triggered = 0.0; } } impl Zapper { fn new(region: NesRegion) -> Self { Self { triggered: 0.0, // Zapper takes ~100ms to change to "released" after trigger is pulled trigger_release_delay: Cpu::region_clock_rate(region) / 10.0, x: 0, y: 0, radius: 3, connected: false, } } #[must_use] fn read(&self, ppu: &Ppu) -> u8 { if self.connected { self.triggered() | self.light_sense(ppu) } else { 0x00 } } fn triggered(&self) -> u8 { if self.triggered > 0.0 { 0x10 } else { 0x00 } } fn light_sense(&self, ppu: &Ppu) -> u8 { let width = size::WIDTH; let height = size::HEIGHT; let scanline = ppu.scanline; let cycle = ppu.cycle; let min_y = self.y.saturating_sub(self.radius); let max_y = (self.y + self.radius).min(height - 1); let min_x = self.x.saturating_sub(self.radius); let max_x = (self.x + self.radius).min(width - 1); for y in min_y..=max_y { for x in min_x..=max_x { let behind_ppu = scanline >= y && (scanline - y) <= 20 && (scanline != y || cycle > x); let brightness = ppu.pixel_brightness(x, y); if behind_ppu && brightness >= 85 { trace!("zapper light: {brightness}"); return 0x00; } } } 0x08 } } impl Clock for Zapper { fn clock(&mut self) { if self.triggered > 0.0 { self.triggered -= 1.0; } } } impl Reset for Zapper { fn reset(&mut self, _kind: ResetKind) { self.triggered = 0.0; } } ================================================ FILE: tetanes-core/src/lib.rs ================================================ #![doc = include_str!("../README.md")] #![doc( html_favicon_url = "https://github.com/lukexor/tetanes/blob/main/assets/linux/icon.png?raw=true", html_logo_url = "https://github.com/lukexor/tetanes/blob/main/assets/linux/icon.png?raw=true" )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod action; pub mod apu; pub mod bus; pub mod cart; pub mod debug; pub mod fs; pub mod time; #[macro_use] pub mod common; pub mod control_deck; pub mod cpu; pub mod error; pub mod genie; pub mod input; pub mod mapper; pub mod mem; pub mod ppu; pub mod sys; pub mod video; pub mod prelude { //! The prelude re-exports all the common structs/enums used for basic NES emulation. pub use crate::{ action::Action, apu::{Apu, Channel}, cart::Cart, common::{Clock, NesRegion, Regional, Reset, ResetKind, Sample}, control_deck::{Config, ControlDeck, HeadlessMode}, cpu::Cpu, genie::GenieCode, input::{FourPlayer, Input, Player}, mapper::{Map, Mapper, MapperRevision}, mem::RamState, ppu::{Mirroring, Ppu}, video::Frame, }; } #[cfg(test)] mod tests { use super::prelude::*; use crate::{ apu::{ dmc::Dmc, filter::FilterChain, frame_counter::FrameCounter, noise::Noise, pulse::Pulse, triangle::Triangle, }, bus::{self, Bus}, cpu::{IrqFlags, Status, instr::AddrMode}, debug::PpuDebugger, mapper::{ Axrom, BandaiFCG, Bf909x, Bnrom, Cnrom, ColorDreams, Exrom, Fxrom, Gxrom, JalecoSs88006, Namco163, Nina001, Nina003006, Nrom, Pxrom, SunsoftFme7, Sxrom, Txrom, Uxrom, Vrc6, }, mem::{ConstArray, Memory}, ppu::{ CIRam, PaletteRam, ctrl::Ctrl, mask::Mask, scroll::Scroll, sprite::Sprite, status::Status as PpuStatus, }, }; use std::collections::HashMap; /// Utility to aid in struct field layout size and alignment. macro_rules! print_struct_layout { ($ty:ty, $($field:ident: $field_ty:ty),+$(,)?) => {{ use ::std::mem::{offset_of, size_of}; let mut field_rows = vec![ $( ( stringify!($field), offset_of!($ty, $field), size_of::<$field_ty>() ), )+ ]; field_rows.sort_by_key(|&(_, offset, _)| offset); println!("{} total size: {} bytes", stringify!($ty), size_of::<$ty>()); for (field, offset, size) in field_rows { println!(" {field:<25}: offset {offset:4}, size {size:4}"); } }}; } /// Utility to aid in enum size and alignment. macro_rules! print_enum_layout { ($ty:ty, $($variant:ident($variant_ty:ty)),+$(,)?) => {{ println!("{} enum: {} bytes", stringify!($ty), size_of::<$ty>()); $( println!(" {:<15}: size {:4}", stringify!($variant), size_of::<$variant_ty>()); )+ }} } // Utility to help print alignment and size of struct field for cache-optimization. #[test] fn print_layouts() { print_struct_layout!( Cpu, cycle: u32, master_clock: u32, start_cycles: u8, end_cycles: u8, pc: u16, operand: u16, addr_mode: AddrMode, sp: u8, acc: u8, x: u8, y: u8, status: Status, irq_flags: IrqFlags, bus: Bus, corrupted: bool, disasm: String, ); print_struct_layout!( Bus, wram: Memory>, open_bus: u8, ram_state: RamState, region: NesRegion, ppu: Ppu, apu: Apu, input: Input, genie_codes: HashMap, ); print_struct_layout!( Ppu, master_clock: u32, cycle: u16, scanline: u16, mask: Mask, ctrl: Ctrl, scroll: Scroll, tile_shift_lo: u16, tile_shift_hi: u16, tile_addr: u16, tile_lo: u8, tile_hi: u8, clock_divider: u8, open_bus: u8, reset_signal: bool, curr_palette: u8, prev_palette: u8, next_palette: u8, skip_rendering: bool, spr_count: u8, spr_in_range: bool, spr_zero_in_range: bool, spr_zero_visible: bool, oam_eval_done: bool, oamaddr: u8, oamaddr_lo: u8, oamaddr_hi: u8, secondary_oamaddr: u8, overflow_count: u8, oam_fetch: u8, vblank_scanline: u16, prerender_scanline: u16, is_visible_scanline: bool, is_prerender_scanline: bool, is_render_scanline: bool, is_pal_spr_eval_scanline: bool, status: PpuStatus, frame: Frame, ciram: CIRam, secondary_oamdata: ConstArray, sprites: Box<[Sprite]>, spr_present: ConstArray, oamdata: ConstArray, palette: PaletteRam, mapper: Mapper, vram_buffer: u8, prevent_vbl: bool, region: NesRegion, emulate_warmup: bool, debugger: PpuDebugger, ); print_struct_layout!( Apu, master_clock: u32, clock: u32, cpu_cycle: u32, should_clock: bool, sample_counter: f32, sample_period: f32, frame_counter: FrameCounter, pulse1: Pulse, pulse2: Pulse, triangle: Triangle, noise: Noise, dmc: Dmc, filter_chain: FilterChain, audio_samples: Vec, channel_outputs: Box<[f32]>, clock_rate: f32, sample_rate: f32, speed: f32, mapper_enabled: bool, region: NesRegion, skip_mixing: bool, ); print_enum_layout!( Mapper, Nrom(Nrom), Sxrom(Sxrom), Uxrom(Uxrom), Cnrom(Cnrom), Txrom(Txrom), Exrom(Exrom), Axrom(Axrom), Pxrom(Pxrom), Fxrom(Fxrom), ColorDreams(ColorDreams), BandaiFCG(BandaiFCG), JalecoSs88006(JalecoSs88006), Namco163(Namco163), Vrc6(Vrc6), Bnrom(Bnrom), Nina001(Nina001), Gxrom(Gxrom), SunsoftFme7(SunsoftFme7), Bf909x(Bf909x), Nina003006(Nina003006), ); } } ================================================ FILE: tetanes-core/src/mapper/bandai_fcg.rs ================================================ //! `Bandai FCG` (Mappers 016, 153, 157, and 159). //! //! use crate::{ cart::Cart, common::{Clock, Regional, Reset, Sram}, fs, mapper::{self, Map, Mapper, Mirroring}, mem::{Banks, Memory}, ppu::CIRam, }; use serde::{Deserialize, Serialize}; use std::{cmp::Ordering, path::Path}; /// `Bandai FCG` registers. #[derive(Default, Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Regs { pub prg_page: u8, pub prg_bank_select: u8, pub prg_ram_enabled: bool, pub chr_regs: [u8; 8], pub irq_latch: u8, pub irq_counter: u16, pub irq_enabled: bool, pub irq_pending: bool, pub irq_reload: u16, } /// Memory operation. #[derive(Default, Debug, Clone, Serialize, Deserialize)] #[must_use] pub enum MemoryOp { None, Read, Write, #[default] ReadWrite, } /// `Bandai FCG` (Mappers 016, 153, 157, and 159). #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct BandaiFCG { pub chr: Memory>, pub prg_rom: Memory>, pub prg_ram: Memory>, pub regs: Regs, pub has_chr_ram: bool, pub mirroring: Mirroring, pub mapper_num: u16, pub submapper_num: u8, pub barcode_reader: Option, pub standard_eeprom: Option, pub extra_eeprom: Option, pub sram_access: MemoryOp, pub reg_access: MemoryOp, pub chr_banks: Banks, pub prg_rom_banks: Banks, } impl BandaiFCG { const PRG_WINDOW: usize = 16 * 1024; const PRG_RAM_SIZE: usize = 8 * 1024; // Mapper 153 const CHR_ROM_WINDOW: usize = 1024; const CHR_RAM_SIZE: usize = 8 * 1024; pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, ) -> Result { let (chr, has_chr_ram) = cart.chr_rom_or_ram(chr_rom, Self::CHR_RAM_SIZE); let chr_window = if has_chr_ram { Self::CHR_RAM_SIZE } else { Self::CHR_ROM_WINDOW }; let chr_banks = Banks::new(0x0000, 0x1FFF, chr.len(), chr_window)?; let prg_rom_banks = Banks::new(0x8000, 0xFFFF, prg_rom.len(), Self::PRG_WINDOW)?; let prg_ram = cart.prg_ram_or_default(Self::PRG_RAM_SIZE); let mut bandai_fcg = Self { chr, prg_rom, prg_ram, regs: Regs::default(), has_chr_ram, mirroring: cart.mirroring(), mapper_num: cart.mapper_num(), submapper_num: cart.submapper_num(), barcode_reader: None, standard_eeprom: None, extra_eeprom: None, sram_access: MemoryOp::default(), reg_access: MemoryOp::Write, chr_banks, prg_rom_banks, }; // Mapper 157 is used for Datach Joint ROM System boards if bandai_fcg.mapper_num == 16 { // INES Mapper 016 submapper 4: FCG-1/2 ASIC, no serial EEPROM, banked CHR-ROM // INES Mapper 016 submapper 5: LZ93D50 ASIC and no or 256-byte serial EEPROM, banked // CHR-ROM // Add a 256 byte serial EEPROM (24C02) if matches!(bandai_fcg.submapper_num, 0 | 5) && bandai_fcg.prg_ram.len() == 256 { // Connect a 256-byte EEPROM for iNES roms, and when submapper 5 + 256 bytes of // save ram in header bandai_fcg.standard_eeprom = Some(Eeprom::new(EepromModel::X24C02)); } } else if bandai_fcg.mapper_num == 157 { bandai_fcg.barcode_reader = Some(BarcodeReader::new()); // Datach Joint ROM System // // It contains an internal 256-byte serial EEPROM (24C02) that is shared among all // Datach games. // // One game, Battle Rush: Build up Robot Tournament, has an additional external // 128-byte serial EEPROM (24C01) on the game cartridge. // // The NES 2.0 header's PRG-NVRAM field will only denote whether the game cartridge has // an additional 128-byte serial EEPROM if !cart.is_nes2() || bandai_fcg.prg_ram.len() == 128 { bandai_fcg.extra_eeprom = Some(Eeprom::new(EepromModel::X24C01)); } // All mapper 157 games have an internal 256-byte EEPROM bandai_fcg.standard_eeprom = Some(Eeprom::new(EepromModel::X24C02)); } else if bandai_fcg.mapper_num == 159 { // LZ93D50 with 128 byte serial EEPROM (24C01) bandai_fcg.standard_eeprom = Some(Eeprom::new(EepromModel::X24C01)); } if bandai_fcg.mapper_num == 16 { if matches!(bandai_fcg.submapper_num, 0 | 4) { bandai_fcg.reg_access = MemoryOp::Read; } if matches!(bandai_fcg.submapper_num, 0 | 5) { bandai_fcg.sram_access = MemoryOp::Read; } } else { // For iNES Mapper 153 (with SRAM), the writeable ports must only be mirrored across // $8000-$FFFF. Mappers 157 and 159 do not need to support the FCG-1 and -2 and so // should only mirror the ports across $8000-$FFFF. if bandai_fcg.mapper_num == 153 { // Mapper 153 has regular save ram from $6000-$7FFF, need to remove the register for both read & writes bandai_fcg.sram_access = MemoryOp::None; } else { bandai_fcg.sram_access = MemoryOp::Read; } } let last_bank = bandai_fcg.prg_rom_banks.last(); bandai_fcg.prg_rom_banks.set(1, last_bank); Ok(bandai_fcg.into()) } fn write_chr_bank(&mut self, addr: u16, val: u8) { let bank = usize::from(addr & 0x07); self.regs.chr_regs[bank] = val; if self.mapper_num == 153 || self.prg_rom_banks.page_count() >= 0x20 { self.regs.prg_bank_select = 0; for reg in self.regs.chr_regs { self.regs.prg_bank_select |= (reg & 0x01) << 4; } self.prg_rom_banks .set(0, (self.regs.prg_page | self.regs.prg_bank_select).into()); self.prg_rom_banks .set(1, 0x0F | usize::from(self.regs.prg_bank_select)); } else if !self.has_chr_ram && self.mapper_num != 157 { self.chr_banks.set(bank, val.into()); } if let Some(eeprom) = &mut self.extra_eeprom { if self.mapper_num == 157 && (addr & 0x0F) <= 3 { eeprom.write_scl((val >> 3) & 0x01) } } } fn write_prg_bank(&mut self, val: u8) { self.regs.prg_page = val & 0x0F; self.prg_rom_banks .set(0, (self.regs.prg_page | self.regs.prg_bank_select).into()); } const fn write_mirroring(&mut self, val: u8) { self.mirroring = match val & 0b11 { 0b00 => Mirroring::Vertical, 0b01 => Mirroring::Horizontal, 0b10 => Mirroring::SingleScreenA, _ => Mirroring::SingleScreenB, }; } const fn write_irq_ctrl(&mut self, val: u8) { self.regs.irq_enabled = val & 0x01 == 0x01; // Wiki claims there is no reload value, however this seems to be the only way to make // Famicom Jump II - Saikyou no 7 Nin work properly if self.mapper_num != 16 || !matches!(self.submapper_num, 0 | 4) { // On the LZ93D50 (Submapper 5), writing to this register also copies the latch to the // actual counter. self.regs.irq_counter = self.regs.irq_reload; } self.regs.irq_pending = false; } fn write_irq_latch(&mut self, addr: u16, val: u8) { let (mask, val) = if addr & 0x0C == 0x0C { (0x00FF, u16::from(val) << 8) } else { (0xFF00, u16::from(val)) }; if self.mapper_num != 16 || !matches!(self.submapper_num, 0 | 4) { // On the LZ93D50 (Submapper 5), these registers instead modify a latch that will only // be copied to the actual counter when register $800A is written to. self.regs.irq_reload = (self.regs.irq_reload & mask) | val; } else { // On the FCG-1/2 (Submapper 4), writing to these two registers directly // modifies the counter itself; all such games therefore disable counting before // changing the counter value. self.regs.irq_counter = (self.regs.irq_counter & mask) | val; } } fn write_eeprom_ctrl(&mut self, val: u8) { let sda = (val & 0x40) >> 6; if let Some(eeprom) = &mut self.standard_eeprom { let scl = (val & 0x20) >> 5; eeprom.write(scl, sda); } if let Some(eeprom) = &mut self.extra_eeprom { eeprom.write_sda(sda); } } #[inline(always)] pub const fn prg_ram_enabled(&self) -> bool { self.mapper_num == 153 && self.regs.prg_ram_enabled } } impl Map for BandaiFCG { // Mapper 016 // // PPU $0000..=$03FF 1K switchable CHR-ROM bank // PPU $0400..=$07FF 1K switchable CHR-ROM bank // PPU $0800..=$0BFF 1K switchable CHR-ROM bank // PPU $0c00..=$0FFF 1K switchable CHR-ROM bank // PPU $1000..=$13FF 1K switchable CHR-ROM bank // PPU $1400..=$17FF 1K switchable CHR-ROM bank // PPU $1800..=$1BFF 1K switchable CHR-ROM bank // PPU $1c00..=$1FFF 1K switchable CHR-ROM bank // CPU $8000..=$BFFF 16K switchable PRG-ROM bank // CPU $C000..=$FFFF 16K PRG-ROM bank, fixed to the last bank // // Mapper 153 // // CPU $6000..=$7FFF 8K battery-backed WRAM // CPU $8000..=$BFFF 16K switchable PRG-ROM bank // CPU $C000..=$FFFF 16K PRG-ROM bank, fixed to the last bank // PPU $0000..=$1FFF 8K fixed CHR-ROM bank // // Mapper 157 // // CPU $8000..=$BFFF 16K switchable PRG-ROM bank // CPU $C000..=$FFFF 16K PRG-ROM bank, fixed to the last bank // PPU $0000..=$1FFF 8K fixed CHR-ROM bank // // Mapper 159 // // PPU $0000..=$03FF 1K switchable CHR-ROM bank // PPU $0400..=$07FF 1K switchable CHR-ROM bank // PPU $0800..=$0BFF 1K switchable CHR-ROM bank // PPU $0c00..=$0FFF 1K switchable CHR-ROM bank // PPU $1000..=$13FF 1K switchable CHR-ROM bank // PPU $1400..=$17FF 1K switchable CHR-ROM bank // PPU $1800..=$1BFF 1K switchable CHR-ROM bank // PPU $1c00..=$1FFF 1K switchable CHR-ROM bank // CPU $8000..=$BFFF 16K switchable PRG-ROM bank // CPU $C000..=$FFFF 16K PRG-ROM bank, fixed to the last bank /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => self.chr[self.chr_banks.translate(addr)], 0x2000..=0x3EFF => ciram.peek(addr, self.mirroring), _ => 0, } } /// Read a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_read(&mut self, addr: u16) -> u8 { if matches!(addr, 0x6000..=0x7FFF) { if !matches!(self.sram_access, MemoryOp::Read | MemoryOp::ReadWrite) { return 0; } let mut val = 0x00; if let Some(barcode_reader) = &mut self.barcode_reader { val |= barcode_reader.read(); } if let (Some(eeprom1), Some(eeprom2)) = (&mut self.standard_eeprom, &mut self.extra_eeprom) { val |= (eeprom1.read() & eeprom2.read()) << 4; } else if let Some(eeprom) = &mut self.standard_eeprom { val |= eeprom.read() << 4; } val } else { self.prg_peek(addr) } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x6000..=0x7FFF if self.prg_ram_enabled() => self.prg_ram[usize::from(addr - 0x6000)], 0x8000..=0xFFFF => self.prg_rom[self.prg_rom_banks.translate(addr)], _ => 0, } } /// Write a byte to CHR-RAM/CIRAM at a given address. #[inline(always)] fn chr_write(&mut self, addr: u16, val: u8, ciram: &mut CIRam) { match addr { 0x0000..=0x1FFF => self.chr[self.chr_banks.translate(addr)] = val, 0x2000..=0x3EFF => ciram.write(addr, val, self.mirroring), _ => (), } } /// Write a byte to PRG-RAM at a given address. #[inline(always)] fn prg_write(&mut self, addr: u16, val: u8) { match addr { 0x6000..=0x7FFF if self.prg_ram_enabled() => { self.prg_ram[usize::from(addr - 0x6000)] = val; } 0x6000..=0xFFFF => match addr & 0x0F { 0x00..=0x07 => self.write_chr_bank(addr, val), 0x08 => self.write_prg_bank(val), 0x09 => self.write_mirroring(val), 0x0A => self.write_irq_ctrl(val), 0x0B..=0x0C => self.write_irq_latch(addr, val), 0x0D => { if self.mapper_num == 153 { self.regs.prg_ram_enabled = (val & 0x20) == 0x20; } else if matches!(self.sram_access, MemoryOp::Write | MemoryOp::ReadWrite) { self.write_eeprom_ctrl(val); } } _ => (), }, _ => (), } } /// Whether an IRQ is pending acknowledgement. fn irq_pending(&self) -> bool { self.regs.irq_pending } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Clock for BandaiFCG { fn clock(&mut self) { if let Some(barcode_reader) = &mut self.barcode_reader { barcode_reader.clock(); } // Checking counter before decrementing seems to be the only way to get both Famicom Jump // II - Saikyou no 7 Nin (J) and Magical Taruruuto-kun 2 - Mahou Daibouken (J) to work // without glitches with the same code. if self.regs.irq_enabled { if self.regs.irq_counter == 0 { self.regs.irq_pending = true; } self.regs.irq_counter = self.regs.irq_counter.wrapping_sub(1); } } } impl Sram for BandaiFCG { fn save(&self, path: impl AsRef) -> fs::Result<()> { if let Some(eeprom) = &self.standard_eeprom { eeprom.save(&path)?; } if let Some(eeprom) = &self.extra_eeprom { eeprom.save(&path)?; } Ok(()) } fn load(&mut self, path: impl AsRef) -> fs::Result<()> { if let Some(eeprom) = &mut self.standard_eeprom { eeprom.load(&path)?; } if let Some(eeprom) = &mut self.extra_eeprom { eeprom.load(&path)?; } Ok(()) } } impl Regional for BandaiFCG {} impl Reset for BandaiFCG {} #[derive(Default, Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct BarcodeReader { pub data: Box<[u8]>, pub master_clock: usize, pub insert_cycle: usize, pub new_barcode: u64, pub new_barcode_digit_count: u32, } impl BarcodeReader { pub fn new() -> Self { Self { data: Default::default(), master_clock: 0, insert_cycle: 0, new_barcode: 0, new_barcode_digit_count: 0, } } pub const fn read(&self) -> u8 { let elapsed_cycles = self.master_clock - self.insert_cycle; let bit_number = elapsed_cycles / 1000; if bit_number < self.data.len() { self.data[bit_number] } else { 0x00 } } pub const fn input(&mut self, barcode: u64, digit_count: u32) { self.new_barcode = barcode; self.new_barcode_digit_count = digit_count; } pub fn barcode(&self) -> String { format!( "{:0>width$}", self.new_barcode, width = self.new_barcode_digit_count as usize ) } pub fn init(&mut self) { self.insert_cycle = self.master_clock; static PREFIX_PARITY_TYPE: [[u8; 6]; 10] = [ [8, 8, 8, 8, 8, 8], [8, 8, 0, 8, 0, 0], [8, 8, 0, 0, 8, 0], [8, 8, 0, 0, 0, 8], [8, 0, 8, 8, 0, 0], [8, 0, 0, 8, 8, 0], [8, 0, 0, 0, 8, 8], [8, 0, 8, 0, 8, 0], [8, 0, 8, 0, 0, 8], [8, 0, 0, 8, 0, 8], ]; static DATA_LEFT_ODD: [[u8; 7]; 10] = [ [8, 8, 8, 0, 0, 8, 0], [8, 8, 0, 0, 8, 8, 0], [8, 8, 0, 8, 8, 0, 0], [8, 0, 0, 0, 0, 8, 0], [8, 0, 8, 8, 8, 0, 0], [8, 0, 0, 8, 8, 8, 0], [8, 0, 8, 0, 0, 0, 0], [8, 0, 0, 0, 8, 0, 0], [8, 0, 0, 8, 0, 0, 0], [8, 8, 8, 0, 8, 0, 0], ]; static DATA_LEFT_EVEN: [[u8; 7]; 10] = [ [8, 0, 8, 8, 0, 0, 0], [8, 0, 0, 8, 8, 0, 0], [8, 8, 0, 0, 8, 0, 0], [8, 0, 8, 8, 8, 8, 0], [8, 8, 0, 0, 0, 8, 0], [8, 0, 0, 0, 8, 8, 0], [8, 8, 8, 8, 0, 8, 0], [8, 8, 0, 8, 8, 8, 0], [8, 8, 8, 0, 8, 8, 0], [8, 8, 0, 8, 0, 0, 0], ]; static DATA_RIGHT: [[u8; 7]; 10] = [ [0, 0, 0, 8, 8, 0, 8], [0, 0, 8, 8, 0, 0, 8], [0, 0, 8, 0, 0, 8, 8], [0, 8, 8, 8, 8, 0, 8], [0, 8, 0, 0, 0, 8, 8], [0, 8, 8, 0, 0, 0, 8], [0, 8, 0, 8, 8, 8, 8], [0, 8, 8, 8, 0, 8, 8], [0, 8, 8, 0, 8, 8, 8], [0, 0, 0, 8, 0, 8, 8], ]; let barcode = self.barcode(); let mut codes = Vec::new(); for ch in barcode.chars() { codes.push(ch.to_digit(10).expect("valid barcode character") as usize); } let mut data = Vec::::with_capacity(256); data.extend([8; 33]); data.extend([0, 8, 0]); let mut sum = 0; if barcode.len() == 13 { for i in 0..6 { let odd = PREFIX_PARITY_TYPE[codes[0]][i] != 0; for j in 0..7 { data.push(if odd { DATA_LEFT_ODD[codes[i + 1]][j] } else { DATA_LEFT_EVEN[codes[i + 1]][j] }); } } data.extend([8, 0, 8, 0, 8]); for code in codes.iter().skip(7).take(5) { for code_data in DATA_RIGHT[*code].iter().take(7) { data.push(*code_data); } } for (i, code) in codes.iter().enumerate().take(12) { sum += if (i & 1) == 1 { *code * 3 } else { *code }; } } else { for code in codes.iter().take(4) { for code_data in DATA_LEFT_ODD[*code].iter().take(7) { data.push(*code_data); } } data.extend([8, 0, 8, 0, 8]); for code in codes.iter().skip(4).take(3) { for code_data in DATA_RIGHT[*code].iter().take(7) { data.push(*code_data); } } for (i, code) in codes.iter().enumerate().take(7) { sum += if (i & 1) == 1 { *code } else { *code * 3 }; } } sum = (10 - (sum % 10)) % 10; for sum_data in DATA_RIGHT[sum].iter().take(7) { data.push(*sum_data); } data.extend([0, 8, 0]); data.extend([8; 32]); self.data = data.into(); } } impl Clock for BarcodeReader { fn clock(&mut self) { self.master_clock += 1; } } #[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub enum EepromModel { X24C01, X24C02, } #[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub enum EepromMode { #[default] Idle, Addr, Read, Write, SendAck, WaitAck, ChipAddr, } #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Eeprom { pub model: EepromModel, pub mode: EepromMode, pub next_mode: EepromMode, pub chip_addr: u8, pub addr: u8, pub data: u8, pub counter: u8, pub output: u8, pub prev_scl: u8, pub prev_sda: u8, pub rom_data: Memory>, } impl Eeprom { pub fn new(model: EepromModel) -> Self { let rom_size = match model { EepromModel::X24C01 => 128, EepromModel::X24C02 => 256, }; Self { model, mode: EepromMode::default(), next_mode: EepromMode::default(), chip_addr: 0, addr: 0, data: 0, counter: 0, output: 0, prev_scl: 0, prev_sda: 0, rom_data: Memory::new(rom_size), } } pub const fn read(&self) -> u8 { self.output } pub fn write(&mut self, scl: u8, sda: u8) { match self.model { EepromModel::X24C01 => { if self.prev_scl > 0 && scl > 0 && sda < self.prev_sda { // START is identified by a high to low transition of the SDA line while the // clock SCL is *stable* in the high state self.mode = EepromMode::Addr; self.addr = 0; self.counter = 0; self.output = 1; } else if self.prev_scl > 0 && scl > 0 && sda > self.prev_sda { // STOP is identified by a low to high transition of the SDA line while the // clock SCL is *stable* in the high state self.mode = EepromMode::Idle; self.output = 1; } else if scl > self.prev_scl { // Clock rise match self.mode { EepromMode::Addr => { // To initiate a write operation, the master sends a start condition // followed by a seven bit word address and a write bit. match self.counter.cmp(&7) { Ordering::Less => { if let Some(addr) = self.write_bit(self.addr, sda) { self.addr = addr; } } Ordering::Equal => { // 8th bit to determine if we're in read or write mode self.counter = 8; if sda > 0 { self.next_mode = EepromMode::Read; self.data = self.rom_data[usize::from(self.addr & 0x7F)]; } else { self.next_mode = EepromMode::Write; } } _ => (), } } EepromMode::Read => self.read_bit(), EepromMode::Write => { if let Some(data) = self.write_bit(self.data, sda) { self.data = data; } } EepromMode::SendAck => self.output = 0, EepromMode::WaitAck if sda == 0 => { // We expected an ack, but received something else, return to idle // mode self.next_mode = EepromMode::Idle; } _ => (), } } else if scl < self.prev_scl { // Clock fall match self.mode { EepromMode::Addr if self.counter == 8 => { // After receiving the address, the X24C01 responds with an // acknowledge, then waits for eight bits of data self.mode = EepromMode::SendAck; self.output = 1; } EepromMode::SendAck => { // After sending an ack, move to the next mode of operation self.mode = self.next_mode; self.counter = 0; self.output = 1; } EepromMode::Read if self.counter == 8 => { // After sending all 8 bits, wait for an ack self.mode = EepromMode::WaitAck; self.addr = (self.addr + 1) & 0x7F; } EepromMode::Write if self.counter == 8 => { // After receiving all 8 bits, send an ack and then wait self.mode = EepromMode::SendAck; self.next_mode = EepromMode::Idle; self.rom_data[usize::from(self.addr & 0x7F)] = self.data; self.addr = (self.addr + 1) & 0x7F; } _ => (), } } self.prev_scl = scl; self.prev_sda = sda; } EepromModel::X24C02 => { if self.prev_scl > 0 && scl > 0 && sda < self.prev_sda { // START is identified by a high to low transition of the SDA line while the // clock SCL is *stable* in the high state self.mode = EepromMode::ChipAddr; self.counter = 0; self.output = 1; } else if self.prev_scl > 0 && scl > 0 && sda > self.prev_sda { // STOP is identified by a low to high transition of the SDA line while the // clock SCL is *stable* in the high state self.mode = EepromMode::Idle; self.output = 1; } else if scl > self.prev_scl { // Clock rise match self.mode { EepromMode::ChipAddr => { if let Some(chip_addr) = self.write_bit(self.chip_addr, sda) { self.chip_addr = chip_addr; } } EepromMode::Addr => { if let Some(addr) = self.write_bit(self.addr, sda) { self.addr = addr; } } EepromMode::Read => self.read_bit(), EepromMode::Write => { if let Some(data) = self.write_bit(self.data, sda) { self.data = data; } } EepromMode::SendAck => self.output = 0, EepromMode::WaitAck if sda == 0 => { self.next_mode = EepromMode::Read; self.data = self.rom_data[usize::from(self.addr)]; } _ => (), } } else if scl < self.prev_scl { // Clock fall match self.mode { // Upon a correct compare the X24C02 outputs an acknowledge on the SDA line EepromMode::ChipAddr if self.counter == 8 => { if (self.chip_addr & 0xA0) == 0xA0 { self.mode = EepromMode::SendAck; self.counter = 0; self.output = 1; // The last bit of the slave address defines the operation to // be performed. When set to one a read operation is selected, // when set to zero a write operations is selected if (self.chip_addr & 0x01) == 0x01 { // Current Address Read // Upon receipt of the slave address with the R/W bit set // to one, the X24C02 issues an acknowledge and transmits // the eight bit word during the next eight clock cycles self.next_mode = EepromMode::Read; self.data = self.rom_data[usize::from(self.addr)]; } else { self.mode = EepromMode::Addr; } } else { // This chip wasn't selected, go back to idle mode self.mode = EepromMode::Idle; self.counter = 0; self.output = 1; } } EepromMode::Addr if self.counter == 8 => { // Finished receiving all 8 bits of the address, send an ack and then starting writing the value self.mode = EepromMode::SendAck; self.next_mode = EepromMode::Write; self.counter = 0; self.output = 1; } EepromMode::Read if self.counter == 8 => { // After sending all 8 bits, wait for an ack self.mode = EepromMode::WaitAck; self.addr = self.addr.wrapping_add(1); } EepromMode::Write if self.counter == 8 => { // After receiving all 8 bits, send an ack and then wait self.mode = EepromMode::SendAck; self.next_mode = EepromMode::Write; self.counter = 0; self.rom_data[usize::from(self.addr)] = self.data; self.addr = self.addr.wrapping_add(1); } EepromMode::SendAck | EepromMode::WaitAck => { self.mode = self.next_mode; self.counter = 0; self.output = 1; } _ => (), } } self.prev_scl = scl; self.prev_sda = sda; } } } pub fn write_scl(&mut self, scl: u8) { self.write(scl, self.prev_sda); } pub fn write_sda(&mut self, sda: u8) { self.write(self.prev_scl, sda); } pub const fn write_bit(&mut self, dest: u8, val: u8) -> Option { if self.counter < 8 { let mask = !(1 << self.counter); let dest = (dest & mask) | (val << self.counter); self.counter += 1; Some(dest) } else { None } } pub const fn read_bit(&mut self) { if self.counter < 8 { self.output = if self.data & (1 << self.counter) > 0 { 1 } else { 0 }; self.counter += 1; } } pub const fn sram_extension(&self) -> &str { match self.model { EepromModel::X24C01 => "eeprom128", EepromModel::X24C02 => "eeprom256", } } } impl Sram for Eeprom { fn save(&self, path: impl AsRef) -> fs::Result<()> { let extension = self.sram_extension(); fs::save(path.as_ref().with_extension(extension), &self.rom_data) } fn load(&mut self, path: impl AsRef) -> fs::Result<()> { let extension = self.sram_extension(); fs::load(path.as_ref().with_extension(extension)).map(|data| self.rom_data = data) } } #[cfg(test)] mod tests { use super::*; #[test] fn bandai_fcg_barcode_formatting() { let mut reader = BarcodeReader::new(); reader.input(4902425679235, 13); assert_eq!(reader.barcode(), "4902425679235"); // Test zero-padding for EAN-8 reader.input(1234567, 8); assert_eq!(reader.barcode(), "01234567"); } #[test] fn bandai_fcg_ean13_checksum() { // EAN-13: first 12 digits -> checksum is 13th // 490242567923 -> check digit 5 let mut reader = BarcodeReader::new(); reader.input(4902425679235, 13); reader.init(); // The checksum is encoded in the last 7 data bits before the end guard // End guard is [0,8,0] + 32x8, so check digit encoding ends at len-35 let check_digit_pattern = &reader.data[reader.data.len() - 35 - 7..reader.data.len() - 35]; // DATA_RIGHT[5] = [0, 8, 8, 0, 0, 0, 8] assert_eq!(check_digit_pattern, &[0, 8, 8, 0, 0, 0, 8]); } #[test] fn bandai_fcg_ean13_structure() { let mut reader = BarcodeReader::new(); reader.input(4902425679235, 13); reader.init(); // EAN-13 total: 33 (quiet) + 3 (start) + 42 (left) + 5 (center) + 42 (right) + 3 (end) + 32 (quiet) // = 33 + 3 + 42 + 5 + 42 + 3 + 32 = 160 assert_eq!(reader.data.len(), 160); // Start guard at index 33 assert_eq!(&reader.data[33..36], &[0, 8, 0]); // Center guard at index 33 + 3 + 42 = 78 assert_eq!(&reader.data[78..83], &[8, 0, 8, 0, 8]); // End guard at index 83 + 35 = 125 (after 5 digits * 7 bits) assert_eq!(&reader.data[125..128], &[0, 8, 0]); } #[test] fn bandai_fcg_ean8_structure() { let mut reader = BarcodeReader::new(); // Valid EAN-8: 12345670 (checksum 0) reader.input(12345670, 8); reader.init(); // EAN-8: 33 + 3 + 28 + 5 + 28 + 3 + 32 = 132 assert_eq!(reader.data.len(), 132); } } ================================================ FILE: tetanes-core/src/mapper/m000_nrom.rs ================================================ //! `NROM` (Mapper 000). //! //! use crate::{ cart::Cart, common::{Clock, Regional, Reset, ResetKind, Sram}, mapper::{self, Map, Mapper}, mem::{Memory, RamState}, ppu::{CIRam, Mirroring}, }; use serde::{Deserialize, Serialize}; /// `NROM` (Mapper 000). #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Nrom { pub chr: Memory>, pub prg_rom: Memory>, pub prg_ram: Memory>, pub mirroring: Mirroring, pub has_chr_ram: bool, pub mirror_prg_rom: bool, pub ram_state: RamState, } impl Nrom { const PRG_RAM_SIZE: usize = 8 * 1024; const CHR_RAM_SIZE: usize = 8 * 1024; /// Load `Nrom` from `Cart`. pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, ) -> Result { // NROM doesn't have CHR-RAM - but a lot of homebrew games use Mapper 000 with CHR-RAM, so // we'll provide some if no CHR-ROM is available. let (chr, has_chr_ram) = cart.chr_rom_or_ram(chr_rom, Self::CHR_RAM_SIZE); let nrom = Self { chr, prg_rom, // Family Basic only supported 2-4K of PRG-RAM, but we'll provide 8K by default. prg_ram: Memory::with_ram_state(Self::PRG_RAM_SIZE, cart.ram_state), mirroring: cart.mirroring(), has_chr_ram, mirror_prg_rom: cart.prg_rom_size <= 0x4000, ram_state: cart.ram_state, }; Ok(nrom.into()) } } impl Map for Nrom { // PPU $0000..=$1FFF 8K Fixed CHR-ROM Bank // CPU $6000..=$7FFF 2K or 4K PRG-RAM Family Basic only. 8K is provided by default. // CPU $8000..=$BFFF 16K PRG-ROM Bank 1 for NROM128 or NROM256 // CPU $C000..=$FFFF 16K PRG-ROM Bank 2 for NROM256 or Bank 1 Mirror for NROM128 /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x2000..=0x3EFF => ciram.peek(addr, self.mirroring), 0x0000..=0x1FFF => self.chr[usize::from(addr)], _ => 0, } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x8000..=0xBFFF => self.prg_rom[usize::from(addr & 0x3FFF)], 0xC000..=0xFFFF => { let mirror = if self.mirror_prg_rom { 0x3FFF } else { 0x7FFF }; self.prg_rom[usize::from(addr & mirror)] } 0x6000..=0x7FFF => self.prg_ram[usize::from(addr & 0x1FFF)], _ => 0, } } /// Write a byte to CHR-RAM/CIRAM at a given address. #[inline(always)] fn chr_write(&mut self, addr: u16, val: u8, ciram: &mut CIRam) { match addr { 0x2000..=0x3EFF => ciram.write(addr, val, self.mirroring), 0x0000..=0x1FFF if self.has_chr_ram => self.chr[usize::from(addr)] = val, _ => (), } } /// Write a byte to PRG-RAM at a given address. #[inline(always)] fn prg_write(&mut self, addr: u16, val: u8) { if let 0x6000..=0x7FFF = addr { self.prg_ram[usize::from(addr & 0x1FFF)] = val; } } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for Nrom { fn reset(&mut self, kind: ResetKind) { if kind == ResetKind::Hard { self.ram_state.fill(&mut self.prg_ram); } } } impl Clock for Nrom {} impl Regional for Nrom {} impl Sram for Nrom {} ================================================ FILE: tetanes-core/src/mapper/m001_sxrom.rs ================================================ //! `SxROM`/`MMC1` (Mapper 001). //! //! //! use crate::{ cart::Cart, common::{Clock, Regional, Reset, ResetKind, Sram}, fs, mapper::{self, Map, Mapper}, mem::{Banks, Memory}, ppu::{CIRam, Mirroring}, }; use serde::{Deserialize, Serialize}; use std::path::Path; /// MMC1 Revision. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[must_use] pub enum Revision { /// MMC1 Revision A A, /// MMC1 Revisions B & C #[default] BC, } /// `SxROM` registers. #[derive(Clone, Serialize, Deserialize)] #[must_use] pub struct Regs { write_just_occurred: u8, write_buffer: u8, // $8000-$FFFF - 5 bit shift register shift_count: u8, // How many times write_buffer has shifted prg_ram_disabled: bool, // $E000-$FFFF bit 4 chr_mode: bool, // $8000-$9FFF bit 4 prg_mode: bool, // $8000-$9FFF bits 3 prg_bank_select: bool, // $8000-$9FFF bit 2 last_chr_reg: u16, // Last chr register written to chr0: u8, // $A000-$BFFF chr1: u8, // $C000-$DFFF prg: u8, // $E000-$FFFF bits 0-3 } /// `SxROM`/`MMC1` (Mapper 001). #[derive(Clone, Serialize, Deserialize)] #[must_use] pub struct Sxrom { pub chr: Memory>, pub prg_rom: Memory>, pub prg_ram: Memory>, pub chr_banks: Banks, pub prg_ram_banks: Banks, pub prg_rom_banks: Banks, pub regs: Regs, pub has_chr_ram: bool, pub submapper_num: u8, pub mirroring: Mirroring, pub revision: Revision, pub prg_select: bool, } impl Sxrom { const PRG_RAM_WINDOW: usize = 8 * 1024; const PRG_ROM_WINDOW: usize = 16 * 1024; const CHR_WINDOW: usize = 4 * 1024; const PRG_RAM_SIZE: usize = 32 * 1024; // 32K is safely compatible sans NES 2.0 header const CHR_RAM_SIZE: usize = 8 * 1024; const SHIFT_REG_RESET: u8 = 0x80; // Reset shift register when bit 7 is set const MIRRORING_MASK: u8 = 0x03; // 0b00011 const SLOT_SELECT_MASK: u8 = 0x04; // 0b00100 const PRG_MODE_MASK: u8 = 0x08; // 0b01000 const CHR_MODE_MASK: u8 = 0x10; // 0b10000 const DEFAULT_PRG_MODE: u8 = 0x0C; // Mode 3, 16k Fixed Last const CHR_BANK_MASK: u8 = 0x1F; const PRG_BANK_MASK: u8 = 0x0F; const PRG_RAM_DISABLED: u8 = 0x10; // 0b10000 /// Load `Sxrom` from `Cart`. pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, revision: Revision, ) -> Result { let (chr, has_chr_ram) = cart.chr_rom_or_ram(chr_rom, Self::CHR_RAM_SIZE); let prg_ram = cart.prg_ram_or_default(Self::PRG_RAM_SIZE); let chr_banks = Banks::new(0x0000, 0x1FFF, chr.len(), Self::CHR_WINDOW)?; let prg_ram_banks = Banks::new(0x6000, 0x7FFF, prg_ram.len(), Self::PRG_RAM_WINDOW)?; let prg_rom_banks = Banks::new(0x8000, 0xFFFF, prg_rom.len(), Self::PRG_ROM_WINDOW)?; let mut sxrom = Self { prg_rom, chr, prg_ram, chr_banks, prg_ram_banks, prg_rom_banks, regs: Regs { write_just_occurred: 0x00, write_buffer: 0x00, shift_count: 0, prg_ram_disabled: false, chr_mode: false, prg_mode: false, prg_bank_select: false, last_chr_reg: 0xA000, chr0: 0x00, chr1: 0x00, prg: 0x00, }, has_chr_ram, submapper_num: cart.submapper_num(), mirroring: Mirroring::SingleScreenA, revision, prg_select: cart.prg_rom_size == 0x80000, }; sxrom.process_register_write(0x8000, Self::DEFAULT_PRG_MODE); sxrom.process_register_write(0xA000, 0x00); sxrom.process_register_write(0xC000, 0x00); sxrom.process_register_write( 0xE000, if revision == Revision::BC { 0x00 } else { Self::PRG_RAM_DISABLED }, ); sxrom.regs.last_chr_reg = 0xA000; sxrom.update_state(); Ok(sxrom.into()) } /// Reset the shift register write buffer. const fn reset_buffer(&mut self) { self.regs.shift_count = 0; self.regs.write_buffer = 0; } /// Process register write, extracting registers into flags. const fn process_register_write(&mut self, addr: u16, val: u8) { match addr & 0xE000 { 0x8000 => { self.mirroring = match val & Self::MIRRORING_MASK { 0b00 => Mirroring::SingleScreenA, 0b01 => Mirroring::SingleScreenB, 0b10 => Mirroring::Vertical, _ => Mirroring::Horizontal, }; self.regs.prg_bank_select = (val & Self::SLOT_SELECT_MASK) != 0; self.regs.prg_mode = (val & Self::PRG_MODE_MASK) != 0; self.regs.chr_mode = (val & Self::CHR_MODE_MASK) != 0; } 0xA000 => { self.regs.last_chr_reg = addr; self.regs.chr0 = val & Self::CHR_BANK_MASK; } 0xC000 => { self.regs.last_chr_reg = addr; self.regs.chr1 = val & Self::CHR_BANK_MASK; } 0xE000 => { self.regs.prg = val & Self::PRG_BANK_MASK; self.regs.prg_ram_disabled = (val & Self::PRG_RAM_DISABLED) != 0; } _ => (), } } /// Update internal state based on register flags. pub fn update_state(&mut self) { let extra_reg = if self.regs.last_chr_reg == 0xC000 && self.regs.chr_mode { self.regs.chr1 } else { self.regs.chr0 }; let prg_bank_select = if self.prg_select { extra_reg & Self::CHR_MODE_MASK } else { 0x00 }; if self.submapper_num == 5 { // Fixed PRG SEROM, SHROM, SH1ROM use a fixed 32k PRG-ROM with no banking support. self.prg_rom_banks.set_range(0, 1, 0); } else if self.regs.prg_mode { if self.regs.prg_bank_select { self.prg_rom_banks .set(0, (self.regs.prg | prg_bank_select).into()); self.prg_rom_banks .set(1, (Self::PRG_BANK_MASK | prg_bank_select).into()); } else { self.prg_rom_banks.set(1, prg_bank_select.into()); self.prg_rom_banks .set(1, (self.regs.prg | prg_bank_select).into()); } } else { self.prg_rom_banks .set_range(0, 1, ((self.regs.prg & 0xFE) | prg_bank_select).into()); // ignore low bit } if self.regs.chr_mode { self.chr_banks.set(0, self.regs.chr0.into()); self.chr_banks.set(1, self.regs.chr1.into()); } else { self.chr_banks.set(0, (self.regs.chr0 & 0x1E).into()); // ignore low bit self.chr_banks.set(1, ((self.regs.chr0 & 0x1E) + 1).into()); // ignore low bit } } #[inline(always)] pub fn prg_ram_enabled(&self) -> bool { self.revision == Revision::A || !self.regs.prg_ram_disabled } pub const fn set_revision(&mut self, revision: Revision) { self.revision = revision; } } impl Map for Sxrom { // PPU $0000..=$1FFF 4K CHR-ROM/RAM Bank Switchable // CPU $6000..=$7FFF 8K PRG-RAM Bank (optional) // CPU $8000..=$BFFF 16K PRG-ROM Bank Switchable or Fixed to First Bank // CPU $C000..=$FFFF 16K PRG-ROM Bank Fixed to Last Bank or Switchable /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => self.chr[self.chr_banks.translate(addr)], 0x2000..=0x3EFF => ciram.peek(addr, self.mirroring), _ => 0, } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x6000..=0x7FFF if self.prg_ram_enabled() => { self.prg_ram[self.prg_ram_banks.translate(addr)] } 0x8000..=0xFFFF => self.prg_rom[self.prg_rom_banks.translate(addr)], _ => 0, } } /// Write a byte to CHR-RAM/CIRAM at a given address. #[inline(always)] fn chr_write(&mut self, addr: u16, val: u8, ciram: &mut CIRam) { match addr { 0x0000..=0x1FFF if self.has_chr_ram => self.chr[self.chr_banks.translate(addr)] = val, 0x2000..=0x3EFF => ciram.write(addr, val, self.mirroring), _ => (), } } /// Write a byte to PRG-RAM at a given address. #[inline(always)] fn prg_write(&mut self, addr: u16, val: u8) { match addr { 0x6000..=0x7FFF if self.prg_ram_enabled() => { self.prg_ram[self.prg_ram_banks.translate(addr)] = val; } 0x8000..=0xFFFF => { // Writes data into a shift register. At every 5th // write, the data is written out to the `SxROM` registers // and the shift register is cleared // // Load Register $8000-$FFFF // 7654 3210 // Rxxx xxxD // | +- Data bit to be shifted into shift register, LSB first // +--------- 1: Reset shift register and write control with (Control OR $0C), // locking PRG-ROM at $C000-$FFFF to the last bank. // // Control $8000-$9FFF // 43210 // CPPMM // |||++- Mirroring (0: one-screen, lower bank; 1: one-screen, upper bank; // ||| 2: vertical; 3: horizontal) // |++--- PRG-ROM bank mode (0, 1: switch 32K at $8000, ignoring low bit of bank number; // | 2: fix first bank at $8000 and switch 16K bank at $C000; // | 3: fix last bank at $C000 and switch 16K bank at $8000) // +----- CHR-ROM bank mode (0: switch 8K at a time; 1: switch two separate 4K banks) // // CHR bank 0 $A000-$BFFF // 42310 // CCCCC // +++++- Select 4K or 8K CHR bank at PPU $0000 (low bit ignored in 8K mode) // // CHR bank 1 $C000-$DFFF // 43210 // CCCCC // +++++- Select 4K CHR bank at PPU $1000 (ignored in 8K mode) // // For Mapper001 // $A000 and $C000: // 43210 // EDCBA // ||||| // ||||+- CHR A12 // |||+-- CHR A13, if extant (CHR >= 16k) // ||+--- CHR A14, if extant; and PRG-RAM A14, if extant (PRG-RAM = 32k) // |+---- CHR A15, if extant; and PRG-RAM A13, if extant (PRG-RAM >= 16k) // +----- CHR A16, if extant; and PRG-ROM A18, if extant (PRG-ROM = 512k) // // PRG bank $E000-$FFFF // 43210 // RPPPP // |++++- Select 16K PRG-ROM bank (low bit ignored in 32K mode) // +----- PRG-RAM chip enable (0: enabled; 1: disabled; ignored on MMC1A) if self.regs.write_just_occurred > 0 { return; } self.regs.write_just_occurred = 2; if val & Self::SHIFT_REG_RESET == Self::SHIFT_REG_RESET { self.reset_buffer(); self.regs.prg_mode = true; self.regs.prg_bank_select = true; self.update_state(); } else { // Move shift register and write lowest bit of val self.regs.write_buffer >>= 1; self.regs.write_buffer |= (val << 4) & 0x10; self.regs.shift_count += 1; // Check if its time to write if self.regs.shift_count == 5 { self.process_register_write(addr, self.regs.write_buffer); self.update_state(); self.reset_buffer(); } } } _ => (), } } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for Sxrom { fn reset(&mut self, kind: ResetKind) { self.reset_buffer(); self.regs.prg_mode = true; self.regs.prg_bank_select = true; self.update_state(); if kind == ResetKind::Hard { self.regs.write_just_occurred = 0; self.regs.prg_ram_disabled = false; } } } impl Clock for Sxrom { fn clock(&mut self) { if self.regs.write_just_occurred > 0 { self.regs.write_just_occurred -= 1; } } } impl Sram for Sxrom { /// Save RAM to a given path. fn save(&self, path: impl AsRef) -> fs::Result<()> { fs::save(path.as_ref(), &self.prg_ram) } /// Load save RAM from a given path. fn load(&mut self, path: impl AsRef) -> fs::Result<()> { fs::load(path.as_ref()).map(|data: Memory>| self.prg_ram = data) } } impl Regional for Sxrom {} impl std::fmt::Debug for Sxrom { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SxRom") .field("regs", &self.regs) .field("submapper_num", &self.submapper_num) .field("mirroring", &self.mirroring) .field("revision", &self.revision) .field("prg_select", &self.prg_select) .field("chr_banks", &self.chr_banks) .field("prg_ram_banks", &self.prg_ram_banks) .field("prg_ram_enabled", &self.prg_ram_enabled()) .field("prg_rom_banks", &self.prg_rom_banks) .finish() } } impl std::fmt::Debug for Regs { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SxRegs") .field("write_just_occurred", &self.write_just_occurred) .field("write_buffer", &format_args!("0b{:08b}", self.write_buffer)) .field("shift_count", &self.shift_count) .field("prg_ram_disabled", &self.prg_ram_disabled) .field("chr_mode", &self.chr_mode) .field("prg_mode", &self.prg_mode) .field("prg_bank_select", &self.prg_bank_select) .field("last_chr_reg", &self.last_chr_reg) .field("chr0", &format_args!("${:02X}", self.chr0)) .field("chr1", &format_args!("${:02X}", self.chr1)) .field("prg", &format_args!("${:02X}", self.prg)) .finish() } } ================================================ FILE: tetanes-core/src/mapper/m002_uxrom.rs ================================================ //! `UxROM` (Mapper 002). //! //! use crate::{ cart::Cart, common::{Clock, Regional, Reset, Sram}, mapper::{self, Map, Mapper}, mem::{Banks, Memory}, ppu::{CIRam, Mirroring}, }; use serde::{Deserialize, Serialize}; /// `UxROM` (Mapper 002). #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Uxrom { pub chr: Memory>, pub prg_rom: Memory>, pub has_chr_ram: bool, pub mirroring: Mirroring, pub prg_rom_banks: Banks, } impl Uxrom { const PRG_ROM_WINDOW: usize = 16 * 1024; const CHR_RAM_SIZE: usize = 8 * 1024; /// Load `Uxrom` from `Cart`. pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, ) -> Result { let (chr, has_chr_ram) = cart.chr_rom_or_ram(chr_rom, Self::CHR_RAM_SIZE); let prg_rom_banks = Banks::new(0x8000, 0xFFFF, prg_rom.len(), Self::PRG_ROM_WINDOW)?; let mut uxrom = Self { chr, prg_rom, has_chr_ram, mirroring: cart.mirroring(), prg_rom_banks, }; uxrom.prg_rom_banks.set(1, uxrom.prg_rom_banks.last()); Ok(uxrom.into()) } } impl Map for Uxrom { // PPU $0000..=$1FFF 8K Fixed CHR-ROM/CHR-RAM Bank // CPU $8000..=$BFFF 16K PRG-ROM Bank Switchable // CPU $C000..=$FFFF 16K PRG-ROM Fixed to Last Bank /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => self.chr[usize::from(addr)], 0x2000..=0x3EFF => ciram.peek(addr, self.mirroring), _ => 0, } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x8000..=0xFFFF => self.prg_rom[self.prg_rom_banks.translate(addr)], _ => 0, } } /// Write a byte to CHR-RAM/CIRAM at a given address. #[inline(always)] fn chr_write(&mut self, addr: u16, val: u8, ciram: &mut CIRam) { match addr { 0x0000..=0x1FFF => self.chr[usize::from(addr)] = val, 0x2000..=0x3EFF => ciram.write(addr, val, self.mirroring), _ => (), } } /// Write a byte to PRG-RAM at a given address. #[inline(always)] fn prg_write(&mut self, addr: u16, val: u8) { if let 0x8000..=0xFFFF = addr { self.prg_rom_banks.set(0, val.into()) } } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for Uxrom {} impl Clock for Uxrom {} impl Regional for Uxrom {} impl Sram for Uxrom {} ================================================ FILE: tetanes-core/src/mapper/m003_cnrom.rs ================================================ //! `CNROM` (Mapper 003). //! //! //! use crate::{ cart::Cart, common::{Clock, Regional, Reset, Sram}, mapper::{self, Map, Mapper}, mem::{Banks, Memory}, ppu::{CIRam, Mirroring}, }; use serde::{Deserialize, Serialize}; /// `CNROM` (Mapper 003). #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Cnrom { pub chr_rom: Memory>, pub prg_rom: Memory>, pub mirroring: Mirroring, pub chr_banks: Banks, pub mirror_prg_rom: bool, } impl Cnrom { const CHR_ROM_WINDOW: usize = 8 * 1024; /// Load `Cnrom` from `Cart`. pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, ) -> Result { let chr_banks = Banks::new(0x0000, 0x1FFFF, chr_rom.len(), Self::CHR_ROM_WINDOW)?; let mirror_prg_rom = prg_rom.len() <= 0x4000; let cnrom = Self { chr_rom, prg_rom, mirroring: cart.mirroring(), chr_banks, mirror_prg_rom, }; Ok(cnrom.into()) } } impl Map for Cnrom { // PPU $0000..=$1FFF 8K CHR-ROM Banks Switchable // CPU $8000..=$BFFF 16K PRG-ROM Bank Fixed // CPU $C000..=$FFFF 16K PRG-ROM Bank Fixed or Bank 1 Mirror if only 16 KB PRG-ROM /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => self.chr_rom[self.chr_banks.translate(addr)], 0x2000..=0x3EFF => ciram.peek(addr, self.mirroring), _ => 0, } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x8000..=0xBFFF => self.prg_rom[usize::from(addr & 0x3FFF)], 0xC000..=0xFFFF => { let mirror = if self.mirror_prg_rom { 0x3FFF } else { 0x7FFF }; self.prg_rom[usize::from(addr & mirror)] } _ => 0, } } /// Write a byte to PRG-RAM at a given address. #[inline(always)] fn prg_write(&mut self, addr: u16, val: u8) { if let 0x8000..=0xFFFF = addr { self.chr_banks.set(0, val.into()) } } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for Cnrom {} impl Clock for Cnrom {} impl Regional for Cnrom {} impl Sram for Cnrom {} ================================================ FILE: tetanes-core/src/mapper/m004_txrom.rs ================================================ //! `TxROM`/`MMC3` (Mapper 004). //! //! //! use crate::{ cart::Cart, common::{Clock, Regional, Reset, ResetKind, Sram}, fs, mapper::{self, Map, Mapper}, mem::{Banks, Memory}, ppu::{CIRam, Mirroring}, }; use serde::{Deserialize, Serialize}; use std::path::Path; /// MMC3 Revision. /// /// See: /// /// Known Revisions: /// /// Conquest of the Crystal Palace (MMC3B S 9039 1 DB) /// Kickle Cubicle (MMC3B S 9031 3 DA) /// M.C. Kids (MMC3B S 9152 3 AB) /// Mega Man 3 (MMC3B S 9046 1 DB) /// Super Mario Bros. 3 (MMC3B S 9027 5 A) /// Startropics (MMC6B P 03'5) /// Batman (MMC3B 9006KP006) /// Golgo 13: The Mafat Conspiracy (MMC3B 9016KP051) /// Crystalis (MMC3B 9024KPO53) /// Legacy of the Wizard (MMC3A 8940EP) /// /// Only major difference is the IRQ counter #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[must_use] pub enum Revision { /// MMC3 Revision A A, /// MMC3 Revisions B & C #[default] BC, /// Acclaims MMC3 clone - clocks on falling edge Acc, } /// `TxROM` Registers. #[derive(Default, Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Regs { pub bank_select: u8, pub bank_values: [u8; 8], pub irq_latch: u8, pub irq_counter: u8, pub irq_enabled: bool, pub irq_pending: bool, pub irq_reload: bool, pub master_clock: u32, pub a12_low_clock: u32, } /// `TxROM`/`MMC3` (Mapper 004). #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Txrom { pub chr: Memory>, pub prg_rom: Memory>, pub prg_ram: Memory>, pub ex_ram: Memory>, pub regs: Regs, pub has_chr_ram: bool, pub mirroring: Mirroring, pub mapper_num: u16, pub submapper_num: u8, pub revision: Revision, pub chr_banks: Banks, pub prg_ram_banks: Banks, pub prg_rom_banks: Banks, } impl Txrom { const PRG_WINDOW: usize = 8 * 1024; const CHR_WINDOW: usize = 1024; const CHR_WINDOW_76: usize = 2048; const FOUR_SCREEN_RAM_SIZE: usize = 4 * 1024; const PRG_RAM_SIZE: usize = 8 * 1024; const CHR_RAM_SIZE: usize = 8 * 1024; const PRG_MODE_MASK: u8 = 0x40; // Bit 6 of bank select const CHR_INVERSION_MASK: u8 = 0x80; // Bit 7 of bank select /// Create `Txrom` from `Cart`. pub fn new( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, chr_window: usize, ) -> Result { let (chr, has_chr_ram) = cart.chr_rom_or_ram(chr_rom, Self::CHR_RAM_SIZE); let prg_ram = Memory::with_ram_state(Self::PRG_RAM_SIZE, cart.ram_state); let chr_banks = Banks::new(0x0000, 0x1FFF, chr.len(), chr_window)?; let prg_ram_banks = Banks::new(0x6000, 0x7FFF, prg_ram.len(), Self::PRG_WINDOW)?; let prg_rom_banks = Banks::new(0x8000, 0xFFFF, prg_rom.len(), Self::PRG_WINDOW)?; let mut txrom = Self { chr, prg_rom, prg_ram, ex_ram: if cart.mirroring() == Mirroring::FourScreen { Memory::new(Self::FOUR_SCREEN_RAM_SIZE) } else { Memory::empty() }, regs: Regs::default(), has_chr_ram, mirroring: cart.mirroring(), mapper_num: cart.mapper_num(), submapper_num: cart.submapper_num(), revision: Revision::BC, // TODO compare to known games chr_banks, prg_ram_banks, prg_rom_banks, }; let last_bank = txrom.prg_rom_banks.last(); txrom.prg_rom_banks.set(2, last_bank - 1); txrom.prg_rom_banks.set(3, last_bank); Ok(txrom) } /// Load `Txrom` from `Cart`. pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, ) -> Result { Ok(Self::new( cart, chr_rom, prg_rom, if cart.mapper_num() == 76 { Self::CHR_WINDOW_76 } else { Self::CHR_WINDOW }, )? .into()) } pub const fn bank_register(&self, index: usize) -> u8 { self.regs.bank_values[index] } pub const fn set_revision(&mut self, rev: Revision) { self.revision = rev; } #[inline] const fn apply_prg_write_masks(&self, addr: &mut u16, val: &mut u8) { // Redirects all 0x8000..=0xFFFF writes to 0x8000..=0x8001 as all other features do not // exist for the corresponding mappers that call this *addr &= 0x8001; if *addr == 0x8000 { // Disable CHR mode 1 and Prg mode 1 // PRG has the last two 8K banks fixed to the end. // CHR assigns the left pattern table ($0000-$0FFF) two 2K banks, and the right pattern // table ($1000-$1FFF) four 1K banks. *val &= 0x3F; } } pub fn update_prg_banks(&mut self) { let prg_last = self.prg_rom_banks.last(); let prg_lo = self.regs.bank_values[6] as usize; let prg_hi = self.regs.bank_values[7] as usize; if self.regs.bank_select & Self::PRG_MODE_MASK == Self::PRG_MODE_MASK { self.prg_rom_banks.set(0, prg_last - 1); self.prg_rom_banks.set(1, prg_hi); self.prg_rom_banks.set(2, prg_lo); } else { self.prg_rom_banks.set(0, prg_lo); self.prg_rom_banks.set(1, prg_hi); self.prg_rom_banks.set(2, prg_last - 1); } self.prg_rom_banks.set(3, prg_last); } pub fn set_chr_banks(&mut self, f: impl Fn(&mut Banks, &mut [u8])) { f(&mut self.chr_banks, &mut self.regs.bank_values) } pub fn update_chr_banks(&mut self) { match self.mapper_num { 76 => { self.set_chr_banks(|banks, regs| { banks.set(0, regs[2] as usize); banks.set(1, regs[3] as usize); banks.set(2, regs[4] as usize); banks.set(3, regs[5] as usize); }); return; } 88 | 154 => { self.set_chr_banks(|_, regs| { regs[0] &= 0x3F; regs[1] &= 0x3F; regs[2] |= 0x40; regs[3] |= 0x40; regs[4] |= 0x40; regs[5] |= 0x40; }); } _ => (), } // 1: two 2K banks at $1000-$1FFF, four 1 KB banks at $0000-$0FFF // 0: two 2K banks at $0000-$0FFF, four 1 KB banks at $1000-$1FFF let chr = self.regs.bank_values; if self.regs.bank_select & Self::CHR_INVERSION_MASK == Self::CHR_INVERSION_MASK { self.chr_banks.set(0, chr[2] as usize); self.chr_banks.set(1, chr[3] as usize); self.chr_banks.set(2, chr[4] as usize); self.chr_banks.set(3, chr[5] as usize); self.chr_banks.set_range(4, 5, (chr[0] & 0xFE) as usize); self.chr_banks.set_range(6, 7, (chr[1] & 0xFE) as usize); } else { self.chr_banks.set_range(0, 1, (chr[0] & 0xFE) as usize); self.chr_banks.set_range(2, 3, (chr[1] & 0xFE) as usize); self.chr_banks.set(4, chr[2] as usize); self.chr_banks.set(5, chr[3] as usize); self.chr_banks.set(6, chr[4] as usize); self.chr_banks.set(7, chr[5] as usize); } } pub fn update_banks(&mut self) { self.update_prg_banks(); self.update_chr_banks(); } const fn is_a12_rising_edge(&mut self, addr: u16) -> bool { if addr & 0x1000 > 0 { // NOTE: This is technical 3 falling edges of M2 - but because the mapper doesn't have // direct access to the CPUs clock, and is clocked after the PPU runs and calls this // method, we're off by 1 let is_rising_edge = self.regs.a12_low_clock > 0 && self.regs.master_clock.wrapping_sub(self.regs.a12_low_clock) >= 4; self.regs.a12_low_clock = 0; return is_rising_edge; } else if self.regs.a12_low_clock == 0 { self.regs.a12_low_clock = self.regs.master_clock; } false } } impl Map for Txrom { // PPU $0000..=$07FF (or $1000..=$17FF) 2K CHR-ROM/RAM Bank 1 Switchable --+ // PPU $0800..=$0FFF (or $1800..=$1FFF) 2K CHR-ROM/RAM Bank 2 Switchable --|-+ // PPU $1000..=$13FF (or $0000..=$03FF) 1K CHR-ROM/RAM Bank 3 Switchable --+ | // PPU $1400..=$17FF (or $0400..=$07FF) 1K CHR-ROM/RAM Bank 4 Switchable --+ | // PPU $1800..=$1BFF (or $0800..=$0BFF) 1K CHR-ROM/RAM Bank 5 Switchable ----+ // PPU $1C00..=$1FFF (or $0C00..=$0FFF) 1K CHR-ROM/RAM Bank 6 Switchable ----+ // PPU $2000..=$3EFF FourScreen Mirroring (optional) // CPU $6000..=$7FFF 8K PRG-RAM Bank (optional) // CPU $8000..=$9FFF (or $C000..=$DFFF) 8K PRG-ROM Bank 1 Switchable // CPU $A000..=$BFFF 8K PRG-ROM Bank 2 Switchable // CPU $C000..=$DFFF (or $8000..=$9FFF) 8K PRG-ROM Bank 3 Fixed to second-to-last Bank // CPU $E000..=$FFFF 8K PRG-ROM Bank 4 Fixed to Last /// Read a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_read(&mut self, addr: u16, ciram: &CIRam) -> u8 { self.ppu_read(addr); self.chr_peek(addr, ciram) } /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => self.chr[self.chr_banks.translate(addr)], 0x2000..=0x3EFF => { if self.mirroring == Mirroring::FourScreen { self.ex_ram[usize::from(addr & 0x1FFF)] } else { ciram.peek(addr, self.mirroring) } } _ => 0, } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x6000..=0x7FFF => self.prg_ram[self.prg_ram_banks.translate(addr)], 0x8000..=0xFFFF => self.prg_rom[self.prg_rom_banks.translate(addr)], _ => 0, } } /// Write a byte to CHR-RAM/CIRAM at a given address. #[inline(always)] fn chr_write(&mut self, addr: u16, val: u8, ciram: &mut CIRam) { match addr { 0x0000..=0x1FFF => self.chr[self.chr_banks.translate(addr)] = val, 0x2000..=0x3EFF => { if self.mirroring == Mirroring::FourScreen { self.ex_ram[usize::from(addr & 0x1FFF)] = val; } else { ciram.write(addr, val, self.mirroring); } } _ => (), } } /// Write a byte to PRG-RAM at a given address. fn prg_write(&mut self, mut addr: u16, mut val: u8) { match self.mapper_num { 76 | 88 | 95 | 206 => self.apply_prg_write_masks(&mut addr, &mut val), 154 => { self.mirroring = if val & 0x40 == 0x40 { Mirroring::SingleScreenB } else { Mirroring::SingleScreenA }; self.apply_prg_write_masks(&mut addr, &mut val); } _ => (), } match addr { 0x6000..=0x7FFF => self.prg_ram[self.prg_ram_banks.translate(addr)] = val, 0x8000..=0xFFFF => { // 7654 3210 // `CPMx xRRR` // ||| +++- Specify which bank register to update on next write to Bank Data register // ||| 0: Select 2K CHR bank at PPU $0000-$07FF (or $1000-$17FF); // ||| 1: Select 2K CHR bank at PPU $0800-$0FFF (or $1800-$1FFF); // ||| 2: Select 1K CHR bank at PPU $1000-$13FF (or $0000-$03FF); // ||| 3: Select 1K CHR bank at PPU $1400-$17FF (or $0400-$07FF); // ||| 4: Select 1K CHR bank at PPU $1800-$1BFF (or $0800-$0BFF); // ||| 5: Select 1K CHR bank at PPU $1C00-$1FFF (or $0C00-$0FFF); // ||| 6: Select 8K PRG-ROM bank at $8000-$9FFF (or $C000-$DFFF); // ||| 7: Select 8K PRG-ROM bank at $A000-$BFFF // ||+------- Nothing on the MMC3, see MMC6 // |+-------- PRG-ROM bank mode (0: $8000-$9FFF swappable, // | $C000-$DFFF fixed to second-last bank; // | 1: $C000-$DFFF swappable, // | $8000-$9FFF fixed to second-last bank) // +--------- CHR A12 inversion (0: two 2K banks at $0000-$0FFF, // four 1K banks at $1000-$1FFF; // 1: two 2K banks at $1000-$1FFF, // four 1K banks at $0000-$0FFF) // // Match only $8000/1, $A000/1, $C000/1, and $E000/1 match addr & 0xE001 { 0x8000 => { self.regs.bank_select = val; self.update_banks(); } 0x8001 => { let bank = self.regs.bank_select & 0x07; self.regs.bank_values[bank as usize] = val; self.update_banks(); } 0xA000 => { if self.mirroring != Mirroring::FourScreen { self.mirroring = if val & 0x01 == 0x01 { Mirroring::Horizontal } else { Mirroring::Vertical }; self.update_banks(); } } 0xA001 => { // TODO RAM protect? Might conflict with MMC6 } // IRQ 0xC000 => self.regs.irq_latch = val, 0xC001 => self.regs.irq_reload = true, 0xE000 => { self.regs.irq_enabled = false; self.regs.irq_pending = false; } 0xE001 => self.regs.irq_enabled = true, _ => unreachable!("impossible address"), } } _ => (), } if self.mapper_num == 95 && addr & 0x01 == 0x01 { let nametable1 = (self.bank_register(0) >> 5) & 0x01; let nametable2 = (self.bank_register(1) >> 5) & 0x01; self.mirroring = match (nametable1, nametable2) { (0, 0) => Mirroring::SingleScreenA, (1, 1) => Mirroring::SingleScreenB, _ => Mirroring::Horizontal, }; } } /// Synchronize a read from a PPU address. fn ppu_read(&mut self, addr: u16) { // Clock on PPU A12 rising edge if self.is_a12_rising_edge(addr) { let counter = self.regs.irq_counter; if self.regs.irq_counter == 0 || self.regs.irq_reload { self.regs.irq_counter = self.regs.irq_latch; } else { self.regs.irq_counter -= 1; } if self.revision == Revision::A { if (counter > 0 || self.regs.irq_reload) && self.regs.irq_counter == 0 && self.regs.irq_enabled { self.regs.irq_pending = true; } } else if self.regs.irq_counter == 0 && self.regs.irq_enabled { self.regs.irq_pending = true; } self.regs.irq_reload = false; } } /// Whether an IRQ is pending acknowledgement. fn irq_pending(&self) -> bool { self.regs.irq_pending } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for Txrom { fn reset(&mut self, _kind: ResetKind) { self.regs = Regs::default(); self.update_banks(); self.update_chr_banks(); } } impl Clock for Txrom { fn clock(&mut self) { self.regs.master_clock = self.regs.master_clock.wrapping_add(1); } } impl Sram for Txrom { /// Save RAM to a given path. fn save(&self, path: impl AsRef) -> fs::Result<()> { fs::save(path.as_ref(), &self.prg_ram) } /// Load save RAM from a given path. fn load(&mut self, path: impl AsRef) -> fs::Result<()> { fs::load(path.as_ref()).map(|data: Memory>| self.prg_ram = data) } } impl Regional for Txrom {} ================================================ FILE: tetanes-core/src/mapper/m005_exrom.rs ================================================ //! `ExROM`/`MMC5` (Mapper 5). //! //! //! use crate::{ apu::{ PULSE_TABLE, TND_TABLE, dmc::Dmc, pulse::{OutputFreq, Pulse, PulseChannel}, }, cart::Cart, common::{Clock, NesRegion, Regional, Reset, ResetKind, Sample, Sram}, cpu::Cpu, fs, mapper::{self, Map, Mapper}, mem::{Banks, Memory}, ppu::{self, CIRam, Mirroring, PpuAddr}, }; use bitflags::bitflags; use serde::{Deserialize, Serialize}; use std::path::Path; use tracing::warn; /// PRG banking mode. #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[must_use] pub enum PrgMode { Bank32k, Bank16k, Bank16_8k, Bank8k, } /// CHR banking mode. #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[must_use] pub enum ChrMode { Bank8k, Bank4k, Bank2k, Bank1k, } /// CHR bank registers. #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[must_use] pub enum ChrBank { Spr, Bg, } bitflags! { #[derive(Default, Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)] #[must_use] pub struct ExRamRW: u8 { const W = 0x01; const R = 0x02; const RW = Self::R.bits() | Self::W.bits(); } } /// Exram mode registers. #[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct ExRamMode { pub bits: u8, pub nametable: bool, pub attr: bool, pub rw: ExRamRW, } impl Default for ExRamMode { fn default() -> Self { Self::new() } } impl ExRamMode { pub const fn new() -> Self { Self { bits: 0x00, nametable: false, attr: false, rw: ExRamRW::W, } } pub const fn set(&mut self, val: u8) { let val = val & 0b11; self.bits = val; self.nametable = val <= 0b01; self.attr = val == 0b01; self.rw = match val { 0b00 | 0b01 => ExRamRW::W, 0b10 => ExRamRW::RW, _ => ExRamRW::R, }; } } /// Exram nametable select. #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[must_use] pub enum Nametable { ScreenA, ScreenB, ExRam, Fill, } /// Exram nametable mapping registers. #[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct NametableMapping { pub mode: u8, pub select: [Nametable; 4], } impl Default for NametableMapping { fn default() -> Self { Self::new() } } impl NametableMapping { pub const fn new() -> Self { Self { mode: 0x00, select: [Nametable::ScreenA; 4], } } pub fn set(&mut self, val: u8) { let nametable = |val: u8| match val & 0b11 { 0b00 => Nametable::ScreenA, 0b01 => Nametable::ScreenB, 0b10 => Nametable::ExRam, _ => Nametable::Fill, }; self.mode = val; self.select = [ nametable(val), nametable(val >> 2), nametable(val >> 4), nametable(val >> 6), ]; } } /// Exram fill registers. #[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct Fill { pub tile: u8, // $5106 pub attr: usize, // $5107 } impl Default for Fill { fn default() -> Self { Self::new() } } impl Fill { pub const fn new() -> Self { Self { attr: 0x03, tile: 0xFF, } } } /// Vertical split side. #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[must_use] pub enum Side { Left, Right, } /// Vertical split mode. #[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct VSplit { pub mode: u8, // $5200 [ES.T TTTT] pub enabled: bool, // $5200 [E... ....] pub side: Side, // $5200 [.S.. ....] pub tile: u8, // $5200 [...T TTTT] pub scroll: u8, // $5201 pub bank: u8, // $5202 pub in_region: bool, } impl Default for VSplit { fn default() -> Self { Self::new() } } impl VSplit { pub const fn new() -> Self { Self { mode: 0x00, enabled: false, side: Side::Left, tile: 0x00, scroll: 0x00, bank: 0x00, in_region: false, } } } /// `ExROM` registers. #[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct Regs { pub prg_mode: PrgMode, // $5100 pub chr_mode: ChrMode, // $5101 pub prg_ram_protect: [u8; 2], // $5102 - $5103 pub exram_mode: ExRamMode, // $5104 pub nametable_mapping: NametableMapping, // $5105 pub fill: Fill, // $5106 - $5107 pub prg_banks: [usize; 5], // $5113 - $5117 pub chr_banks: [usize; 16], // $5120 - $512B pub chr_hi: usize, // $5130 pub vsplit: VSplit, // $5200 - $5202 pub irq_scanline: u16, // $5203: Write $00 to disable IRQs pub irq_enabled: bool, // $5204 pub irq_pending: bool, pub multiplicand: u8, // $5205: write pub multiplier: u8, // $5206: write pub mult_result: u16, // $5205: read lo, $5206: read hi } impl Default for Regs { fn default() -> Self { Self::new() } } impl Regs { pub const fn new() -> Self { Self { prg_mode: PrgMode::Bank8k, chr_mode: ChrMode::Bank1k, prg_ram_protect: [0x00; 2], exram_mode: ExRamMode::new(), nametable_mapping: NametableMapping::new(), fill: Fill::new(), prg_banks: [0x00; 5], chr_banks: [0x00; 16], chr_hi: 0x00, vsplit: VSplit::new(), irq_scanline: 0x00, irq_enabled: false, irq_pending: false, multiplicand: 0xFF, multiplier: 0xFF, mult_result: 0xFE01, // e.g. 0xFF * 0xFF } } } /// `ExROM` IRQ state. #[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct IrqState { pub in_frame: bool, pub prev_addr: Option, pub match_count: u8, pub pending: bool, } /// Internally tracked PPU status. #[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct PpuStatus { pub fetch_count: u32, pub reading: bool, pub idle_count: u8, pub sprite8x16: bool, // $2000 PPUCTRL: false = 8x8, true = 8x16 pub rendering: bool, pub scanline: u16, } /// `ExROM`/`MMC5` (Mapper 5). #[derive(Clone, Serialize, Deserialize)] #[must_use] pub struct Exrom { pub chr_rom: Memory>, pub prg_rom: Memory>, pub prg_ram: Memory>, pub ex_ram: Memory>, pub regs: Regs, pub mirroring: Mirroring, pub ppu_status: PpuStatus, pub irq_state: IrqState, pub chr_banks: Banks, pub prg_ram_banks: Banks, pub prg_rom_banks: Banks, pub tile_cache: u16, pub last_chr_write: ChrBank, pub region: NesRegion, pub pulse1: Pulse, pub pulse2: Pulse, pub dmc: Dmc, pub dmc_mode: u8, pub cpu_cycle: usize, pub pulse_timer: f32, } impl Exrom { const PRG_WINDOW: usize = 0x2000; const PRG_RAM_SIZE: usize = 0x10000; // Provide 64K since mappers don't always specify const EXRAM_SIZE: usize = 0x0400; const CHR_WINDOW: usize = 0x0400; const ROM_SELECT_MASK: usize = 0x80; // High bit targets ROM bank switching const BANK_MASK: usize = 0x7F; // Ignore high bit for ROM select const SPR_FETCH_START: u32 = 64; const SPR_FETCH_END: u32 = 81; // This conveniently mirrors a 2-bit palette attribute to all four indexes // https://www.nesdev.org/wiki/MMC5#Fill-mode_color_($5107) const ATTR_MIRROR: [u8; 4] = [0x00, 0x55, 0xAA, 0xFF]; // // TODO: See about generating these using oncecell // const ATTR_LOC: [u8; 256] = [ // 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, // 0x07, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, // 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x08, 0x09, 0x0A, 0x0B, 0x0C, // 0x0D, 0x0E, 0x0F, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x08, 0x09, 0x0A, 0x0B, // 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x10, 0x11, 0x12, // 0x13, 0x14, 0x15, 0x16, 0x17, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x10, 0x11, // 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x18, // 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, // 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, // 0x27, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, // 0x26, 0x27, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, // 0x2D, 0x2E, 0x2F, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x28, 0x29, 0x2A, 0x2B, // 0x2C, 0x2D, 0x2E, 0x2F, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, // 0x33, 0x34, 0x35, 0x36, 0x37, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x30, 0x31, // 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, // 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, // 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, // 0x3F, // ]; // const ATTR_SHIFT: [u8; 128] = [ // 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, // 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, // 0, 0, 2, 2, 4, 4, 6, 6, 4, 4, 6, 6, 4, 4, 6, 6, 4, 4, 6, 6, 4, 4, 6, 6, 4, 4, 6, 6, 4, 4, // 6, 6, 4, 4, 6, 6, 4, 4, 6, 6, 4, 4, 6, 6, 4, 4, 6, 6, 4, 4, 6, 6, 4, 4, 6, 6, 4, 4, 6, 6, // 4, 4, 6, 6, 4, 4, 6, 6, // ]; /// Load `Exrom` from `Cart`. pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, ) -> Result { let prg_ram = Memory::with_ram_state(Self::PRG_RAM_SIZE, cart.ram_state); let chr_banks = Banks::new(0x0000, 0x1FFF, chr_rom.len(), Self::CHR_WINDOW)?; let prg_ram_banks = Banks::new(0x6000, 0xFFFF, prg_ram.len(), Self::PRG_WINDOW)?; let prg_rom_banks = Banks::new(0x8000, 0xFFFF, prg_rom.len(), Self::PRG_WINDOW)?; let mut exrom = Self { chr_rom, prg_rom, prg_ram, ex_ram: Memory::new(Self::EXRAM_SIZE), regs: Regs::new(), mirroring: cart.mirroring(), irq_state: IrqState { in_frame: false, prev_addr: None, match_count: 0, pending: false, }, ppu_status: PpuStatus { fetch_count: 0x00, reading: false, idle_count: 0x00, sprite8x16: false, rendering: false, scanline: 0x0000, }, chr_banks, prg_ram_banks, prg_rom_banks, tile_cache: 0, last_chr_write: ChrBank::Spr, region: cart.region(), pulse1: Pulse::new(PulseChannel::One, OutputFreq::Ultrasonic), pulse2: Pulse::new(PulseChannel::Two, OutputFreq::Ultrasonic), dmc: Dmc::new(cart.region()), dmc_mode: 0x01, // Default to read mode cpu_cycle: 0, pulse_timer: 0.0, }; exrom.regs.prg_banks[4] = exrom.prg_rom_banks.last() | Self::ROM_SELECT_MASK; exrom.update_prg_banks(); Ok(exrom.into()) } // $6000 $8000 $A000 $C000 $E000 // +-------+-------------------------------+ // P=%00: | $5113 | <<$5117>> | // +-------+-------------------------------+ // P=%01: | $5113 | <$5115> | <$5117> | // +-------+---------------+-------+-------+ // P=%10: | $5113 | <$5115> | $5116 | $5117 | // +-------+---------------+-------+-------+ // P=%11: | $5113 | $5114 | $5115 | $5116 | $5117 | // +-------+-------+-------+-------+-------+ pub fn update_prg_banks(&mut self) { let mode = self.regs.prg_mode; let banks = self.regs.prg_banks; self.prg_ram_banks.set(0, banks[0]); // $5113 always selects RAM match mode { // $5117 always selects ROM PrgMode::Bank32k => self.prg_rom_banks.set_range(0, 3, banks[4]), PrgMode::Bank16k => { self.set_prg_bank_range(0, 1, banks[2]); self.prg_rom_banks .set_range(2, 3, banks[4] & Self::BANK_MASK); } PrgMode::Bank16_8k => { self.set_prg_bank_range(0, 1, banks[2]); self.set_prg_bank_range(2, 2, banks[3]); self.prg_rom_banks.set(3, banks[4] & Self::BANK_MASK); } PrgMode::Bank8k => { self.set_prg_bank_range(0, 0, banks[1]); self.set_prg_bank_range(1, 1, banks[2]); self.set_prg_bank_range(2, 2, banks[3]); self.prg_rom_banks.set(3, banks[4] & Self::BANK_MASK); } }; } pub fn set_prg_bank_range(&mut self, start: usize, end: usize, bank: usize) { let rom = bank & Self::ROM_SELECT_MASK == Self::ROM_SELECT_MASK; let bank = bank & Self::BANK_MASK; if rom { self.prg_rom_banks.set_range(start, end, bank); } else { self.prg_ram_banks.set_range(start + 1, end + 1, bank); } } pub fn rom_select(&self, addr: u16) -> bool { let mode = self.regs.prg_mode; match addr { 0x6000..=0x7FFF => false, 0xE000..=0xFFFF => true, _ => { if mode == PrgMode::Bank32k { true } else { use PrgMode::{Bank8k, Bank16_8k, Bank16k}; let banks = self.regs.prg_banks; let bank = match (addr, mode) { (0x8000..=0x9FFF, Bank8k) => banks[1], (0x8000..=0xBFFF, Bank16k | Bank16_8k) | (0xA000..=0xBFFF, Bank8k) => { banks[2] } (0xC000..=0xDFFF, Bank8k | Bank16_8k) => banks[3], (0xC000..=0xDFFF, Bank16k) => banks[4], _ => 0x00, }; bank & Self::ROM_SELECT_MASK == Self::ROM_SELECT_MASK } } } } // 'A' Set (Sprites): // $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 // +---------------------------------------------------------------+ // C=%00: | $5127 | // +---------------------------------------------------------------+ // C=%01: | $5123 | $5127 | // +-------------------------------+-------------------------------+ // C=%10: | $5121 | $5123 | $5125 | $5127 | // +---------------+---------------+---------------+---------------+ // C=%11: | $5120 | $5121 | $5122 | $5123 | $5124 | $5125 | $5126 | $5127 | // +-------+-------+-------+-------+-------+-------+-------+-------+ // // 'B' Set (BG): // $0000 $0400 $0800 $0C00 $1000 $1400 $1800 $1C00 // +-------------------------------+-------------------------------+ // C=%00: | $512B | // +-------------------------------+-------------------------------+ // C=%01: | $512B | $512B | // +-------------------------------+-------------------------------+ // C=%10: | $5129 | $512B | $5129 | $512B | // +---------------+---------------+---------------+---------------+ // C=%11: | $5128 | $5129 | $512A | $512B | $5128 | $5129 | $512A | $512B | // +-------+-------+-------+-------+-------+-------+-------+-------+ pub fn update_chr_banks(&mut self, chr_bank: ChrBank) { let hi = self.regs.chr_hi; let banks = match chr_bank { ChrBank::Spr => &self.regs.chr_banks[0..8], ChrBank::Bg => &self.regs.chr_banks[8..16], }; // CHR banks are in actual page sizes which means they need to be shifted appropriately match self.regs.chr_mode { ChrMode::Bank8k => self.chr_banks.set_range(0, 7, hi | (banks[7] << 3)), ChrMode::Bank4k => { self.chr_banks.set_range(0, 3, hi | (banks[3] << 2)); self.chr_banks.set_range(4, 7, hi | (banks[7] << 2)); } ChrMode::Bank2k => { self.chr_banks.set_range(0, 1, hi | (banks[1] << 1)); self.chr_banks.set_range(2, 3, hi | (banks[3] << 1)); self.chr_banks.set_range(4, 5, hi | (banks[5] << 1)); self.chr_banks.set_range(6, 7, hi | (banks[7] << 1)); } ChrMode::Bank1k => { self.chr_banks.set(0, hi | banks[0]); self.chr_banks.set(1, hi | banks[1]); self.chr_banks.set(2, hi | banks[2]); self.chr_banks.set(3, hi | banks[3]); self.chr_banks.set(4, hi | banks[4]); self.chr_banks.set(5, hi | banks[5]); self.chr_banks.set(6, hi | banks[6]); self.chr_banks.set(7, hi | banks[7]); } }; } pub fn read_ex_ram(&self, addr: u16) -> u8 { self.ex_ram[(addr & 0x03FF) as usize] } pub fn write_ex_ram(&mut self, addr: u16, val: u8) { self.ex_ram[(addr & 0x03FF) as usize] = val; } pub const fn inc_fetch_count(&mut self) { self.ppu_status.fetch_count += 1; } pub const fn fetch_count(&self) -> u32 { self.ppu_status.fetch_count } pub const fn sprite8x16(&self) -> bool { self.ppu_status.sprite8x16 } pub fn spr_fetch(&self) -> bool { (Self::SPR_FETCH_START..Self::SPR_FETCH_END).contains(&self.fetch_count()) } pub const fn nametable_select(&self, addr: u16) -> Nametable { self.regs.nametable_mapping.select[((addr >> 10) & 0x03) as usize] } } impl Map for Exrom { // CHR mode 0 // PPU $0000..=$1FFF 8K switchable CHR bank // // CHR mode 1 // PPU $0000..=$0FFF 4K switchable CHR bank // PPU $1000..=$1FFF 4K switchable CHR bank // // CHR mode 2 // PPU $0000..=$07FF 2K switchable CHR bank // PPU $0800..=$0FFF 2K switchable CHR bank // PPU $1000..=$17FF 2K switchable CHR bank // PPU $1800..=$1FFF 2K switchable CHR bank // // CHR mode 3 // PPU $0000..=$03FF 1K switchable CHR bank // PPU $0400..=$07FF 1K switchable CHR bank // PPU $0800..=$0BFF 1K switchable CHR bank // PPU $0C00..=$0FFF 1K switchable CHR bank // PPU $1000..=$13FF 1K switchable CHR bank // PPU $1400..=$17FF 1K switchable CHR bank // PPU $1800..=$1BFF 1K switchable CHR bank // PPU $1C00..=$1FFF 1K switchable CHR bank // // PPU $2000..=$3EFF Up to 3 Nametables + Fill mode // // PRG mode 0 // CPU $6000..=$7FFF 8K switchable PRG RAM bank // CPU $8000..=$FFFF 32K switchable PRG ROM bank // // PRG mode 1 // CPU $6000..=$7FFF 8K switchable PRG RAM bank // CPU $8000..=$BFFF 16K switchable PRG ROM/RAM bank // CPU $C000..=$FFFF 16K switchable PRG ROM bank // // PRG mode 2 // CPU $6000..=$7FFF 8K switchable PRG RAM bank // CPU $8000..=$BFFF 16K switchable PRG ROM/RAM bank // CPU $C000..=$DFFF 8K switchable PRG ROM/RAM bank // CPU $E000..=$FFFF 8K switchable PRG ROM bank // // PRG mode 3 // CPU $6000..=$7FFF 8K switchable PRG RAM bank // CPU $8000..=$9FFF 8K switchable PRG ROM/RAM bank // CPU $A000..=$BFFF 8K switchable PRG ROM/RAM bank // CPU $C000..=$DFFF 8K switchable PRG ROM/RAM bank // CPU $E000..=$FFFF 8K switchable PRG ROM bank /// Read a byte from CHR-ROM/RAM at a given address. fn chr_read(&mut self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => { self.inc_fetch_count(); if self.sprite8x16() { match self.fetch_count() { Self::SPR_FETCH_START => self.update_chr_banks(ChrBank::Spr), Self::SPR_FETCH_END => self.update_chr_banks(ChrBank::Bg), _ => (), } } } 0x2000..=0x3EFF => { let is_attr = addr.is_attr(); // Cache BG tile fetch for later attribute byte fetch if self.regs.exram_mode.attr && !is_attr && !self.spr_fetch() { self.tile_cache = addr & 0x03FF; } // TODO: Detect split // if self.regs.vsplit.in_region && !is_attr { // self.regs.vsplit.tile = ((self.regs.vsplit.scroll & 0xF8) << 2) // | ((self.fetch_count() / 4) & 0x1F) as u8; // } // Monitor tile fetches to trigger IRQs // https://wiki.nesdev.org/w/index.php?title=MMC5#Scanline_Detection_and_Scanline_IRQ let status = &mut self.ppu_status; let irq_state = &mut self.irq_state; // Wait for three consecutive fetches to match the same address, which means we're // at the end of the render scanlines fetching dummy NT bytes if addr <= 0x2FFF && Some(addr) == irq_state.prev_addr { irq_state.match_count += 1; status.fetch_count = 0; if irq_state.match_count == 2 { if irq_state.in_frame { // Scanline IRQ detected status.scanline += 1; if status.scanline == self.regs.irq_scanline { irq_state.pending = true; if self.regs.irq_enabled { self.regs.irq_pending = true; } } } else { irq_state.in_frame = true; status.scanline = 0; } } } else { irq_state.match_count = 0; } irq_state.prev_addr = Some(addr); status.reading = true; } _ => (), } self.chr_peek(addr, ciram) } /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => { if self.regs.exram_mode.attr && !self.spr_fetch() { // Bits 6-7 of 4K CHR bank. Already shifted left by 8 let bank_hi = self.regs.chr_hi << 10; // Bits 0-5 of 4k CHR bank let bank_lo = ((self.read_ex_ram(self.tile_cache) & 0x3F) as usize) << 12; let addr = bank_hi | bank_lo | (addr as usize) & 0x0FFF; self.chr_rom[addr] } else { self.chr_rom[self.chr_banks.translate(addr)] } } 0x2000..=0x3EFF => { let is_attr = addr.is_attr(); // TODO: vsplit // if self.regs.vsplit.in_region { // if is_attr { // // let addr = // // Self::ATTR_OFFSET | u16::from(ATTR_LOC[(self.regs.vsplit.tile as usize) >> 2]); // // let attr = self.read_exram(addr - 0x2000) as usize; // // let shift = ATTR_SHIFT[(self.regs.vsplit.tile as usize) & 0x7F] as usize; // // MappedRead::Data(ATTR_BITS[(attr >> shift) & 0x03]) // } else { // MappedRead::Data(self.read_exram(self.regs.vsplit.tile.into())) // } // } if self.regs.exram_mode.attr && is_attr && !self.spr_fetch() { // ExAttr mode returns attr bits for all nametables, regardless of mapping let attr = (self.read_ex_ram(self.tile_cache) >> 6) & 0x03; Self::ATTR_MIRROR[attr as usize] } else { let nametable_mode = self.regs.exram_mode.nametable; match self.nametable_select(addr) { Nametable::ScreenA => ciram[(addr & 0x03FF).into()], Nametable::ScreenB => { ciram[(ppu::size::NAMETABLE | (addr & 0x03FF)).into()] } Nametable::ExRam if nametable_mode => self.read_ex_ram(addr), Nametable::Fill if nametable_mode => { if is_attr { Self::ATTR_MIRROR[self.regs.fill.attr & 0x03] } else { self.regs.fill.tile } } // If nametable mode is not set, zero is read back _ => 0, } } } _ => 0, } } /// Read a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_read(&mut self, addr: u16) -> u8 { match addr { 0xFFFA | 0xFFFB => { self.irq_state.in_frame = false; // NMI clears in_frame self.irq_state.prev_addr = None; self.irq_state.pending = false; self.regs.irq_pending = false; } _ => (), } let val = self.prg_peek(addr); match addr { 0x5204 => { self.irq_state.pending = false; self.regs.irq_pending = false; } 0x5010 => self.dmc.irq_pending = false, _ => (), } val } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x5010 => { // [I... ...M] DMC // I = IRQ (0 = No IRQ triggered. 1 = IRQ was triggered.) Reading $5010 acknowledges the IRQ and clears this flag. // M = Mode select (0 = write mode. 1 = read mode.) (u8::from(self.dmc.irq_pending) << 7) | self.dmc_mode } 0x5100 => self.regs.prg_mode as u8, 0x5101 => self.regs.chr_mode as u8, 0x5104 => self.regs.exram_mode.bits, 0x5105 => self.regs.nametable_mapping.mode, 0x5106 => self.regs.fill.tile, 0x5107 => self.regs.fill.attr as u8, 0x5015 => { // [.... ..BA] Length status for Pulse 1 (A), 2 (B) let mut status = 0x00; if self.pulse1.length.counter > 0 { status |= 0x01; } if self.pulse2.length.counter > 0 { status |= 0x02; } status } 0x5113..=0x5117 => self.regs.prg_banks[(addr - 0x5113) as usize] as u8, 0x5120..=0x512B => self.regs.chr_banks[(addr - 0x5120) as usize] as u8, 0x5130 => self.regs.chr_hi as u8, 0x5200 => self.regs.vsplit.mode, 0x5201 => self.regs.vsplit.scroll, 0x5202 => self.regs.vsplit.bank, 0x5203 => self.regs.irq_scanline as u8, 0x5204 => { // $5204: [PI.. ....] // P = IRQ currently pending // I = "In Frame" signal // Reading $5204 will clear the pending flag (acknowledging the IRQ). // Clearing is done in the read() function (u8::from(self.regs.irq_pending) << 7) | (u8::from(self.irq_state.in_frame) << 6) } 0x5205 => (self.regs.mult_result & 0xFF) as u8, 0x5206 => ((self.regs.mult_result >> 8) & 0xFF) as u8, 0x5C00..=0x5FFF if self.regs.exram_mode.rw != ExRamRW::W => { // Nametable/Attr modes are not used for RAM, thus are not readable self.read_ex_ram(addr) } 0x6000..=0xDFFF => { if self.rom_select(addr) { self.prg_rom[self.prg_rom_banks.translate(addr)] } else { self.prg_ram[self.prg_ram_banks.translate(addr)] } } 0xE000..=0xFFFF => self.prg_rom[self.prg_rom_banks.translate(addr)], _ => 0, } } /// Write a byte to CHR-RAM/CIRAM at a given address. #[inline(always)] fn chr_write(&mut self, addr: u16, val: u8, ciram: &mut CIRam) { match addr { 0x0000..=0x1FFF => self.chr_rom[self.chr_banks.translate(addr)] = val, 0x2000..=0x3EFF => match self.nametable_select(addr) { Nametable::ScreenA => ciram[(addr & 0x03FF).into()] = val, Nametable::ScreenB => ciram[(ppu::size::NAMETABLE | (addr & 0x03FF)).into()] = val, Nametable::ExRam if self.regs.exram_mode.nametable => { self.write_ex_ram(addr, val); } _ => (), }, _ => (), } } /// Write a byte to PRG-RAM at a given address. fn prg_write(&mut self, addr: u16, val: u8) { match addr { 0x5000 => self.pulse1.write_ctrl(val), // 0x5001 Has no effect since there is no Sweep unit 0x5002 => self.pulse1.write_timer_lo(val), 0x5003 => self.pulse1.write_timer_hi(val), 0x5004 => self.pulse2.write_ctrl(val), // 0x5005 Has no effect since there is no Sweep unit 0x5006 => self.pulse2.write_timer_lo(val), 0x5007 => self.pulse2.write_timer_hi(val), 0x5010 => { // [I... ...M] DMC // I = PCM IRQ enable (1 = enabled.) // M = Mode select (0 = write mode. 1 = read mode.) self.dmc_mode = val & 0x01; self.dmc.irq_enabled = val & 0x80 == 0x80; } 0x5011 if self.dmc_mode == 0 && val != 0x00 => { // [DDDD DDDD] PCM Data // Write mode - writing $00 has no effect self.dmc.write_output(val); } 0x5015 => { // [.... ..BA] Enable flags for Pulse 1 (A), 2 (B) (0=disable, 1=enable) self.pulse1.set_enabled(val & 0x01 == 0x01); self.pulse2.set_enabled(val & 0x02 == 0x02); } 0x5100 => { // [.... ..PP] PRG Mode self.regs.prg_mode = match val & 0x03 { 0 => PrgMode::Bank32k, 1 => PrgMode::Bank16k, 2 => PrgMode::Bank16_8k, 3 => PrgMode::Bank8k, _ => { warn!("invalid PrgMode value: ${:02X}", val); self.regs.prg_mode } }; self.update_prg_banks(); } 0x5101 => { // [.... ..CC] CHR Mode if self.regs.exram_mode.attr { // Bank switching is ignored in extended attribute mode, banks are always 4K self.regs.chr_mode = ChrMode::Bank4k; } else { self.regs.chr_mode = match val & 0x03 { 0 => ChrMode::Bank8k, 1 => ChrMode::Bank4k, 2 => ChrMode::Bank2k, 3 => ChrMode::Bank1k, _ => { warn!("invalid ChrMode value: ${:02X}", val); self.regs.chr_mode } }; } self.update_chr_banks(self.last_chr_write); } 0x5102 | 0x5103 => { // To allow writing to PRG-RAM you must set: // A=%10 // B=%01 // Any other value will prevent PRG-RAM writing. // [.... ..AA] PRG-RAM Protect A // [.... ..BB] PRG-RAM Protect B self.regs.prg_ram_protect[(addr - 0x5102) as usize] = val & 0x03; } 0x5104 => { // [.... ..XX] ExRam mode // Value RAM $5C00-$5FFF RAM Nametable Extended Attr // %00 Write Only Yes No // %01 Write Only Yes Yes // %10 Read/Write No No // %11 Read Only No No self.regs.exram_mode.set(val); } 0x5105 => { // [.... ..HH] // [DDCC BBAA] // // Allows each Nametable slot to be configured: // [ A ][ B ] // [ C ][ D ] // // Values can be the following: // %00 = NES internal NTA // %01 = NES internal NTB // %10 = use ExRAM as NT // %11 = Fill Mode self.regs.nametable_mapping.set(val); // Typical mirroring setups would be: // D C B A // Horizontal: $50 01 01 00 00 // Vertical: $44 01 00 01 00 // SingleScreenA: $00 00 00 00 00 // SingleScreenB: $55 01 01 01 01 // SingleScreen ExRAM: $AA 10 10 10 10 // SingleScreen Fill: $FF 11 11 11 11 self.mirroring = match val { 0x50 => Mirroring::Horizontal, 0x44 => Mirroring::Vertical, 0x00 => Mirroring::SingleScreenA, 0x55 => Mirroring::SingleScreenB, // Any other combination means Mapper provides nametables _ => Mirroring::FourScreen, }; } 0x5106 => self.regs.fill.tile = val, // [TTTT TTTT] Fill Tile 0x5107 => self.regs.fill.attr = (val & 0x03).into(), // [.... ..AA] Fill Attribute bits 0x5113..=0x5117 => { // PRG Bank Switching // $5113: [.... .PPP] // 8k PRG-RAM @ $6000 // $5114-5117: [RPPP PPPP] // R = ROM select (0=select RAM, 1=select ROM) **unused in $5117** // P = PRG page let bank = (addr - 0x5113) as usize; self.regs.prg_banks[bank] = val as usize; self.update_prg_banks(); } 0x5120..=0x512B => { let bank = (addr - 0x5120) as usize; self.regs.chr_banks[bank] = val as usize; if addr < 0x5128 { self.update_chr_banks(ChrBank::Spr); } else { // Mirroring BG self.regs.chr_banks[bank + 4] = self.regs.chr_banks[bank]; self.update_chr_banks(ChrBank::Bg); } } 0x5130 => self.regs.chr_hi = (val as usize & 0x03) << 8, // [.... ..HH] CHR Bank Hi bits 0x5200 => { // [ES.T TTTT] Split control // E = Enable (0=split mode disabled, 1=split mode enabled) // S = Vsplit side (0=split will be on left side, 1=split will be on right) // T = tile number to split at self.regs.vsplit.enabled = val & 0x80 == 0x80; self.regs.vsplit.side = if val & 0x40 == 0x40 { Side::Right } else { Side::Left }; self.regs.vsplit.tile = val & 0x1F; } 0x5201 => self.regs.vsplit.scroll = val, // [YYYY YYYY] Split Y scroll 0x5202 => self.regs.vsplit.bank = val, // [CCCC CCCC] 4k CHR Page for split 0x5203 => self.regs.irq_scanline = u16::from(val), // [IIII IIII] IRQ Target 0x5204 => { self.regs.irq_enabled = val & 0x80 > 0; // [E... ....] IRQ Enable (0=disabled, 1=enabled) if !self.regs.irq_enabled { self.regs.irq_pending = false; } else if self.irq_state.pending { self.regs.irq_pending = true; } } 0x5205 => { self.regs.multiplicand = val; self.regs.mult_result = u16::from(self.regs.multiplicand) * u16::from(self.regs.multiplier); } 0x5206 => { self.regs.multiplier = val; self.regs.mult_result = u16::from(self.regs.multiplicand) * u16::from(self.regs.multiplier); } 0x5207..=0x5209 => {} 0x5C00..=0x5FFF => match self.regs.exram_mode.rw { ExRamRW::W => { let val = if self.ppu_status.rendering { val } else { 0x00 }; self.write_ex_ram(addr, val); } ExRamRW::RW => self.write_ex_ram(addr, val), _ => (), }, 0x6000..=0xDFFF if !self.rom_select(addr) => { self.prg_ram[self.prg_ram_banks.translate(addr)] = val; } _ => (), } } /// Synchronize a write to a PPU register at a given address. fn ppu_write(&mut self, addr: u16, val: u8) { match addr { 0x2000 => self.ppu_status.sprite8x16 = val & 0x20 > 0, 0x2001 => { self.ppu_status.rendering = val & 0x18 > 0; // BG or Spr rendering enabled if !self.ppu_status.rendering { self.irq_state.in_frame = false; self.irq_state.prev_addr = None; } } _ => (), } } /// Whether an IRQ is pending acknowledgement. fn irq_pending(&self) -> bool { self.regs.irq_pending || self.dmc.irq_pending } /// Whether an DMA is pending acknowledgement. fn dma_pending(&self) -> bool { self.dmc.dma_pending } /// Clear pending DMA. fn clear_dma_pending(&mut self) { self.dmc.dma_pending = false; } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for Exrom { fn reset(&mut self, _kind: ResetKind) { self.regs.prg_mode = PrgMode::Bank8k; self.regs.chr_mode = ChrMode::Bank1k; } } impl Clock for Exrom { fn clock(&mut self) { if self.ppu_status.reading { self.ppu_status.idle_count = 0; } else { self.ppu_status.idle_count += 1; // 3 CPU clocks == 1 ppu clock if self.ppu_status.idle_count == 3 { self.ppu_status.idle_count = 0; self.irq_state.in_frame = false; self.irq_state.prev_addr = None; } } self.ppu_status.reading = false; self.pulse1.clock(); self.pulse2.clock(); self.dmc.clock(); self.pulse_timer -= 1.0; if self.pulse_timer <= 0.0 { self.pulse1.clock_half_frame(); self.pulse2.clock_half_frame(); self.pulse_timer = Cpu::region_clock_rate(self.region) / 240.0; } self.pulse1.length.reload(); self.pulse2.length.reload(); self.cpu_cycle = self.cpu_cycle.wrapping_add(1); } } impl Regional for Exrom { fn region(&self) -> NesRegion { self.dmc.region() } fn set_region(&mut self, region: NesRegion) { self.dmc.set_region(region); } } impl Sram for Exrom { /// Save RAM to a given path. fn save(&self, path: impl AsRef) -> fs::Result<()> { fs::save(path.as_ref(), &self.prg_ram) } /// Load save RAM from a given path. fn load(&mut self, path: impl AsRef) -> fs::Result<()> { fs::load(path.as_ref()).map(|data: Memory>| self.prg_ram = data) } } impl Sample for Exrom { fn output(&self) -> f32 { let pulse1 = self.pulse1.output(); let pulse2 = self.pulse2.output(); let pulse = PULSE_TABLE[(pulse1 + pulse2) as usize]; let dmc = TND_TABLE[self.dmc.output() as usize]; -(pulse + dmc) } } impl std::fmt::Debug for Exrom { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Exrom") .field("regs", &self.regs) .field("mirroring", &self.mirroring) .field("ppu_status", &self.ppu_status) .field("irq_state", &self.irq_state) .field("exram_len", &self.ex_ram.len()) .field("prg_ram_banks", &self.prg_ram_banks) .field("prg_rom_banks", &self.prg_rom_banks) .field("chr_banks", &self.chr_banks) .field("tile_cache", &self.tile_cache) .field("last_chr_write", &self.last_chr_write) .field("region", &self.region) .field("pulse1", &self.pulse1) .field("pulse2", &self.pulse2) .field("dmc", &self.dmc) .field("dmc_mode", &self.dmc_mode) .field("cpu_cycle", &self.cpu_cycle) .field("pulse_timer", &self.pulse_timer) .finish() } } ================================================ FILE: tetanes-core/src/mapper/m007_axrom.rs ================================================ //! `AxROM` (Mapper 007). //! //! use crate::{ cart::Cart, common::{Clock, Regional, Reset, Sram}, mapper::{self, Map, Mapper}, mem::{Banks, Memory}, ppu::{CIRam, Mirroring}, }; use serde::{Deserialize, Serialize}; /// `AxROM` (Mapper 007). #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Axrom { pub chr: Memory>, pub prg_rom: Memory>, pub has_chr_ram: bool, pub mirroring: Mirroring, pub prg_rom_banks: Banks, } impl Axrom { const PRG_ROM_WINDOW: usize = 32 * 1024; const CHR_RAM_SIZE: usize = 8 * 1024; const SINGLE_SCREEN_B: u8 = 0b10000; /// Load `Axrom` from `Cart`. pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, ) -> Result { let (chr, has_chr_ram) = cart.chr_rom_or_ram(chr_rom, Self::CHR_RAM_SIZE); let prg_rom_banks = Banks::new(0x8000, 0xFFFF, prg_rom.len(), Self::PRG_ROM_WINDOW)?; let axrom = Self { chr, prg_rom, has_chr_ram, mirroring: cart.mirroring(), prg_rom_banks, }; Ok(axrom.into()) } } impl Map for Axrom { // PPU $0000..=$1FFF 8K CHR-RAM Bank Fixed // CPU $8000..=$FFFF 32K switchable PRG-ROM bank /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => self.chr[usize::from(addr)], 0x2000..=0x3EFF => ciram.peek(addr, self.mirroring), _ => 0, } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x8000..=0xFFFF => self.prg_rom[self.prg_rom_banks.translate(addr)], _ => 0, } } /// Write a byte to CHR-RAM/CIRAM at a given address. #[inline(always)] fn chr_write(&mut self, addr: u16, val: u8, ciram: &mut CIRam) { match addr { 0x0000..=0x1FFF => self.chr[usize::from(addr)] = val, 0x2000..=0x3EFF => ciram.write(addr, val, self.mirroring), _ => (), } } /// Write a byte to PRG-RAM at a given address. #[inline(always)] fn prg_write(&mut self, addr: u16, val: u8) { if let 0x8000..=0xFFFF = addr { self.prg_rom_banks.set(0, (val & 0x0F).into()); self.mirroring = if val & Self::SINGLE_SCREEN_B == Self::SINGLE_SCREEN_B { Mirroring::SingleScreenB } else { Mirroring::SingleScreenA }; } } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for Axrom {} impl Clock for Axrom {} impl Regional for Axrom {} impl Sram for Axrom {} ================================================ FILE: tetanes-core/src/mapper/m009_pxrom.rs ================================================ //! `PxROM`/`MMC2` (Mapper 009). //! //! use crate::{ cart::Cart, common::{Clock, Regional, Reset, ResetKind, Sram}, mapper::{self, Map, Mapper, Mirroring}, mem::{Banks, Memory}, ppu::CIRam, }; use serde::{Deserialize, Serialize}; /// `PxROM`/`MMC2` (Mapper 009). #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Pxrom { pub chr_rom: Memory>, pub prg_rom: Memory>, pub prg_ram: Memory>, pub mirroring: Mirroring, pub chr_banks: Banks, pub prg_rom_banks: Banks, // CHR-ROM $FD/0000 bank select ($B000-$BFFF) // CHR-ROM $FE/0000 bank select ($C000-$CFFF) // CHR-ROM $FD/1000 bank select ($D000-$DFFF) // CHR-ROM $FE/1000 bank select ($E000-$EFFF) // 7 bit 0 // ---- ---- // xxxC CCCC // | |||| // +-++++- Select 4K CHR-ROM bank for PPU $0000/$1000-$0FFF/$1FFF // used when latch 0/1 = $FD/$FE pub latch: [usize; 2], pub latch_banks: [u8; 4], } impl Pxrom { const PRG_WINDOW: usize = 8 * 1024; const CHR_ROM_WINDOW: usize = 4 * 1024; const PRG_RAM_SIZE: usize = 8 * 1024; const MIRRORING_MASK: u8 = 0x01; /// Load `Pxrom` from `Cart`. pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, ) -> Result { let prg_ram = Memory::with_ram_state(Self::PRG_RAM_SIZE, cart.ram_state); let chr_banks = Banks::new(0x0000, 0x1FFF, chr_rom.len(), Self::CHR_ROM_WINDOW)?; let prg_rom_banks = Banks::new(0x8000, 0xFFFF, prg_rom.len(), Self::PRG_WINDOW)?; let mut pxrom = Self { chr_rom, prg_rom, prg_ram, mirroring: cart.mirroring(), chr_banks, prg_rom_banks, latch: [0x00; 2], latch_banks: [0x00; 4], }; let last_bank = pxrom.prg_rom_banks.last(); pxrom.prg_rom_banks.set(1, last_bank - 2); pxrom.prg_rom_banks.set(2, last_bank - 1); pxrom.prg_rom_banks.set(3, last_bank); Ok(pxrom.into()) } pub fn update_banks(&mut self) { let bank0 = self.latch_banks[self.latch[0]] as usize; let bank1 = self.latch_banks[self.latch[1] + 2] as usize; self.chr_banks.set(0, bank0); self.chr_banks.set(1, bank1); } } impl Map for Pxrom { // PPU $0000..=$0FFF Two 4K switchable CHR-ROM banks // PPU $1000..=$1FFF Two 4K switchable CHR-ROM banks // CPU $6000..=$7FFF 8K PRG-RAM bank (PlayChoice version only) // CPU $8000..=$9FFF 8K switchable PRG-ROM bank // CPU $A000..=$FFFF Three 8K PRG-ROM banks, fixed to the last three banks /// Read a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_read(&mut self, addr: u16, ciram: &CIRam) -> u8 { let val = self.chr_peek(addr, ciram); // Update latch after read match addr { 0x0FD8 | 0x0FE8 | 0x1FD8..=0x1FDF | 0x1FE8..=0x1FEF => { let addr = addr as usize; self.latch[addr >> 12] = ((addr >> 4) & 0xFF) - 0xFD; self.update_banks(); } _ => (), } val } /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => self.chr_rom[self.chr_banks.translate(addr)], 0x2000..=0x3EFF => ciram.peek(addr, self.mirroring), _ => 0, } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x6000..=0x7FFF => self.prg_ram[usize::from(addr & 0x1FFF)], 0x8000..=0xFFFF => self.prg_rom[self.prg_rom_banks.translate(addr)], _ => 0, } } /// Write a byte to PRG-RAM at a given address. #[inline(always)] fn prg_write(&mut self, addr: u16, val: u8) { match addr { 0x6000..=0x7FFF => self.prg_ram[usize::from(addr & 0x1FFF)] = val, 0xA000..=0xAFFF => self.prg_rom_banks.set(0, (val & 0x0F).into()), 0xB000..=0xEFFF => { self.latch_banks[((addr - 0xB000) >> 12) as usize] = val & 0x1F; self.update_banks(); } 0xF000..=0xFFFF => { self.mirroring = match val & Self::MIRRORING_MASK { 0b00 => Mirroring::Vertical, _ => Mirroring::Horizontal, }; } _ => (), } } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for Pxrom { fn reset(&mut self, _kind: ResetKind) { self.latch = [0x00; 2]; self.latch_banks = [0x00; 4]; self.update_banks(); } } impl Clock for Pxrom {} impl Regional for Pxrom {} impl Sram for Pxrom {} ================================================ FILE: tetanes-core/src/mapper/m010_fxrom.rs ================================================ //! `FxROM`/`MMC4` (Mapper 010). //! //! use crate::{ cart::Cart, common::{Clock, Regional, Reset, ResetKind, Sram}, fs, mapper::{self, Map, Mapper, Mirroring}, mem::{Banks, Memory}, ppu::CIRam, }; use serde::{Deserialize, Serialize}; use std::path::Path; /// `FxROM`/`MMC4` (Mapper 010). #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Fxrom { pub chr_rom: Memory>, pub prg_rom: Memory>, pub prg_ram: Memory>, pub mirroring: Mirroring, pub chr_banks: Banks, pub prg_rom_banks: Banks, // CHR-ROM $FD/0000 bank select ($B000-$BFFF) // CHR-ROM $FE/0000 bank select ($C000-$CFFF) // CHR-ROM $FD/1000 bank select ($D000-$DFFF) // CHR-ROM $FE/1000 bank select ($E000-$EFFF) // 7 bit 0 // ---- ---- // xxxC CCCC // | |||| // +-++++- Select 4K CHR-ROM bank for PPU $0000/$1000-$0FFF/$1FFF // used when latch 0/1 = $FD/$FE pub latch: [usize; 2], pub latch_banks: [u8; 4], } impl Fxrom { const PRG_WINDOW: usize = 16 * 1024; const CHR_ROM_WINDOW: usize = 4 * 1024; const PRG_RAM_SIZE: usize = 8 * 1024; const MIRRORING_MASK: u8 = 0x01; /// Load `Fxrom` from `Cart`. pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, ) -> Result { let prg_ram = Memory::with_ram_state(Self::PRG_RAM_SIZE, cart.ram_state); let chr_banks = Banks::new(0x0000, 0x1FFF, chr_rom.len(), Self::CHR_ROM_WINDOW)?; let prg_rom_banks = Banks::new(0x8000, 0xFFFF, prg_rom.len(), Self::PRG_WINDOW)?; let mut fxrom = Self { chr_rom, prg_rom, prg_ram, mirroring: cart.mirroring(), chr_banks, prg_rom_banks, latch: [0x00; 2], latch_banks: [0x00; 4], }; fxrom.prg_rom_banks.set(1, fxrom.prg_rom_banks.last()); Ok(fxrom.into()) } pub fn update_banks(&mut self) { let bank0 = self.latch_banks[self.latch[0]] as usize; let bank1 = self.latch_banks[self.latch[1] + 2] as usize; self.chr_banks.set(0, bank0); self.chr_banks.set(1, bank1); } } impl Map for Fxrom { // PPU $0000..=$0FFF Two 4K switchable CHR-ROM banks // PPU $1000..=$1FFF Two 4K switchable CHR-ROM banks // CPU $6000..=$7FFF 8K PRG-RAM bank // CPU $8000..=$BFFF 16K switchable PRG-ROM bank // CPU $C000..=$FFFF 16K PRG-ROM bank, fixed to the last bank /// Read a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_read(&mut self, addr: u16, ciram: &CIRam) -> u8 { let val = self.chr_peek(addr, ciram); // Update latch after read match addr { 0x0FD8..=0x0FDF | 0x0FE8..=0xFEF | 0x1FD8..=0x1FDF | 0x1FE8..=0x1FEF => { let addr = addr as usize; self.latch[addr >> 12] = ((addr >> 4) & 0xFF) - 0xFD; self.update_banks(); } _ => (), } val } /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => self.chr_rom[self.chr_banks.translate(addr)], 0x2000..=0x3EFF => ciram.peek(addr, self.mirroring), _ => 0, } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x6000..=0x7FFF => self.prg_ram[usize::from(addr & 0x1FFF)], 0x8000..=0xFFFF => self.prg_rom[self.prg_rom_banks.translate(addr)], _ => 0, } } /// Write a byte to PRG-RAM at a given address. #[inline(always)] fn prg_write(&mut self, addr: u16, val: u8) { match addr { 0x6000..=0x7FFF => self.prg_ram[usize::from(addr & 0x1FFF)] = val, 0xA000..=0xAFFF => { self.prg_rom_banks.set(0, (val & 0x0F).into()); } 0xB000..=0xEFFF => { self.latch_banks[((addr - 0xB000) >> 12) as usize] = val & 0x1F; self.update_banks(); } 0xF000..=0xFFFF => { self.mirroring = match val & Self::MIRRORING_MASK { 0b00 => Mirroring::Vertical, _ => Mirroring::Horizontal, }; } _ => (), } } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for Fxrom { fn reset(&mut self, _kind: ResetKind) { self.latch = [0x00; 2]; self.latch_banks = [0x00; 4]; self.update_banks(); } } impl Sram for Fxrom { /// Save RAM to a given path. fn save(&self, path: impl AsRef) -> fs::Result<()> { fs::save(path.as_ref(), &self.prg_ram) } /// Load save RAM from a given path. fn load(&mut self, path: impl AsRef) -> fs::Result<()> { fs::load(path.as_ref()).map(|data: Memory>| self.prg_ram = data) } } impl Clock for Fxrom {} impl Regional for Fxrom {} ================================================ FILE: tetanes-core/src/mapper/m011_color_dreams.rs ================================================ //! `Color Dreams` (Mapper 011). //! //! use crate::{ cart::Cart, common::{Clock, Regional, Reset, Sram}, mapper::{self, Map, Mapper, Mirroring}, mem::{Banks, Memory}, ppu::CIRam, }; use serde::{Deserialize, Serialize}; /// `Color Dreams` (Mapper 011). #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct ColorDreams { pub chr_rom: Memory>, pub prg_rom: Memory>, pub mapper_num: u16, pub mirroring: Mirroring, pub chr_banks: Banks, pub prg_rom_banks: Banks, } impl ColorDreams { const PRG_WINDOW: usize = 32 * 1024; const CHR_ROM_WINDOW: usize = 8 * 1024; const CHR_BANK_MASK: u8 = 0b1111_0000; const PRG_BANK_MASK: u8 = 0b0000_0011; /// Load `ColorDreams` from `Cart`. pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, ) -> Result { let chr_banks = Banks::new(0x0000, 0x1FFF, chr_rom.len(), Self::CHR_ROM_WINDOW)?; let prg_rom_banks = Banks::new(0x8000, 0xFFFF, prg_rom.len(), Self::PRG_WINDOW)?; let color_dreams = Self { chr_rom, prg_rom, mapper_num: cart.mapper_num(), mirroring: cart.mirroring(), chr_banks, prg_rom_banks, }; Ok(color_dreams.into()) } } impl Map for ColorDreams { // PPU $0000..=$1FFF 8K switchable CHR-ROM bank // CPU $8000..=$FFFF 32K switchable PRG-ROM bank /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => self.chr_rom[self.chr_banks.translate(addr)], 0x2000..=0x3EFF => ciram.peek(addr, self.mirroring), _ => 0, } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x8000..=0xFFFF => self.prg_rom[self.prg_rom_banks.translate(addr)], _ => 0, } } /// Write a byte to PRG-RAM at a given address. #[inline(always)] fn prg_write(&mut self, addr: u16, mut val: u8) { if let 0x8000..=0xFFFF = addr { if self.mapper_num == 144 { // Intentionally defective variant where only the least significant bit alwys wins // bus conflict // See: val |= self.prg_read(addr) & 0x01; } self.chr_banks .set(0, ((val & Self::CHR_BANK_MASK) >> 4).into()); self.prg_rom_banks .set(0, (val & Self::PRG_BANK_MASK).into()); } } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for ColorDreams {} impl Clock for ColorDreams {} impl Regional for ColorDreams {} impl Sram for ColorDreams {} ================================================ FILE: tetanes-core/src/mapper/m018_jalecoss88006.rs ================================================ //! `Jaleco SS88006` (Mapper 018). //! //! use crate::{ cart::Cart, common::{Clock, Regional, Reset, ResetKind, Sram}, mapper::{self, Map, Mapper}, mem::{BankAccess, Banks, Memory}, ppu::{CIRam, Mirroring}, }; use serde::{Deserialize, Serialize}; /// `Jaleco SS88006` page bit. #[derive(Debug)] #[must_use] enum PageBit { Low, High, } impl PageBit { const fn page(&self, page: usize, val: u8) -> usize { let val = (val as usize) & 0x0F; match self { PageBit::Low => (page & 0xF0) | val, PageBit::High => (val << 4) | (page & 0x0F), } } } impl From for PageBit { fn from(addr: u16) -> Self { if addr & 0x01 == 0x01 { Self::High } else { Self::Low } } } /// `Jaleco SS88006` registers. #[derive(Default, Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Regs { pub irq_enabled: bool, pub irq_pending: bool, pub irq_reload: [u8; 4], pub irq_counter_size: u8, } /// `Jaleco SS88006` (Mapper 018). #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct JalecoSs88006 { pub chr_rom: Memory>, pub prg_rom: Memory>, pub prg_ram: Memory>, pub regs: Regs, pub irq_counter: u16, pub mirroring: Mirroring, pub chr_banks: Banks, pub prg_ram_banks: Banks, pub prg_rom_banks: Banks, } impl JalecoSs88006 { const PRG_WINDOW: usize = 8 * 1024; const PRG_RAM_SIZE: usize = 8 * 1024; const CHR_WINDOW: usize = 1024; const IRQ_MASKS: [u16; 4] = [0xFFFF, 0x0FFF, 0x00FF, 0x000F]; /// Load `JalecoSs88006` from `Cart`. pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, ) -> Result { let prg_ram = cart.prg_ram_or_default(Self::PRG_RAM_SIZE); let chr_banks = Banks::new(0x0000, 0x1FFF, chr_rom.len(), Self::CHR_WINDOW)?; let prg_ram_banks = Banks::new(0x6000, 0x7FFF, prg_ram.len(), Self::PRG_WINDOW)?; let prg_rom_banks = Banks::new(0x8000, 0xFFFF, prg_rom.len(), Self::PRG_WINDOW)?; let mut jalecoss88006 = Self { chr_rom, prg_rom, prg_ram, regs: Regs::default(), irq_counter: 0, mirroring: cart.mirroring(), chr_banks, prg_ram_banks, prg_rom_banks, }; jalecoss88006 .prg_rom_banks .set(3, jalecoss88006.prg_rom_banks.last()); Ok(jalecoss88006.into()) } fn update_prg_bank(&mut self, bank: usize, val: u8, bits: PageBit) { self.prg_rom_banks .set(bank, bits.page(self.prg_rom_banks.page(bank), val)); } fn update_chr_bank(&mut self, bank: usize, val: u8, bits: PageBit) { self.chr_banks .set(bank, bits.page(self.chr_banks.page(bank), val)); } } impl Map for JalecoSs88006 { // PPU $0000..=$03FF: 1K CHR Bank 1 Switchable // PPU $0400..=$07FF: 1K CHR Bank 2 Switchable // PPU $0800..=$0BFF: 1K CHR Bank 3 Switchable // PPU $0C00..=$0FFF: 1K CHR Bank 4 Switchable // PPU $1000..=$13FF: 1K CHR Bank 5 Switchable // PPU $1400..=$17FF: 1K CHR Bank 6 Switchable // PPU $1800..=$1BFF: 1K CHR Bank 7 Switchable // PPU $1C00..=$1FFF: 1K CHR Bank 8 Switchable // // CPU $6000..=$7FFF: 8K PRG-RAM Bank, if WRAM is present // CPU $8000..=$9FFF: 8K PRG-ROM Bank 1 Switchable // CPU $A000..=$BFFF: 8K PRG-ROM Bank 2 Switchable // CPU $C000..=$DFFF: 8K PRG-ROM Bank 3 Switchable // CPU $E000..=$FFFF: 8K PRG-ROM Bank 4 Fixed to last /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => self.chr_rom[self.chr_banks.translate(addr)], 0x2000..=0x3EFF => ciram.peek(addr, self.mirroring), _ => 0, } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x6000..=0x7FFF if self.prg_ram_banks.readable(addr) => { self.prg_ram[self.prg_ram_banks.translate(addr)] } 0x8000..=0xFFFF => self.prg_rom[self.prg_rom_banks.translate(addr)], _ => 0, } } /// Write a byte to PRG-RAM at a given address. fn prg_write(&mut self, addr: u16, val: u8) { match addr { 0x6000..=0x7FFF => { if self.prg_ram_banks.writable(addr) { self.prg_ram[self.prg_ram_banks.translate(addr)] = val; } } _ => match addr & 0xF003 { 0x8000 | 0x8001 => self.update_prg_bank(0, val, PageBit::from(addr)), 0x8002 | 0x8003 => self.update_prg_bank(1, val, PageBit::from(addr)), 0x9000 | 0x9001 => self.update_prg_bank(2, val, PageBit::from(addr)), 0x9002 => { let prg_ram_access = if val & 0x01 == 0x01 { if val & 0x02 == 0x02 { BankAccess::ReadWrite } else { BankAccess::Read } } else { BankAccess::None }; self.prg_ram_banks.set_access(0, prg_ram_access); } 0xA000 | 0xA001 => self.update_chr_bank(0, val, PageBit::from(addr)), 0xA002 | 0xA003 => self.update_chr_bank(1, val, PageBit::from(addr)), 0xB000 | 0xB001 => self.update_chr_bank(2, val, PageBit::from(addr)), 0xB002 | 0xB003 => self.update_chr_bank(3, val, PageBit::from(addr)), 0xC000 | 0xC001 => self.update_chr_bank(4, val, PageBit::from(addr)), 0xC002 | 0xC003 => self.update_chr_bank(5, val, PageBit::from(addr)), 0xD000 | 0xD001 => self.update_chr_bank(6, val, PageBit::from(addr)), 0xD002 | 0xD003 => self.update_chr_bank(7, val, PageBit::from(addr)), 0xE000..=0xE003 => self.regs.irq_reload[(addr & 0x03) as usize] = val, 0xF000 => { self.regs.irq_pending = false; self.irq_counter = u16::from(self.regs.irq_reload[0]) | (u16::from(self.regs.irq_reload[1]) << 4) | (u16::from(self.regs.irq_reload[2]) << 8) | (u16::from(self.regs.irq_reload[3]) << 12); } 0xF001 => { self.regs.irq_enabled = val & 0x01 == 0x01; self.regs.irq_pending = false; if val & 0x08 == 0x08 { self.regs.irq_counter_size = 3; } else if val & 0x04 == 0x04 { self.regs.irq_counter_size = 2; } else if val & 0x02 == 0x02 { self.regs.irq_counter_size = 1; } else { self.regs.irq_counter_size = 0; } } 0xF002 => { self.mirroring = match val & 0x03 { 0b00 => Mirroring::Horizontal, 0b01 => Mirroring::Vertical, 0b10 => Mirroring::SingleScreenA, _ => Mirroring::SingleScreenB, }; } 0xF003 => { // TODO: Expansion audio } _ => (), }, } } /// Whether an IRQ is pending acknowledgement. fn irq_pending(&self) -> bool { self.regs.irq_pending } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for JalecoSs88006 { fn reset(&mut self, kind: ResetKind) { self.regs = Regs::default(); if kind == ResetKind::Hard { self.prg_rom_banks.set(3, self.prg_rom_banks.last()); } } } impl Clock for JalecoSs88006 { fn clock(&mut self) { if self.regs.irq_enabled { let irq_mask = Self::IRQ_MASKS[self.regs.irq_counter_size as usize]; let counter = self.irq_counter & irq_mask; if counter == 0 { self.regs.irq_pending = true; } self.irq_counter = (self.irq_counter & !irq_mask) | (counter.wrapping_sub(1) & irq_mask); } } } impl Regional for JalecoSs88006 {} impl Sram for JalecoSs88006 {} ================================================ FILE: tetanes-core/src/mapper/m019_namco163.rs ================================================ //! `Namco163` (Mapper 019). //! //! use crate::{ cart::Cart, common::{Clock, Regional, Reset, ResetKind, Sample, Sram}, fs, mapper::{self, Map, Mapper}, mem::{BankAccess, Banks, ConstArray, Memory}, ppu::{CIRam, Mirroring}, }; use serde::{Deserialize, Serialize}; /// `Namco163` board. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[must_use] pub enum Board { #[default] Unknown, Namco163, Namco175, Namco340, } /// `Namco163` registers. #[derive(Default, Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Regs { pub irq_counter: u16, pub irq_pending: bool, pub nt_select_lo: bool, pub nt_select_hi: bool, pub prg_ram_protect: u8, } /// `Namco163` (Mapper 019). #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Namco163 { pub chr_rom: Memory>, pub prg_rom: Memory>, pub prg_ram: Memory>, pub regs: Regs, pub board: Board, pub mapper_num: u16, pub submapper_num: u8, pub audio: Audio, pub auto_detect_board: bool, pub mirroring: Mirroring, pub prg_ram_written_to: bool, pub nt_bank_enable: [bool; 12], pub chr_banks: Banks, pub prg_ram_banks: Banks, pub prg_rom_banks: Banks, } impl Namco163 { const PRG_WINDOW: usize = 8 * 1024; const PRG_RAM_SIZE: usize = 8 * 1024; const CHR_WINDOW: usize = 1024; /// Load `Namco163` from `Cart`. pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, ) -> Result { let mut auto_detect_board = false; let prg_ram = cart.prg_ram_or_default(Self::PRG_RAM_SIZE); let chr_banks = Banks::new(0x0000, 0x3FFF, chr_rom.len(), Self::CHR_WINDOW)?; let prg_ram_banks = Banks::new(0x6000, 0x7FFF, prg_ram.len(), Self::PRG_WINDOW)?; let prg_rom_banks = Banks::new(0x8000, 0xFFFF, prg_rom.len(), Self::PRG_WINDOW)?; let mut namco163 = Self { chr_rom, prg_rom, prg_ram, regs: Regs::default(), board: match cart.mapper_num() { 19 => { auto_detect_board = cart.game_info.is_none(); Board::Namco163 } 210 => match cart.submapper_num() { 1 => Board::Namco175, 2 => Board::Namco340, _ => { auto_detect_board = true; Board::Unknown } }, _ => Board::Unknown, }, mapper_num: cart.mapper_num(), submapper_num: cart.submapper_num(), audio: Audio::new(), auto_detect_board, mirroring: cart.mirroring(), prg_ram_written_to: false, nt_bank_enable: [false; 12], chr_banks, prg_ram_banks, prg_rom_banks, }; // Default 0x2000.=0x2FFF to NTRAM for bank in 8..12 { namco163.nt_bank_enable[bank] = true; namco163.chr_banks.set(bank, ((bank - 8) * 0x0400) & 0x03FF); } namco163.prg_rom_banks.set(3, namco163.prg_rom_banks.last()); namco163.update_prg_ram_access(); Ok(namco163.into()) } fn update_prg_ram_access(&mut self) { if self.prg_ram_banks.banks_len() == 0 { return; } let access = |read_write| { if read_write { BankAccess::ReadWrite } else { BankAccess::Read } }; let write_protect = self.regs.prg_ram_protect; match self.board { Board::Namco163 => { self.prg_ram_banks.set_access_range(0, 3, access(true)); } Board::Namco175 => { self.prg_ram_banks .set_access_range(0, 3, access(write_protect & 0x01 == 0x01)); } _ => { self.prg_ram_banks.set_access_range(0, 3, BankAccess::None); } } } #[inline] fn maybe_set_board(&mut self, board: Board) { if self.auto_detect_board && (!self.prg_ram_written_to || self.board != Board::Namco340) && self.board != board { tracing::debug!("auto detecting board: {board:?}"); self.board = board; } } } impl Map for Namco163 { // PPU $0000..=$03FF 1K CHR Bank 1 Switchable // PPU $0400..=$07FF 1K CHR Bank 2 Switchable // PPU $0800..=$0BFF 1K CHR Bank 3 Switchable // PPU $0C00..=$0FFF 1K CHR Bank 4 Switchable // PPU $1000..=$13FF 1K CHR Bank 5 Switchable // PPU $1400..=$17FF 1K CHR Bank 6 Switchable // PPU $1800..=$1BFF 1K CHR Bank 7 Switchable // PPU $1C00..=$1FFF 1K CHR Bank 8 Switchable // PPU $2000..=$23FF 1K CHR Bank 9 Switchable // PPU $2400..=$27FF 1K CHR Bank 10 Switchable // PPU $2800..=$2BFF 1K CHR Bank 11 Switchable // PPU $2C00..=$2FFF 1K CHR Bank 12 Switchable // // CPU $6000..=$7FFF 8K PRG-RAM Bank, if WRAM is present // CPU $8000..=$9FFF 8K PRG-ROM Bank 1 Switchable // CPU $A000..=$BFFF 8K PRG-ROM Bank 2 Switchable // CPU $C000..=$DFFF 8K PRG-ROM Bank 3 Switchable // CPU $E000..=$FFFF 8K PRG-ROM Bank 4, fixed to last // $0400..=$07FF bank 1 > page N -> addr + page * $0400 // $0800..=$0BFF bank 2 -> page N -> addr + page * $0400 // $0C00..=$0FFF bank 3 -> page N -> addr + page * $0400 // $1000..=$13FF bank 4 -> page N -> addr + page * $0400 // $1400..=$17FF bank 5 -> page N -> addr + page * $0400 // $1800..=$1BFF bank 6 -> page N -> addr + page * $0400 // $1C00..=$1FFF bank 7 -> page N -> addr + page * $0400 // $2000..=$23FF bank 8 -> page N -> addr + page * $0400 // $2400..=$27FF bank 9 -> page N -> addr + page * $0400 // $2800..=$2BFF bank 10 -> page N -> addr + page * $0400 // $2C00..=$2FFF bank 11 -> page N -> addr + page * $0400 /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x3EFF => { let bank = addr >> 10; let addr = self.chr_banks.translate(addr); if self.nt_bank_enable[bank as usize] { ciram[addr] } else { self.chr_rom[addr] } } _ => 0, } } /// Read a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_read(&mut self, addr: u16) -> u8 { match addr { 0x4800..=0x4FFF => self.audio.read_register(addr), _ => self.prg_peek(addr), } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x6000..=0x7FFF => { if self.prg_ram_banks.readable(addr) { self.prg_ram[self.prg_ram_banks.translate(addr)] } else { 0 } } 0x8000..=0xFFFF => self.prg_rom[self.prg_rom_banks.translate(addr)], _ => match addr & 0xF800 { 0x4800 => self.audio.peek_register(addr), 0x5000 => (self.regs.irq_counter & 0xFF) as u8, 0x5800 => (self.regs.irq_counter >> 8) as u8, _ => 0, }, } } /// Write a byte to CHR-RAM/CIRAM at a given address. #[inline(always)] fn chr_write(&mut self, addr: u16, val: u8, ciram: &mut CIRam) { if let 0x0000..=0x3EFF = addr { let bank = addr >> 10; let addr = self.chr_banks.translate(addr); if self.nt_bank_enable[bank as usize] { ciram[addr] = val; } } } /// Write a byte to PRG-RAM at a given address. fn prg_write(&mut self, addr: u16, val: u8) { match addr { 0x4800..=0x4FFF => { self.maybe_set_board(Board::Namco163); self.audio.write_register(addr, val) } 0x5000..=0x57FF => { self.maybe_set_board(Board::Namco163); self.regs.irq_counter = (self.regs.irq_counter & 0xFF00) | u16::from(val); self.regs.irq_pending = false; } 0x5800..=0x5FFF => { self.maybe_set_board(Board::Namco163); self.regs.irq_counter = (self.regs.irq_counter & 0xFF) | (u16::from(val) << 8); self.regs.irq_pending = false; } 0x6000..=0x7FFF => { self.prg_ram_written_to = true; if self.board == Board::Namco340 { self.maybe_set_board(Board::Unknown); } if self.prg_ram_banks.writable(addr) { self.prg_ram[self.prg_ram_banks.translate(addr)] = val; } } 0x8000..=0xDFFF => { if addr >= 0xC800 { self.maybe_set_board(Board::Namco163); } else if addr >= 0xC000 && self.board != Board::Namco163 { self.maybe_set_board(Board::Namco175); } if addr >= 0xC000 && self.board == Board::Namco175 { self.regs.prg_ram_protect = val; self.update_prg_ram_access(); } else { let bank = ((addr - 0x8000) >> 11) as usize; let nt_select = match addr { 0x8000..=0x9FFF => !self.regs.nt_select_lo, 0xA000..=0xBFFF => !self.regs.nt_select_hi, _ => true, }; let nt_bank_enable = nt_select && val >= 0xE0 && self.board == Board::Namco163; self.nt_bank_enable[bank] = nt_bank_enable; if nt_bank_enable { self.chr_banks.set(bank, (val & 0x01).into()); } else { self.chr_banks.set(bank, val.into()); } } } 0xE000..=0xE7FF => { if val & 0x80 == 0x80 || (val & 0x40 == 0x40 && self.board != Board::Namco163) { self.maybe_set_board(Board::Namco340); } self.prg_rom_banks.set(0, (val & 0x3F).into()); match self.board { Board::Namco340 => { self.mirroring = match (val & 0xC0) >> 6 { 0b00 => Mirroring::SingleScreenA, 0b01 => Mirroring::Vertical, 0b10 => Mirroring::Horizontal, _ => Mirroring::SingleScreenB, }; } Board::Namco163 => self.audio.write_register(addr, val), _ => (), } } 0xE800..=0xEFFF => { self.prg_rom_banks.set(1, (val & 0x3F).into()); if self.board == Board::Namco163 { self.regs.nt_select_lo = (val & 0x40) == 0x40; self.regs.nt_select_hi = (val & 0x80) == 0x80; } } 0xF000..=0xF7FF => self.prg_rom_banks.set(2, (val & 0x3F).into()), 0xF800..=0xFFFF => { self.maybe_set_board(Board::Namco163); if self.board == Board::Namco163 { self.regs.prg_ram_protect = val; self.update_prg_ram_access(); self.audio.write_register(addr, val); } } _ => (), } } /// Whether an IRQ is pending acknowledgement. fn irq_pending(&self) -> bool { self.regs.irq_pending } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for Namco163 { fn reset(&mut self, kind: ResetKind) { if kind == ResetKind::Hard { self.regs = Regs::default(); } for bank in 8..12 { self.nt_bank_enable[bank] = true; self.chr_banks.set(bank, ((bank - 8) * 0x0400) & 0x03FF); } self.prg_ram_written_to = false; self.prg_rom_banks.set(3, self.prg_rom_banks.last()); self.update_prg_ram_access(); self.audio = Audio::new(); } } impl Clock for Namco163 { fn clock(&mut self) { if self.regs.irq_counter & 0x8000 > 0 && self.regs.irq_counter & 0x7FFF != 0x7FFF { self.regs.irq_counter = self.regs.irq_counter.wrapping_add(1); if self.regs.irq_counter & 0x7FFF == 0x7FFF { self.regs.irq_pending = true; } } if self.board == Board::Namco163 { self.audio.clock(); } } } impl Regional for Namco163 {} impl Sram for Namco163 { fn save(&self, path: impl AsRef) -> fs::Result<()> { fs::save(path.as_ref(), &(&self.prg_ram, &self.audio.ram)) } fn load(&mut self, path: impl AsRef) -> fs::Result<()> { fs::load::<(Memory>, ConstArray)>(path.as_ref()).map( |(prg_ram, audio_ram)| { self.prg_ram = prg_ram; self.audio.ram = audio_ram; }, ) } } impl Sample for Namco163 { fn output(&self) -> f32 { self.audio.output() } } #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Audio { pub ram: ConstArray, pub addr: usize, pub auto_increment: bool, pub disabled: bool, pub update_counter: u8, pub current_channel: i8, pub channel_out: [f32; Self::CHANNEL_COUNT], pub out: f32, #[serde(skip, default)] phase_ext: [u32; Self::CHANNEL_COUNT], } impl Default for Audio { fn default() -> Self { Self::new() } } impl Audio { const CHANNEL_COUNT: usize = 8; const REG_FREQ_LOW: usize = 0x00; const REG_FREQ_MID: usize = 0x02; const REG_FREQ_HIGH: usize = 0x04; const REG_WAVE_LEN: usize = 0x04; const REG_WAVE_ADDR: usize = 0x06; const REG_VOLUME: usize = 0x07; pub fn new() -> Self { Self { ram: ConstArray::new(), addr: 0, auto_increment: false, disabled: false, update_counter: 0, current_channel: 7, channel_out: [0.0; Self::CHANNEL_COUNT], out: 0.0, phase_ext: [0; Self::CHANNEL_COUNT], } } #[must_use] pub fn read_register(&mut self, addr: u16) -> u8 { let val = self.peek_register(addr); if self.auto_increment { self.addr = (self.addr + 1) & 0x7F; } val } #[must_use] #[allow(clippy::missing_const_for_fn)] // false positive on non-const deref coercion pub fn peek_register(&self, addr: u16) -> u8 { if matches!(addr, 0x4800..=0x4FFF) { self.ram[self.addr] } else { 0 } } pub fn write_register(&mut self, addr: u16, val: u8) { match addr { 0x4800..=0x4FFF => { self.ram[self.addr] = val; if self.auto_increment { self.addr = (self.addr + 1) & 0x7F; } } 0xE000..=0xE7FF => self.disabled = val & 0x40 == 0x40, 0xF800..=0xFFFF => { self.addr = (val & 0x7F).into(); self.auto_increment = val & 0x80 == 0x80; } _ => (), } } #[must_use] #[inline] pub const fn output(&self) -> f32 { // TODO: -40db - it's not accurate according to https://www.nesdev.org/wiki/Namco_163_audio#Mixing // but it's way too loud otherwise. Should fix root cause and update to use NES 2.0 // submapper_num, if set 0.0001 * self.out } #[inline] fn update_output(&mut self) { // "Because the high frequency generated by the channel cycling can be unpleasant, and // emulation of high frequency audio can be difficult, it is often preferred to simply sum // the channel outputs, and divide the output volume by the number of active channels." // See: https://www.nesdev.org/wiki/Namco_163_audio#Mixing let channel_count = usize::from(self.channel_count()); self.out = self.channel_out.iter().skip(7 - channel_count).sum::() / (channel_count + 1) as f32; } #[must_use] #[inline] const fn base_addr(&self) -> usize { (0x40 + self.current_channel * 0x08) as usize } #[must_use] #[inline] const fn phase(&self) -> u32 { self.phase_ext[self.current_channel as usize] } #[must_use] #[inline] fn wave_length(&self) -> u32 { let base_addr = self.base_addr(); 256 - u32::from(self.ram[base_addr + Self::REG_WAVE_LEN] & 0xFC) } #[must_use] #[inline] fn wave_address(&self) -> u32 { let base_addr = self.base_addr(); u32::from(self.ram[base_addr + Self::REG_WAVE_ADDR]) } #[must_use] #[inline] #[allow(clippy::missing_const_for_fn)] // false positive on non-const deref coercion fn volume(&self) -> u8 { let base_addr = self.base_addr(); self.ram[base_addr + Self::REG_VOLUME] & 0x0F } #[inline] const fn set_phase(&mut self, phase: u32) { self.phase_ext[self.current_channel as usize] = phase; } #[must_use] #[inline] fn frequency(&self) -> u32 { let base_addr = self.base_addr(); let freq_high = u32::from(self.ram[base_addr + Self::REG_FREQ_HIGH] & 0x03) << 16; let freq_mid = u32::from(self.ram[base_addr + Self::REG_FREQ_MID]) << 8; let freq_low = u32::from(self.ram[base_addr + Self::REG_FREQ_LOW]); freq_high | freq_mid | freq_low } #[inline] fn update_channel(&mut self) { let mut phase = self.phase(); let frequency = self.frequency(); let wave_length = self.wave_length(); let wave_addr = self.wave_address(); let volume = self.volume(); phase = (phase + frequency) % (wave_length << 16); let sample_addr = (((phase >> 16) + wave_addr) & 0xFF) as usize; let sample = if sample_addr & 0x01 == 0x01 { self.ram[sample_addr / 2] >> 4 } else { self.ram[sample_addr / 2] & 0x0F }; self.channel_out[self.current_channel as usize] = sample.wrapping_sub(8) as f32 * volume as f32; self.update_output(); self.set_phase(phase); } #[must_use] #[inline] #[allow(clippy::missing_const_for_fn)] // false positive on non-const deref coercion fn channel_count(&self) -> u8 { (self.ram[0x7F] >> 4) & 0x07 } } impl Clock for Audio { fn clock(&mut self) { if !self.disabled { self.update_counter += 1; if self.update_counter == 15 { self.update_counter = 0; self.update_channel(); self.current_channel -= 1; if self.current_channel < 7 - self.channel_count() as i8 { self.current_channel = 7; } } } } } ================================================ FILE: tetanes-core/src/mapper/m024_m026_vrc6.rs ================================================ //! `VRC6` (Mapper 024). //! //! use crate::{ apu::PULSE_TABLE, cart::Cart, common::{Clock, Regional, Reset, ResetKind, Sample, Sram}, mapper::{self, Map, Mapper, vrc_irq::VrcIrq}, mem::{Banks, Memory}, ppu::{CIRam, Mirroring}, }; use serde::{Deserialize, Serialize}; /// `VRC6` revision. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[must_use] pub enum Revision { /// VRC6a #[default] A, /// VRC6b B, } /// `VRC6` registers. #[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct Regs { pub banking_mode: u8, pub prg: [usize; 4], pub chr: [usize; 8], } /// `VRC6` (Mapper 024). #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Vrc6 { pub chr_rom: Memory>, pub prg_rom: Memory>, pub prg_ram: Memory>, pub regs: Regs, pub revision: Revision, pub mirroring: Mirroring, pub irq: VrcIrq, pub audio: Audio, pub nt_banks: [usize; 4], pub chr_banks: Banks, pub prg_ram_banks: Banks, pub prg_rom_banks: Banks, } impl Vrc6 { const PRG_RAM_SIZE: usize = 8 * 1024; const PRG_WINDOW: usize = 8 * 1024; const CHR_WINDOW: usize = 1024; /// Load `Vrc6` from `Cart`. pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, revision: Revision, ) -> Result { let prg_ram = cart.prg_ram_or_default(Self::PRG_RAM_SIZE); let chr_banks = Banks::new(0x0000, 0x1FFF, chr_rom.len(), Self::CHR_WINDOW)?; let prg_ram_banks = Banks::new(0x6000, 0x7FFF, prg_ram.len(), Self::PRG_RAM_SIZE)?; let prg_rom_banks = Banks::new(0x8000, 0xFFFF, prg_rom.len(), Self::PRG_WINDOW)?; let mut vrc6 = Self { chr_rom, prg_rom, prg_ram, regs: Regs::default(), revision, mirroring: cart.mirroring(), irq: VrcIrq::default(), audio: Audio::new(), nt_banks: [0; 4], chr_banks, prg_ram_banks, prg_rom_banks, }; vrc6.prg_rom_banks.set(3, vrc6.prg_rom_banks.last()); Ok(vrc6.into()) } #[inline(always)] #[must_use] pub const fn prg_ram_enabled(&self) -> bool { self.regs.banking_mode & 0x80 == 0x80 } pub fn set_nametables(&mut self, nametables: &[usize]) { for (bank, page) in nametables.iter().enumerate() { self.set_nametable_page(bank, *page); } } pub fn set_mirroring(&mut self, mirroring: Mirroring) { self.mirroring = mirroring; match self.mirroring { Mirroring::Vertical => self.set_nametables(&[0, 1, 0, 1]), Mirroring::Horizontal => self.set_nametables(&[0, 0, 1, 1]), Mirroring::SingleScreenA => self.set_nametables(&[0, 0, 0, 0]), Mirroring::SingleScreenB => self.set_nametables(&[1, 1, 1, 1]), Mirroring::FourScreen => self.set_nametables(&[0, 1, 2, 3]), } } pub const fn set_nametable_page(&mut self, bank: usize, page: usize) { self.nt_banks[bank] = page; } pub fn update_chr_banks(&mut self) { let (mask, or_mask) = if self.regs.banking_mode & 0x20 == 0x20 { (0xFE, 1) } else { (0xFF, 0) }; match self.regs.banking_mode & 0x03 { 0 => { self.chr_banks.set(0, self.regs.chr[0]); self.chr_banks.set(1, self.regs.chr[1]); self.chr_banks.set(2, self.regs.chr[2]); self.chr_banks.set(3, self.regs.chr[3]); self.chr_banks.set(4, self.regs.chr[4]); self.chr_banks.set(5, self.regs.chr[5]); self.chr_banks.set(6, self.regs.chr[6]); self.chr_banks.set(7, self.regs.chr[7]); } 1 => { self.chr_banks.set(0, self.regs.chr[0] & mask); self.chr_banks.set(1, (self.regs.chr[0] & mask) | or_mask); self.chr_banks.set(2, self.regs.chr[1] & mask); self.chr_banks.set(3, (self.regs.chr[1] & mask) | or_mask); self.chr_banks.set(4, self.regs.chr[2] & mask); self.chr_banks.set(5, (self.regs.chr[2] & mask) | or_mask); self.chr_banks.set(6, self.regs.chr[3] & mask); self.chr_banks.set(7, (self.regs.chr[3] & mask) | or_mask); } _ => { self.chr_banks.set(0, self.regs.chr[0]); self.chr_banks.set(1, self.regs.chr[1]); self.chr_banks.set(2, self.regs.chr[2]); self.chr_banks.set(3, self.regs.chr[3]); self.chr_banks.set(4, self.regs.chr[4] & mask); self.chr_banks.set(5, (self.regs.chr[4] & mask) | or_mask); self.chr_banks.set(6, self.regs.chr[5] & mask); self.chr_banks.set(7, (self.regs.chr[5] & mask) | or_mask); } } if self.regs.banking_mode & 0x10 == 0x10 { // CHR-ROM self.set_mirroring(Mirroring::FourScreen); match self.regs.banking_mode & 0x2F { 0x20 | 0x27 => { self.set_nametable_page(0, self.regs.chr[6] & 0xFE); self.set_nametable_page(1, (self.regs.chr[6] & 0xFE) | 1); self.set_nametable_page(2, self.regs.chr[7] & 0xFE); self.set_nametable_page(3, (self.regs.chr[7] & 0xFE) | 1); } 0x23 | 0x24 => { self.set_nametable_page(0, self.regs.chr[6] & 0xFE); self.set_nametable_page(1, self.regs.chr[7] & 0xFE); self.set_nametable_page(2, (self.regs.chr[6] & 0xFE) | 1); self.set_nametable_page(3, (self.regs.chr[7] & 0xFE) | 1); } 0x28 | 0x2F => { self.set_nametable_page(0, self.regs.chr[6] & 0xFE); self.set_nametable_page(1, self.regs.chr[6] & 0xFE); self.set_nametable_page(2, self.regs.chr[7] & 0xFE); self.set_nametable_page(3, self.regs.chr[7] & 0xFE); } 0x2B | 0x2C => { self.set_nametable_page(0, (self.regs.chr[6] & 0xFE) | 1); self.set_nametable_page(1, (self.regs.chr[7] & 0xFE) | 1); self.set_nametable_page(2, (self.regs.chr[6] & 0xFE) | 1); self.set_nametable_page(3, (self.regs.chr[7] & 0xFE) | 1); } _ => match self.regs.banking_mode & 0x07 { 0 | 6 | 7 => { self.set_nametable_page(0, self.regs.chr[6]); self.set_nametable_page(1, self.regs.chr[6]); self.set_nametable_page(2, self.regs.chr[7]); self.set_nametable_page(3, self.regs.chr[7]); } 1 | 5 => { self.set_nametable_page(0, self.regs.chr[4]); self.set_nametable_page(1, self.regs.chr[5]); self.set_nametable_page(2, self.regs.chr[6]); self.set_nametable_page(3, self.regs.chr[7]); } _ => { self.set_nametable_page(0, self.regs.chr[6]); self.set_nametable_page(1, self.regs.chr[7]); self.set_nametable_page(2, self.regs.chr[6]); self.set_nametable_page(3, self.regs.chr[7]); } }, } } else { // CIRAM match self.regs.banking_mode & 0x2F { 0x20 | 0x27 => self.set_mirroring(Mirroring::Vertical), 0x23 | 0x24 => self.set_mirroring(Mirroring::Horizontal), 0x28 | 0x2F => self.set_mirroring(Mirroring::SingleScreenA), 0x2B | 0x2C => self.set_mirroring(Mirroring::SingleScreenB), _ => { self.set_mirroring(Mirroring::FourScreen); match self.regs.banking_mode & 0x07 { 0 | 6 | 7 => { self.set_nametable_page(0, self.regs.chr[6] & 0x01); self.set_nametable_page(1, self.regs.chr[6] & 0x01); self.set_nametable_page(2, self.regs.chr[7] & 0x01); self.set_nametable_page(3, self.regs.chr[7] & 0x01); } 1 | 5 => { self.set_nametable_page(0, self.regs.chr[4] & 0x01); self.set_nametable_page(1, self.regs.chr[5] & 0x01); self.set_nametable_page(2, self.regs.chr[6] & 0x01); self.set_nametable_page(3, self.regs.chr[7] & 0x01); } _ => { self.set_nametable_page(0, self.regs.chr[6] & 0x01); self.set_nametable_page(1, self.regs.chr[7] & 0x01); self.set_nametable_page(2, self.regs.chr[6] & 0x01); self.set_nametable_page(3, self.regs.chr[7] & 0x01); } } } } } } } impl Map for Vrc6 { // PPU $0000..=$03FF 1K switchable CHR-ROM bank // PPU $0400..=$07FF 1K switchable CHR-ROM bank // PPU $0800..=$0BFF 1K switchable CHR-ROM bank // PPU $0C00..=$0FFF 1K switchable CHR-ROM bank // PPU $1000..=$13FF 1K switchable CHR-ROM bank // PPU $1400..=$17FF 1K switchable CHR-ROM bank // PPU $1800..=$1BFF 1K switchable CHR-ROM bank // PPU $1C00..=$1FFF 1K switchable CHR-ROM bank // PPU $2000..=$3EFF Switchable Nametables // // CPU $6000..=$7FFF 8K PRG-RAM bank, fixed // CPU $8000..=$BFFF 16K switchable PRG-ROM bank // CPU $C000..=$DFFF 8K switchable PRG-ROM bank // CPU $E000..=$FFFF 8K PRG-ROM bank, fixed to the last bank /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => self.chr_rom[self.chr_banks.translate(addr)], 0x2000..=0x3EFF => { let addr = addr - 0x2000; let a10 = (self.nt_banks[((addr >> 10) & 0x03) as usize] << 10) as u16; let addr = a10 | (!a10 & addr); if self.regs.banking_mode & 0x10 == 0x00 { ciram[addr.into()] } else { self.chr_rom[self.chr_banks.translate(addr)] } } _ => 0, } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x6000..=0x7FFF if self.prg_ram_enabled() => { self.prg_ram[self.prg_ram_banks.translate(addr)] } 0x8000..=0xFFFF => self.prg_rom[self.prg_rom_banks.translate(addr)], _ => 0, } } /// Write a byte to PRG-RAM at a given address. fn prg_write(&mut self, mut addr: u16, val: u8) { match addr { 0x6000..=0x7FFF => { if self.prg_ram_enabled() { self.prg_ram[self.prg_ram_banks.translate(addr)] = val; } } _ => { if self.revision == Revision::B { // Revision B swaps A0 and A1 lines addr = (addr & 0xFFFC) | ((addr & 0x01) << 1) | ((addr & 0x02) >> 1); } // Only A0, A1 and A12-15 are used for registers, remaining addresses are mirrored. match addr & 0xF003 { 0x8000..=0x8003 => { // [.... PPPP] // |||| // ++++- Select 16 KB PRG-ROM bank at $8000-$BFFF self.prg_rom_banks .set_range(0, 1, ((val & 0x0F) << 1).into()); } 0x9000..=0x9003 | 0xA000..=0xA002 | 0xB000..=0xB002 => { self.audio.write_register(addr, val); } 0xB003 => { // [W.PN MMDD] // | || |||| // | || ||++- PPU banking mode; see below // | || ++--- Mirroring varies by banking mode, see below // | |+------ 1: Nametables come from CHRROM, 0: Nametables come from CIRAM // | +------- CHR A10 is 1: subject to further rules 0: according to the latched value // +--------- PRG RAM enable self.regs.banking_mode = val; self.update_chr_banks(); } 0xC000..=0xC003 => { // [...P PPPP] // | |||| // +-++++- Select 8 KB PRG-ROM bank at $C000-$DFFF self.prg_rom_banks.set(2, (val & 0x1F).into()); } 0xD000..=0xD003 => { self.regs.chr[(addr & 0x03) as usize] = val.into(); self.update_chr_banks(); } 0xE000..=0xE003 => { self.regs.chr[(4 + (addr & 0x03)) as usize] = val.into(); self.update_chr_banks(); } 0xF000 => self.irq.write_reload(val), 0xF001 => self.irq.write_control(val), 0xF002 => self.irq.acknowledge(), _ => (), } } } } /// Whether an IRQ is pending acknowledgement. fn irq_pending(&self) -> bool { self.irq.irq_pending } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for Vrc6 { fn reset(&mut self, kind: ResetKind) { self.irq.reset(kind); self.audio.reset(kind); } } impl Clock for Vrc6 { fn clock(&mut self) { self.irq.clock(); self.audio.clock(); } } impl Regional for Vrc6 {} impl Sram for Vrc6 {} impl Sample for Vrc6 { fn output(&self) -> f32 { self.audio.output() } } #[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct Audio { pub pulse1: Pulse, pub pulse2: Pulse, pub saw: Saw, pub halt: bool, pub out: f32, } impl Default for Audio { fn default() -> Self { Self::new() } } impl Audio { const fn new() -> Self { Self { pulse1: Pulse::new(), pulse2: Pulse::new(), saw: Saw::new(), halt: false, out: 0.0, } } #[must_use] fn output(&self) -> f32 { let pulse_scale = PULSE_TABLE[PULSE_TABLE.len() - 1] / 15.0; pulse_scale * self.out } fn write_register(&mut self, addr: u16, val: u8) { // Only A0, A1 and A12-15 are used for registers, remaining addresses are mirrored. match addr & 0xF003 { 0x9000..=0x9002 => self.pulse1.write_register(addr, val), 0x9003 => { self.halt = val & 0x01 == 0x01; let freq_shift = if val & 0x04 == 0x04 { 8 } else if val & 0x02 == 0x02 { 4 } else { 0 }; self.pulse1.set_freq_shift(freq_shift); self.pulse2.set_freq_shift(freq_shift); self.saw.set_freq_shift(freq_shift); } 0xA000..=0xA002 => self.pulse2.write_register(addr, val), 0xB000..=0xB002 => self.saw.write_register(addr, val), _ => unreachable!("impossible Vrc6Audio register: {}", addr), } } } impl Clock for Audio { fn clock(&mut self) { if !self.halt { self.pulse1.clock(); self.pulse2.clock(); self.saw.clock(); self.out = self.pulse1.volume() + self.pulse2.volume() + self.saw.volume(); } } } impl Reset for Audio { fn reset(&mut self, _kind: ResetKind) { self.halt = false; } } #[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct Pulse { pub enabled: bool, pub volume: u8, pub duty_cycle: u8, pub ignore_duty: bool, pub frequency: u16, pub timer: u16, pub step: u8, pub freq_shift: u8, } impl Default for Pulse { fn default() -> Self { Self::new() } } impl Pulse { const fn new() -> Self { Self { enabled: false, volume: 0, duty_cycle: 0, ignore_duty: false, frequency: 1, timer: 1, step: 0, freq_shift: 0, } } fn write_register(&mut self, addr: u16, val: u8) { match addr & 0x03 { 0 => { self.volume = val & 0x0F; self.duty_cycle = (val & 0x70) >> 4; self.ignore_duty = val & 0x80 == 0x80; } 1 => self.frequency = (self.frequency & 0x0F00) | u16::from(val), 2 => { self.frequency = ((u16::from(val) & 0x0F) << 8) | (self.frequency & 0xFF); self.enabled = val & 0x80 == 0x80; if !self.enabled { self.step = 0; } } _ => unreachable!("impossible Vrc6Pulse register: {}", addr), } } const fn set_freq_shift(&mut self, val: u8) { self.freq_shift = val; } fn volume(&self) -> f32 { if self.enabled && (self.ignore_duty || self.step <= self.duty_cycle) { f32::from(self.volume) } else { 0.0 } } } impl Clock for Pulse { fn clock(&mut self) { if self.enabled { self.timer -= 1; if self.timer == 0 { self.step = (self.step + 1) & 0x0F; self.timer = (self.frequency >> self.freq_shift) + 1; } } } } #[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct Saw { pub enabled: bool, pub accum: u8, pub accum_rate: u8, pub frequency: u16, pub timer: u16, pub step: u8, pub freq_shift: u8, } impl Default for Saw { fn default() -> Self { Self::new() } } impl Saw { const fn new() -> Self { Self { enabled: false, accum: 0, accum_rate: 0, frequency: 1, timer: 1, step: 0, freq_shift: 0, } } fn write_register(&mut self, addr: u16, val: u8) { match addr & 0x03 { 0 => { self.accum_rate = val & 0x3F; } 1 => self.frequency = (self.frequency & 0x0F00) | u16::from(val), 2 => { self.frequency = ((u16::from(val) & 0x0F) << 8) | (self.frequency & 0xFF); self.enabled = val & 0x80 == 0x80; if !self.enabled { self.accum = 0; self.step = 0; } } _ => unreachable!("impossible Vrc6Saw register: {}", addr), } } const fn set_freq_shift(&mut self, val: u8) { self.freq_shift = val; } fn volume(&self) -> f32 { if self.enabled { f32::from(self.accum >> 3) } else { 0.0 } } } impl Clock for Saw { fn clock(&mut self) { if self.enabled { self.timer -= 1; if self.timer == 0 { self.step = (self.step + 1) % 14; self.timer = (self.frequency >> self.freq_shift) + 1; if self.step == 0 { self.accum = 0; } else if self.step & 0x01 == 0x00 { self.accum += self.accum_rate; } } } } } ================================================ FILE: tetanes-core/src/mapper/m034_bnrom.rs ================================================ //! `BNROM` (Mapper 034). //! //! use crate::{ cart::Cart, common::{Clock, Regional, Reset, Sram}, mapper::{self, Map, Mapper}, mem::{Banks, Memory}, ppu::{CIRam, Mirroring}, }; use serde::{Deserialize, Serialize}; /// `BNROM` (Mapper 034). #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Bnrom { pub chr: Memory>, pub prg_rom: Memory>, pub has_chr_ram: bool, pub mirroring: Mirroring, pub prg_rom_banks: Banks, } impl Bnrom { const PRG_ROM_WINDOW: usize = 32 * 1024; const CHR_RAM_SIZE: usize = 8 * 1024; /// Load `Bnrom` from `Cart`. pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, ) -> Result { let (chr, has_chr_ram) = cart.chr_rom_or_ram(chr_rom, Self::CHR_RAM_SIZE); let prg_rom_banks = Banks::new(0x8000, 0xFFFF, prg_rom.len(), Self::PRG_ROM_WINDOW)?; let bnrom = Self { chr, prg_rom, has_chr_ram, mirroring: cart.mirroring(), prg_rom_banks, }; Ok(bnrom.into()) } } impl Map for Bnrom { // PPU $0000..=$1FFF 8K CHR-RAM Bank Fixed // CPU $8000..=$FFFF 32K switchable PRG-ROM bank /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => self.chr[usize::from(addr) & (Self::CHR_RAM_SIZE - 1)], 0x2000..=0x3EFF => ciram.peek(addr, self.mirroring), _ => 0, } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x8000..=0xFFFF => self.prg_rom[self.prg_rom_banks.translate(addr)], _ => 0, } } /// Write a byte to CHR-RAM/CIRAM at a given address. #[inline(always)] fn chr_write(&mut self, addr: u16, val: u8, ciram: &mut CIRam) { match addr { 0x0000..=0x1FFF if self.has_chr_ram => self.chr[usize::from(addr)] = val, 0x2000..=0x3EFF => ciram.write(addr, val, self.mirroring), _ => (), } } /// Write a byte to PRG-RAM at a given address. #[inline(always)] fn prg_write(&mut self, addr: u16, val: u8) { if let 0x8000..=0xFFFF = addr { self.prg_rom_banks.set(0, val.into()) } } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for Bnrom {} impl Clock for Bnrom {} impl Regional for Bnrom {} impl Sram for Bnrom {} ================================================ FILE: tetanes-core/src/mapper/m034_nina001.rs ================================================ //! `NINA-001` (Mapper 034). //! //! use crate::{ cart::Cart, common::{Clock, Regional, Reset, Sram}, mapper::{self, Map, Mapper}, mem::{Banks, Memory}, ppu::{CIRam, Mirroring}, }; use serde::{Deserialize, Serialize}; /// `NINA-001` (Mapper 034). #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Nina001 { pub chr_rom: Memory>, pub prg_rom: Memory>, pub prg_ram: Memory>, pub mirroring: Mirroring, pub chr_banks: Banks, pub prg_rom_banks: Banks, } impl Nina001 { const PRG_ROM_WINDOW: usize = 32 * 1024; const PRG_RAM_SIZE: usize = 8 * 1024; const CHR_ROM_WINDOW: usize = 4 * 1024; pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, ) -> Result { let prg_ram = Memory::with_ram_state(Self::PRG_RAM_SIZE, cart.ram_state); let chr_banks = Banks::new(0x0000, 0x1FFF, chr_rom.len(), Self::CHR_ROM_WINDOW)?; let prg_rom_banks = Banks::new(0x8000, 0xFFFF, prg_rom.len(), Self::PRG_ROM_WINDOW)?; let nina001 = Self { chr_rom, prg_rom, prg_ram, // hardwired to horizontal mirroring: Mirroring::Horizontal, chr_banks, prg_rom_banks, }; Ok(nina001.into()) } } impl Map for Nina001 { // PPU $0000..=$0FFF 4K switchable CHR ROM bank // PPU $1000..=$1FFF 4K switchable CHR ROM bank // CPU $8000..=$FFFF 32K switchable PRG ROM bank /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => self.chr_rom[self.chr_banks.translate(addr)], 0x2000..=0x3EFF => ciram.peek(addr, self.mirroring), _ => 0, } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x6000..=0x7FFF => self.prg_ram[usize::from(addr) & (Self::PRG_RAM_SIZE - 1)], 0x8000..=0xFFFF => self.prg_rom[self.prg_rom_banks.translate(addr)], _ => 0, } } /// Write a byte to CHR-RAM/CIRAM at a given address. #[inline(always)] fn chr_write(&mut self, addr: u16, val: u8, ciram: &mut CIRam) { match addr { 0x0000..=0x1FFF => self.chr_rom[self.chr_banks.translate(addr)] = val, 0x2000..=0x3EFF => ciram.write(addr, val, self.mirroring), _ => (), } } /// Write a byte to PRG-RAM at a given address. #[inline(always)] fn prg_write(&mut self, addr: u16, val: u8) { if let 0x6000..=0x7FFF = addr { match addr { 0x7FFD => self.prg_rom_banks.set(0, (val & 0x01).into()), 0x7FFE => self.chr_banks.set(0, (val & 0x0F).into()), 0x7FFF => self.chr_banks.set(1, (val & 0x0F).into()), _ => (), } self.prg_ram[usize::from(addr) & (Self::PRG_RAM_SIZE - 1)] = val; } } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for Nina001 {} impl Clock for Nina001 {} impl Regional for Nina001 {} impl Sram for Nina001 {} ================================================ FILE: tetanes-core/src/mapper/m066_gxrom.rs ================================================ //! `GxROM` (Mapper 066). //! //! use crate::{ cart::Cart, common::{Clock, Regional, Reset, Sram}, mapper::{self, Map, Mapper}, mem::{Banks, Memory}, ppu::{CIRam, Mirroring}, }; use serde::{Deserialize, Serialize}; /// `GxROM` (Mapper 066). #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Gxrom { pub chr_rom: Memory>, pub prg_rom: Memory>, pub mirroring: Mirroring, pub chr_banks: Banks, pub prg_rom_banks: Banks, } impl Gxrom { const PRG_ROM_WINDOW: usize = 32 * 1024; const CHR_WINDOW: usize = 8 * 1024; const CHR_BANK_MASK: u8 = 0x0F; // 0b1111 const PRG_BANK_MASK: u8 = 0x30; // 0b110000 pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, ) -> Result { let chr_banks = Banks::new(0x0000, 0x1FFF, chr_rom.len(), Self::CHR_WINDOW)?; let prg_rom_banks = Banks::new(0x8000, 0xFFFF, prg_rom.len(), Self::PRG_ROM_WINDOW)?; let gxrom = Self { chr_rom, prg_rom, mirroring: cart.mirroring(), chr_banks, prg_rom_banks, }; Ok(gxrom.into()) } } impl Map for Gxrom { // PPU $0000..=$1FFF 8K CHR-ROM Bank Switchable // CPU $8000..=$FFFF 32K PRG-ROM Bank Switchable /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => self.chr_rom[self.chr_banks.translate(addr)], 0x2000..=0x3EFF => ciram.peek(addr, self.mirroring), _ => 0, } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x8000..=0xFFFF => self.prg_rom[self.prg_rom_banks.translate(addr)], _ => 0, } } /// Write a byte to PRG-RAM at a given address. #[inline(always)] fn prg_write(&mut self, addr: u16, val: u8) { if let 0x8000..=0xFFFF = addr { self.chr_banks.set(0, (val & Self::CHR_BANK_MASK).into()); self.prg_rom_banks .set(0, ((val & Self::PRG_BANK_MASK) >> 4).into()); } } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for Gxrom {} impl Clock for Gxrom {} impl Regional for Gxrom {} impl Sram for Gxrom {} ================================================ FILE: tetanes-core/src/mapper/m069_sunsoft_fme7.rs ================================================ //! `Sunsoft FME7` (Mapper 069). //! //! use crate::{ apu::PULSE_TABLE, cart::Cart, common::{Clock, Regional, Reset, Sample, Sram}, mapper::{self, Map, Mapper}, mem::{Banks, Memory}, ppu::{CIRam, Mirroring}, }; use serde::{Deserialize, Serialize}; /// `Sunsoft FME7` registers. #[derive(Default, Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Regs { pub command: u8, pub parameter: u8, pub prg_ram_enabled: bool, pub irq_enabled: bool, pub irq_pending: bool, pub irq_counter_enabled: bool, pub irq_counter: u16, } /// `Sunsoft FME7` (Mapper 069). #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct SunsoftFme7 { pub chr_rom: Memory>, pub prg_rom: Memory>, pub prg_ram: Memory>, pub regs: Regs, pub mirroring: Mirroring, pub audio: Audio, pub chr_banks: Banks, pub prg_banks: Banks, pub prg_rom_banks: Banks, } impl SunsoftFme7 { const PRG_WINDOW: usize = 8 * 1024; const PRG_RAM_SIZE: usize = 32 * 1024; const CHR_WINDOW: usize = 1024; pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, ) -> Result { let prg_ram = Memory::with_ram_state(Self::PRG_RAM_SIZE, cart.ram_state); let chr_banks = Banks::new(0x0000, 0x1FFF, chr_rom.len(), Self::CHR_WINDOW)?; let prg_ram_banks = Banks::new(0x6000, 0x7FFF, prg_ram.len(), Self::PRG_WINDOW)?; let prg_rom_banks = Banks::new(0x8000, 0xFFFF, prg_rom.len(), Self::PRG_WINDOW)?; let mut sunsoft_fme7 = Self { chr_rom, prg_rom, prg_ram, regs: Regs::default(), mirroring: cart.mirroring(), audio: Audio::new(), chr_banks, prg_banks: prg_ram_banks, prg_rom_banks, }; sunsoft_fme7 .prg_rom_banks .set(3, sunsoft_fme7.prg_rom_banks.last()); Ok(sunsoft_fme7.into()) } } impl Map for SunsoftFme7 { // PPU $0000..=$03FF 1K CHR-ROM Bank 1 Switchable // PPU $0400..=$07FF 1K CHR-ROM Bank 2 Switchable // PPU $0800..=$0BFF 1K CHR-ROM Bank 3 Switchable // PPU $0C00..=$0FFF 1K CHR-ROM Bank 4 Switchable // PPU $1000..=$13FF 1K CHR-ROM Bank 5 Switchable // PPU $1400..=$17FF 1K CHR-ROM Bank 6 Switchable // PPU $1800..=$1BFF 1K CHR-ROM Bank 7 Switchable // PPU $1C00..=$1FFF 1K CHR-ROM Bank 8 Switchable // CPU $6000..=$7FFF 8K PRG-ROM or PRG-RAM Bank 1 Switchable // CPU $8000..=$9FFF 8K PRG-ROM Bank 1 Switchable // CPU $A000..=$BFFF 8K PRG-ROM Bank 2 Switchable // CPU $C000..=$DFFF 8K PRG-ROM Bank 3 Switchable // CPU $E000..=$FFFF 8K PRG-ROM Bank 4 fixed to last /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => self.chr_rom[self.chr_banks.translate(addr)], 0x2000..=0x3EFF => ciram.peek(addr, self.mirroring), _ => 0, } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x6000..=0x7FFF => { if self.regs.prg_ram_enabled { self.prg_ram[self.prg_banks.translate(addr)] } else { self.prg_rom[self.prg_banks.translate(addr)] } } 0x8000..=0xFFFF => self.prg_rom[self.prg_rom_banks.translate(addr)], _ => 0, } } /// Write a byte to PRG-RAM at a given address. fn prg_write(&mut self, addr: u16, val: u8) { match addr { 0x6000..=0x7FFF if self.regs.prg_ram_enabled => { self.prg_ram[self.prg_banks.translate(addr)] = val; } 0x8000..=0x9FFF => self.regs.command = val & 0x0F, 0xA000..=0xBFFF => match self.regs.command { 0..=7 => self.chr_banks.set(self.regs.command.into(), val.into()), 8 => { self.regs.parameter = val; self.regs.prg_ram_enabled = val & 0x80 == 0x80; self.prg_banks.set(0, (val & 0x3F).into()); } 9..=0xB => { let bank = self.regs.command - 9; self.prg_rom_banks.set(bank.into(), (val & 0x3F).into()); } 0xC => { self.mirroring = match val & 0x03 { 0b00 => Mirroring::Vertical, 0b01 => Mirroring::Horizontal, 0b10 => Mirroring::SingleScreenA, _ => Mirroring::SingleScreenB, } } 0xD => { self.regs.irq_enabled = (val & 0x01) == 0x01; self.regs.irq_counter_enabled = (val & 0x80) == 0x80; self.regs.irq_pending = false; } 0xE => self.regs.irq_counter = (self.regs.irq_counter & 0xFF00) | u16::from(val), 0xF => { self.regs.irq_counter = (self.regs.irq_counter & 0xFF) | (u16::from(val) << 8); } _ => (), }, 0xC000..=0xFFFF => self.audio.write_register(addr, val), _ => (), } } /// Whether an IRQ is pending acknowledgement. fn irq_pending(&self) -> bool { self.regs.irq_pending } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for SunsoftFme7 {} impl Clock for SunsoftFme7 { fn clock(&mut self) { if self.regs.irq_counter_enabled { self.regs.irq_counter = self.regs.irq_counter.wrapping_sub(1); if self.regs.irq_counter == 0xFFFF && self.regs.irq_enabled { self.regs.irq_pending = true; } } self.audio.clock(); } } impl Regional for SunsoftFme7 {} impl Sram for SunsoftFme7 {} impl Sample for SunsoftFme7 { fn output(&self) -> f32 { self.audio.output() } } #[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct Audio { clock_timer: u8, register: u8, registers: [u8; 16], volumes: [u8; 16], timers: [i16; 3], steps: [u8; 3], out: f32, } impl Default for Audio { fn default() -> Self { Self::new() } } impl Audio { pub fn new() -> Self { let mut audio = Self { clock_timer: 1, register: 0, registers: [0; 16], volumes: [0; 16], timers: [0; 3], steps: [0; 3], out: 0.0, }; let mut output = 1.0; for volume in audio.volumes.iter_mut().skip(1) { // +1.5dB 2x for every 1 step in volume output *= 1.188_502_227_437_018_5; output *= 1.188_502_227_437_018_5; *volume = output as u8; } audio } #[must_use] #[inline] pub fn output(&self) -> f32 { let pulse_scale = PULSE_TABLE[PULSE_TABLE.len() - 1] / 15.0; pulse_scale * self.out } #[must_use] #[inline] pub fn period(&self, channel: usize) -> u16 { let register = channel * 2; u16::from(self.registers[register]) | (u16::from(self.registers[register + 1]) << 8) } #[must_use] #[inline] pub fn envelope_period(&self) -> u16 { u16::from(self.registers[0x0B]) | (u16::from(self.registers[0x0C]) << 8) } #[must_use] #[inline] pub const fn noise_period(&self) -> u8 { self.registers[0x06] } #[must_use] #[inline] pub const fn volume(&self, channel: usize) -> u8 { self.volumes[(self.registers[channel + 8] & 0x0F) as usize] } #[must_use] #[inline] pub const fn envelope_enabled(&self, channel: usize) -> bool { self.registers[channel + 8] & 0x10 == 0x10 } #[must_use] #[inline] pub const fn square_enabled(&self, channel: usize) -> bool { (self.registers[0x07] >> channel) & 0x01 == 0x00 } #[must_use] #[inline] pub const fn noise_enabled(&self, channel: usize) -> bool { (self.registers[0x07] >> (channel + 3)) & 0x01 == 0x00 } const fn write_register(&mut self, addr: u16, val: u8) { match addr { 0xC000..=0xDFFF => self.register = val, 0xE000..=0xFFFF if self.register <= 0x0F => { self.registers[self.register as usize] = val; } _ => (), } } } impl Clock for Audio { fn clock(&mut self) { if self.clock_timer == 0 { self.clock_timer = 1; for channel in 0..3 { self.timers[channel] -= 1; if self.timers[channel] <= 0 { self.timers[channel] = self.period(channel) as i16; self.steps[channel] = (self.steps[channel] + 1) & 0x0F; } } self.out = [0, 1, 2] .into_iter() .filter(|&channel| self.square_enabled(channel) && self.steps[channel] < 0x08) .map(|channel| self.volume(channel) as f32) .sum(); } self.clock_timer -= 1; } } ================================================ FILE: tetanes-core/src/mapper/m071_bf909x.rs ================================================ //! `Bf909x` (Mapper 071). //! //! use crate::{ cart::Cart, common::{Clock, Regional, Reset, Sram}, mapper::{self, Map, Mapper}, mem::{Banks, Memory}, ppu::{CIRam, Mirroring}, }; use serde::{Deserialize, Serialize}; /// `Bf909x` revision. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[must_use] pub enum Revision { #[default] Bf909x, Bf9097, } /// `Bf909x` (Mapper 071). #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Bf909x { pub chr: Memory>, pub prg_rom: Memory>, pub has_chr_ram: bool, pub revision: Revision, pub mirroring: Mirroring, pub prg_rom_banks: Banks, } impl Bf909x { const PRG_ROM_WINDOW: usize = 16 * 1024; const CHR_RAM_SIZE: usize = 8 * 1024; const SINGLE_SCREEN_A: u8 = 0x10; // 0b10000 pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, ) -> Result { let (chr, has_chr_ram) = cart.chr_rom_or_ram(chr_rom, Self::CHR_RAM_SIZE); let prg_rom_banks = Banks::new(0x8000, 0xFFFF, prg_rom.len(), Self::PRG_ROM_WINDOW)?; let mut bf909x = Self { chr, prg_rom, has_chr_ram, revision: if cart.submapper_num() == 1 { Revision::Bf9097 } else { Revision::Bf909x }, mirroring: cart.mirroring(), prg_rom_banks, }; bf909x.prg_rom_banks.set(1, bf909x.prg_rom_banks.last()); Ok(bf909x.into()) } pub const fn set_revision(&mut self, rev: Revision) { self.revision = rev; } } impl Map for Bf909x { // PPU $0000..=$1FFF 8K Fixed CHR-ROM Banks // CPU $8000..=$BFFF 16K PRG-ROM Bank Switchable // CPU $C000..=$FFFF 16K PRG-ROM Fixed to Last Bank /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => self.chr[usize::from(addr)], 0x2000..=0x3EFF => ciram.peek(addr, self.mirroring), _ => 0, } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x8000..=0xFFFF => self.prg_rom[self.prg_rom_banks.translate(addr)], _ => 0, } } /// Write a byte to CHR-RAM/CIRAM at a given address. #[inline(always)] fn chr_write(&mut self, addr: u16, val: u8, ciram: &mut CIRam) { match addr { 0x0000..=0x1FFF if self.has_chr_ram => self.chr[usize::from(addr)] = val, 0x2000..=0x3EFF => ciram.write(addr, val, self.mirroring), _ => (), } } /// Write a byte to PRG-RAM at a given address. #[inline(always)] fn prg_write(&mut self, addr: u16, val: u8) { if let 0x8000..=0xFFFF = addr { // Firehawk uses $9000 to change mirroring if addr == 0x9000 { self.revision = Revision::Bf9097; } if addr >= 0xC000 || self.revision != Revision::Bf9097 { self.prg_rom_banks.set(0, val.into()); } else { self.mirroring = if val & Self::SINGLE_SCREEN_A == Self::SINGLE_SCREEN_A { Mirroring::SingleScreenA } else { Mirroring::SingleScreenB }; } } } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for Bf909x {} impl Clock for Bf909x {} impl Regional for Bf909x {} impl Sram for Bf909x {} ================================================ FILE: tetanes-core/src/mapper/m079_nina003_006.rs ================================================ //! `NINA-003`/`NINA-006` (Mapper 079). //! //! use crate::{ cart::Cart, common::{Clock, Regional, Reset, Sram}, mapper::{self, Map, Mapper}, mem::{Banks, Memory}, ppu::{CIRam, Mirroring}, }; use serde::{Deserialize, Serialize}; /// `NINA-003`/`NINA-006` (Mapper 079). #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub struct Nina003006 { pub chr_rom: Memory>, pub prg_rom: Memory>, pub mirroring: Mirroring, pub mapper_num: u16, pub chr_banks: Banks, pub prg_rom_banks: Banks, } impl Nina003006 { const PRG_ROM_WINDOW: usize = 32 * 1024; const CHR_ROM_WINDOW: usize = 8 * 1024; pub fn load( cart: &Cart, chr_rom: Memory>, prg_rom: Memory>, ) -> Result { let chr_banks = Banks::new(0x0000, 0x1FFF, chr_rom.len(), Self::CHR_ROM_WINDOW)?; let prg_rom_banks = Banks::new(0x8000, 0xFFFF, prg_rom.len(), Self::PRG_ROM_WINDOW)?; let nina003006 = Self { chr_rom, prg_rom, mirroring: cart.mirroring(), mapper_num: cart.mapper_num(), chr_banks, prg_rom_banks, }; Ok(nina003006.into()) } } impl Map for Nina003006 { // PPU $0000..=$1FFF 8K switchable CHR ROM bank // CPU $8000..=$FFFF 32K switchable PRG ROM bank /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x0000..=0x1FFF => self.chr_rom[self.chr_banks.translate(addr)], 0x2000..=0x3EFF => ciram.peek(addr, self.mirroring), _ => 0, } } /// Peek a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { match addr { 0x8000..=0xFFFF => self.prg_rom[self.prg_rom_banks.translate(addr)], _ => 0, } } /// Write a byte to PRG-RAM at a given address. #[inline(always)] fn prg_write(&mut self, addr: u16, val: u8) { if (addr & 0xE100) == 0x4100 { if self.mapper_num == 113 { self.prg_rom_banks.set(0, ((val >> 3) & 0x07).into()); self.chr_banks .set(0, ((val & 0x07) | ((val >> 3) & 0x08)).into()); self.mirroring = if val & 0x80 == 0x80 { Mirroring::Vertical } else { Mirroring::Horizontal }; } else { self.prg_rom_banks.set(0, ((val >> 3) & 0x01).into()); self.chr_banks.set(0, (val & 0x07).into()); } } } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { self.mirroring } } impl Reset for Nina003006 {} impl Clock for Nina003006 {} impl Regional for Nina003006 {} impl Sram for Nina003006 {} ================================================ FILE: tetanes-core/src/mapper/vrc_irq.rs ================================================ //! `VrcIrq` //! //! use crate::common::{Clock, Reset, ResetKind}; use serde::{Deserialize, Serialize}; #[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct VrcIrq { pub reload: u8, pub counter: u8, pub prescalar_counter: i16, pub irq_pending: bool, pub enabled: bool, pub enabled_after_ack: bool, pub cycle_mode: bool, } impl VrcIrq { pub const fn write_reload(&mut self, val: u8) { self.reload = val; } pub const fn write_control(&mut self, val: u8) { self.enabled_after_ack = val & 0x01 == 0x01; self.enabled = val & 0x02 == 0x02; self.cycle_mode = val & 0x04 == 0x04; if self.enabled { self.counter = self.reload; self.prescalar_counter = 341; } self.irq_pending = false; } pub const fn acknowledge(&mut self) { self.enabled = self.enabled_after_ack; self.irq_pending = false; } } impl Clock for VrcIrq { #[inline] fn clock(&mut self) { if self.enabled { self.prescalar_counter -= 3; if self.cycle_mode || self.prescalar_counter <= 0 { if self.counter == 0xFF { self.counter = self.reload; self.irq_pending = true; } else { self.counter += 1; } self.prescalar_counter += 341; } } } } impl Reset for VrcIrq { fn reset(&mut self, _kind: ResetKind) { self.reload = 0; self.counter = 0; self.prescalar_counter = 0; self.enabled = false; self.enabled_after_ack = false; self.cycle_mode = false; } } ================================================ FILE: tetanes-core/src/mapper.rs ================================================ //! Memory Mappers for cartridges. //! //! use crate::{ common::{Clock, NesRegion, Regional, Reset, ResetKind, Sample, Sram}, fs, mem, ppu::{CIRam, Mirroring}, }; use serde::{Deserialize, Serialize}; use std::path::Path; pub use bandai_fcg::BandaiFCG; // m016, m153, m157, m159 pub use m000_nrom::Nrom; pub use m001_sxrom::{Revision as Mmc1Revision, Sxrom}; pub use m002_uxrom::Uxrom; pub use m003_cnrom::Cnrom; pub use m004_txrom::{Revision as Mmc3Revision, Txrom}; pub use m005_exrom::Exrom; pub use m007_axrom::Axrom; pub use m009_pxrom::Pxrom; pub use m010_fxrom::Fxrom; pub use m011_color_dreams::ColorDreams; pub use m018_jalecoss88006::JalecoSs88006; pub use m019_namco163::Namco163; pub use m024_m026_vrc6::Vrc6; pub use m034_bnrom::Bnrom; pub use m034_nina001::Nina001; pub use m066_gxrom::Gxrom; pub use m069_sunsoft_fme7::SunsoftFme7; pub use m071_bf909x::{Bf909x, Revision as Bf909Revision}; pub use m079_nina003_006::Nina003006; pub mod bandai_fcg; pub mod m000_nrom; pub mod m001_sxrom; pub mod m002_uxrom; pub mod m003_cnrom; pub mod m004_txrom; pub mod m005_exrom; pub mod m007_axrom; pub mod m009_pxrom; pub mod m010_fxrom; pub mod m011_color_dreams; pub mod m018_jalecoss88006; pub mod m019_namco163; pub mod m024_m026_vrc6; pub mod m034_bnrom; pub mod m034_nina001; pub mod m066_gxrom; pub mod m069_sunsoft_fme7; pub mod m071_bf909x; pub mod m079_nina003_006; pub mod vrc_irq; /// Errors that mappers can return. #[derive(thiserror::Error, Debug)] #[must_use] pub enum Error { /// A mapper banking error. #[error(transparent)] Bank(#[from] mem::Error), } /// Allow user-controlled mapper revision for mappers that are difficult to auto-detect correctly. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[must_use] pub enum MapperRevision { // Mmc1 and Vrc6 should be properly detected by the mapper number /// No known detection except DB lookup Mmc3(Mmc3Revision), /// Can compare to submapper 1, if header is correct Bf909(Bf909Revision), } impl std::fmt::Display for MapperRevision { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = match self { Self::Mmc3(rev) => match rev { Mmc3Revision::A => "MMC3A", Mmc3Revision::BC => "MMC3B/C", Mmc3Revision::Acc => "MMC3Acc", }, Self::Bf909(rev) => match rev { Bf909Revision::Bf909x => "BF909x", Bf909Revision::Bf9097 => "BF9097", }, }; write!(f, "{s}") } } /// A `Mapper` is a specific cart variant with dedicated memory mapping logic for memory addressing and /// bank switching. #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] pub enum Mapper { None(()), /// `NROM` (Mapper 000) Nrom(Nrom), /// `SxROM`/`MMC1` (Mapper 001) Sxrom(Sxrom), /// `UxROM` (Mapper 002) Uxrom(Uxrom), /// `CNROM` (Mapper 003) Cnrom(Cnrom), /// `TxROM`/`MMC3` (Mappers 004, 088, 095, 206) Txrom(Txrom), /// `ExROM`/`MMC5` (Mapper 5) Exrom(Box), /// `AxROM` (Mapper 007) Axrom(Axrom), /// `PxROM`/`MMC2` (Mapper 009) Pxrom(Pxrom), /// `FxROM`/`MMC4` (Mapper 010) Fxrom(Fxrom), /// `Color Dreams` (Mapper 011) ColorDreams(ColorDreams), /// `Bandai FCG` (Mappers 016, 153, 157, and 159) BandaiFCG(Box), /// `Jaleco SS88006` (Mapper 018) JalecoSs88006(JalecoSs88006), /// `Namco163` (Mapper 019) Namco163(Box), /// `VRC6` (Mapper 024). Vrc6(Box), /// `BNROM` (Mapper 034). Bnrom(Bnrom), /// `NINA-001` (Mapper 034). Nina001(Nina001), /// `GxROM` (Mapper 066). Gxrom(Gxrom), /// `Sunsoft FME7` (Mapper 069). SunsoftFme7(SunsoftFme7), /// `Bf909x` (Mapper 071). Bf909x(Bf909x), /// `NINA-003`/`NINA-006` (Mapper 079). Nina003006(Nina003006), } /// Implement `From` for `Mapper`. macro_rules! impl_from_board { (@impl $variant:ident, $board:ident) => { impl From<$board> for Mapper { fn from(board: $board) -> Self { Self::$variant(board) } } }; (@impl $variant:ident, Box<$board:ident>) => { impl From<$board> for Mapper { fn from(board: $board) -> Self { Self::$variant(Box::new(board)) } } impl From> for Mapper { fn from(board: Box<$board>) -> Self { Self::$variant(board) } } }; ($($variant:ident($($tt:tt)+)),+ $(,)?) => { $(impl_from_board!(@impl $variant, $($tt)+);)+ }; } impl_from_board!( Nrom(Nrom), Sxrom(Sxrom), Uxrom(Uxrom), Cnrom(Cnrom), Txrom(Txrom), Exrom(Box), Axrom(Axrom), Pxrom(Pxrom), Fxrom(Fxrom), ColorDreams(ColorDreams), BandaiFCG(Box), JalecoSs88006(JalecoSs88006), Namco163(Box), Vrc6(Box), Bnrom(Bnrom), Nina001(Nina001), Gxrom(Gxrom), SunsoftFme7(SunsoftFme7), Bf909x(Bf909x), Nina003006(Nina003006), ); /// Implement `Map` function for all `Mapper` variants. macro_rules! impl_map { ($self:expr, $fn:ident$(,)? $($args:expr),*$(,)?) => { match $self { Mapper::None(m) => m.$fn($($args),*), Mapper::Nrom(m) => m.$fn($($args),*), Mapper::Sxrom(m) => m.$fn($($args),*), Mapper::Uxrom(m) => m.$fn($($args),*), Mapper::Cnrom(m) => m.$fn($($args),*), Mapper::Txrom(m) => m.$fn($($args),*), Mapper::Exrom(m) => m.$fn($($args),*), Mapper::Axrom(m) => m.$fn($($args),*), Mapper::Pxrom(m) => m.$fn($($args),*), Mapper::Fxrom(m) => m.$fn($($args),*), Mapper::ColorDreams(m) => m.$fn($($args),*), Mapper::BandaiFCG(m) => m.$fn($($args),*), Mapper::JalecoSs88006(m) => m.$fn($($args),*), Mapper::Namco163(m) => m.$fn($($args),*), Mapper::Vrc6(m) => m.$fn($($args),*), Mapper::Bnrom(m) => m.$fn($($args),*), Mapper::Nina001(m) => m.$fn($($args),*), Mapper::Gxrom(m) => m.$fn($($args),*), Mapper::SunsoftFme7(m) => m.$fn($($args),*), Mapper::Bf909x(m) => m.$fn($($args),*), Mapper::Nina003006(m) => m.$fn($($args),*), } }; } impl Map for Mapper { /// Read a byte from CHR-ROM/RAM/CIRAM at a given address. #[inline(always)] fn chr_read(&mut self, addr: u16, ciram: &CIRam) -> u8 { impl_map!(self, chr_read, addr, ciram) } /// Peek a byte from CHR-ROM/RAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { impl_map!(self, chr_peek, addr, ciram) } /// Read a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_read(&mut self, addr: u16) -> u8 { impl_map!(self, prg_read, addr) } /// Read a byte from PRG-ROM/RAM at a given address. #[inline(always)] fn prg_peek(&self, addr: u16) -> u8 { impl_map!(self, prg_peek, addr) } /// Write a byte to CHR-RAM/CIRAM at a given address. #[inline(always)] fn chr_write(&mut self, addr: u16, val: u8, ciram: &mut CIRam) { impl_map!(self, chr_write, addr, val, ciram) } /// Write a byte to PRG-RAM at a given address. #[inline(always)] fn prg_write(&mut self, addr: u16, val: u8) { impl_map!(self, prg_write, addr, val) } /// Synchronize a read from a PPU address. fn ppu_read(&mut self, addr: u16) { impl_map!(self, ppu_read, addr) } /// Synchronize a write to a PPU address. fn ppu_write(&mut self, addr: u16, val: u8) { impl_map!(self, ppu_write, addr, val) } /// Whether an IRQ is pending acknowledgement. fn irq_pending(&self) -> bool { impl_map!(self, irq_pending) } /// Whether an DMA is pending acknowledgement. fn dma_pending(&self) -> bool { impl_map!(self, dma_pending) } /// Clear pending DMA. fn clear_dma_pending(&mut self) { impl_map!(self, clear_dma_pending) } /// Returns the current [`Mirroring`] mode. #[inline(always)] fn mirroring(&self) -> Mirroring { impl_map!(self, mirroring) } } impl Sample for Mapper { /// Output a single audio sample. #[inline] fn output(&self) -> f32 { match self { Self::Exrom(exrom) => exrom.output(), Self::Namco163(namco163) => namco163.output(), Self::Vrc6(vrc6) => vrc6.output(), Self::SunsoftFme7(sunsoft_fme7) => sunsoft_fme7.output(), _ => 0.0, } } } impl Reset for Mapper { /// Reset the component given the [`ResetKind`]. fn reset(&mut self, kind: ResetKind) { impl_map!(self, reset, kind) } } impl Clock for Mapper { /// Clock component once. #[inline] fn clock(&mut self) { impl_map!(self, clock) } } impl Regional for Mapper { /// Return the current region. fn region(&self) -> NesRegion { impl_map!(self, region) } /// Set the region. fn set_region(&mut self, region: NesRegion) { impl_map!(self, set_region, region) } } impl Sram for Mapper { /// Save RAM to a given path. fn save(&self, path: impl AsRef) -> fs::Result<()> { impl_map!(self, save, path) } /// Load save RAM from a given path. fn load(&mut self, path: impl AsRef) -> fs::Result<()> { impl_map!(self, load, path) } } impl Mapper { /// An empty Mapper. pub const fn none() -> Self { Self::None(()) } /// Whether mapper is `None`. pub const fn is_none(&self) -> bool { matches!(self, Self::None(_)) } } impl Default for Mapper { fn default() -> Self { Self::none() } } /// Trait implemented for all [`Mapper`]s. pub trait Map: Clock + Regional + Reset + Sram { /// Read a byte from CHR-ROM/RAM/CIRAM at a given address. #[inline(always)] fn chr_read(&mut self, addr: u16, ciram: &CIRam) -> u8 { self.chr_peek(addr, ciram) } /// Peek a byte from CHR-ROM/RAM at a given address. // `chr_peek` has to be implemented at read from CHR and CIRam. fn chr_peek(&self, _addr: u16, _ciram: &CIRam) -> u8; /// Read a byte from PRG-ROM/RAM at a given address. /// /// Defaults to `prg_peek`. #[inline(always)] fn prg_read(&mut self, addr: u16) -> u8 { self.prg_peek(addr) } /// Peek a byte from PRG-ROM/RAM at a given address. // `prg_peek` has to be implemented to read PRG-ROM. fn prg_peek(&self, _addr: u16) -> u8; /// Write a byte to CHR-RAM/CIRAM at a given address. // `chr_write` has to be implemented at least to write to CIRam. #[inline(always)] fn chr_write(&mut self, addr: u16, val: u8, ciram: &mut CIRam) { if let 0x2000..=0x3EFF = addr { ciram.write(addr, val, self.mirroring()); } } /// Write a byte to PRG-RAM at a given address. fn prg_write(&mut self, _addr: u16, _val: u8) {} /// Synchronize a read from a PPU address. fn ppu_read(&mut self, _addr: u16) {} /// Synchronize a write to a PPU address. fn ppu_write(&mut self, _addr: u16, _val: u8) {} /// Whether an IRQ is pending acknowledgement. fn irq_pending(&self) -> bool { false } /// Clear pending DMA. fn clear_dma_pending(&mut self) {} /// Whether an DMA is pending acknowledgement. fn dma_pending(&self) -> bool { false } /// Returns the current [`Mirroring`] mode. // All mappers have mirroring, even if it's hard-wired. fn mirroring(&self) -> Mirroring; } impl Map for () { fn chr_peek(&self, addr: u16, ciram: &CIRam) -> u8 { match addr { 0x2000..=0x3EFF => ciram.peek(addr, self.mirroring()), _ => 0, } } fn prg_peek(&self, _addr: u16) -> u8 { 0 } fn mirroring(&self) -> Mirroring { Mirroring::default() } } impl Sample for () {} impl Reset for () {} impl Clock for () {} impl Regional for () {} impl Sram for () {} ================================================ FILE: tetanes-core/src/mem.rs ================================================ //! Memory and Bankswitching implementations. use rand::Rng; use serde::{ Deserialize, Deserializer, Serialize, Serializer, de::{SeqAccess, Visitor}, ser::SerializeTuple, }; use std::{ fmt, marker::PhantomData, num::NonZeroUsize, ops::{Deref, DerefMut, Index, IndexMut, Range, RangeInclusive}, str::FromStr, }; use tracing::warn; /// Represents ROM or RAM memory in bytes, with a custom Debug implementation that avoids /// printing the entire contents. #[derive(Default, Copy, Clone, Serialize, Deserialize)] pub struct Memory { data: D, } impl Memory> { /// Create an empty `Memory` instance. pub fn empty() -> Self { Self { data: Vec::new().into_boxed_slice(), } } /// Create a default `Memory` instance. pub fn new(mut size: usize) -> Self { if size > 0 && !size.is_power_of_two() { warn!("memory size {size} must be a power of two"); size = size.next_power_of_two(); } Self { data: vec![0; size].into_boxed_slice(), } } pub fn with_ram_state(size: usize, state: RamState) -> Self { let mut mem = Self::new(size); state.fill(&mut mem.data); mem } /// Shortens `Memory` by keeping the first `size` bytes and dropping the rest. pub fn truncate(&mut self, size: usize) { let mut data = std::mem::take(&mut self.data).to_vec(); data.truncate(size); self.data = data.into_boxed_slice(); } } impl Memory> { /// Create a default ROM `Memory` instance. pub fn new_const() -> Self where T: Default + Copy, { Self::default() } } impl Memory> { /// Fill memory based on [`RamState`]. pub fn with_ram_state_const(state: RamState) -> Self { let mut mem = Self::default(); state.fill(&mut *mem.data); mem } } impl fmt::Debug for Memory> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Memory") .field("len", &self.data.len()) .finish() } } impl fmt::Debug for Memory> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Memory") .field("len", &self.data.len()) .finish() } } impl Deref for Memory { type Target = D; fn deref(&self) -> &Self::Target { &self.data } } impl DerefMut for Memory { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.data } } impl> AsRef<[T]> for Memory { fn as_ref(&self) -> &[T] { self.data.as_ref() } } impl> AsMut<[T]> for Memory { fn as_mut(&mut self) -> &mut [T] { self.data.as_mut() } } impl Index for Memory> { type Output = T; #[inline(always)] fn index(&self, index: usize) -> &Self::Output { self.data.index(index & (self.data.len() - 1)) } } impl IndexMut for Memory> { #[inline(always)] fn index_mut(&mut self, index: usize) -> &mut Self::Output { self.data.index_mut(index & (self.data.len() - 1)) } } impl Index> for Memory> { type Output = [T]; #[inline] fn index(&self, range: Range) -> &Self::Output { self.data .index((range.start & (self.data.len() - 1))..range.end.min(self.len())) } } impl IndexMut> for Memory> { #[inline] fn index_mut(&mut self, range: Range) -> &mut Self::Output { self.data .index_mut((range.start & (self.data.len() - 1))..range.end.min(self.len())) } } impl Index> for Memory> { type Output = [T]; #[inline] fn index(&self, range: RangeInclusive) -> &Self::Output { self.data.index( (range.start() & (self.data.len() - 1))..=*range.end().min(&(self.data.len() - 1)), ) } } impl IndexMut> for Memory> { #[inline] fn index_mut(&mut self, range: RangeInclusive) -> &mut Self::Output { self.data.index_mut( (range.start() & (self.data.len() - 1))..=*range.end().min(&(self.data.len() - 1)), ) } } #[repr(transparent)] #[derive(Copy, Clone)] pub struct ConstArray { data: [T; N], } impl ConstArray { /// Create a new `ConstSlice` instance. pub fn new() -> Self where T: Default + Copy, { Self::default() } /// Create a new `ConstSlice` instance filled with `val`. pub const fn filled(val: T) -> Self where T: Copy, { Self { data: [val; N] } } } impl fmt::Debug for ConstArray { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ConstArray") .field("len", &self.data.len()) .finish() } } impl Default for ConstArray { fn default() -> Self { Self { data: [T::default(); N], } } } impl From<[T; N]> for ConstArray { fn from(data: [T; N]) -> Self { Self { data } } } impl Deref for ConstArray { type Target = [T; N]; #[inline] fn deref(&self) -> &Self::Target { &self.data } } impl DerefMut for ConstArray { #[inline] fn deref_mut(&mut self) -> &mut Self::Target { &mut self.data } } impl AsRef<[T]> for ConstArray { #[inline] fn as_ref(&self) -> &[T] { self.data.as_ref() } } impl AsMut<[T]> for ConstArray { #[inline] fn as_mut(&mut self) -> &mut [T] { self.data.as_mut() } } impl Index for ConstArray { type Output = T; #[inline] fn index(&self, index: usize) -> &Self::Output { self.data.index(index & (N - 1)) } } impl IndexMut for ConstArray { #[inline] fn index_mut(&mut self, index: usize) -> &mut Self::Output { self.data.index_mut(index & (N - 1)) } } impl Index> for ConstArray { type Output = [T]; #[inline] fn index(&self, range: Range) -> &Self::Output { self.data.index(range.start & (N - 1)..range.end.min(N)) } } impl IndexMut> for ConstArray { #[inline] fn index_mut(&mut self, range: Range) -> &mut Self::Output { self.data.index_mut(range.start & (N - 1)..range.end.min(N)) } } impl Index> for ConstArray { type Output = [T]; #[inline] fn index(&self, range: RangeInclusive) -> &Self::Output { self.data .index(range.start() & (N - 1)..=*range.end().min(&(N - 1))) } } impl IndexMut> for ConstArray { #[inline] fn index_mut(&mut self, range: RangeInclusive) -> &mut Self::Output { self.data .index_mut(range.start() & (N - 1)..=*range.end().min(&(N - 1))) } } impl Serialize for ConstArray { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut s = serializer.serialize_tuple(N)?; for item in &self.data { s.serialize_element(item)?; } s.end() } } impl<'de, T, const N: usize> Deserialize<'de> for ConstArray where T: Deserialize<'de> + Default + Copy, { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct ArrayVisitor(PhantomData); impl<'de, T, const N: usize> Visitor<'de> for ArrayVisitor where T: Deserialize<'de> + Default + Copy, { type Value = [T; N]; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter.write_str(&format!("an array of length {N}")) } #[inline] fn visit_seq(self, mut seq: A) -> Result where A: SeqAccess<'de>, { let mut data = [T::default(); N]; for data in &mut data { match (seq.next_element())? { Some(val) => *data = val, None => return Err(serde::de::Error::invalid_length(N, &self)), } } Ok(data) } } deserializer .deserialize_tuple(N, ArrayVisitor(PhantomData)) .map(|data| Self { data }) } } /// A trait that represents memory read operations. Reads typically have side-effects. pub trait Read { /// Read from the given address. #[inline(always)] fn read(&mut self, addr: u16) -> u8 { self.peek(addr) } /// Peek from the given address. fn peek(&self, addr: u16) -> u8; } /// A trait that represents memory write operations. pub trait Write { /// Write value to the given address. fn write(&mut self, addr: u16, val: u8); } /// RAM in a given state on startup. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[must_use] pub enum RamState { #[default] AllZeros, AllOnes, Random, } impl RamState { /// Return `RamState` options as a slice. pub const fn as_slice() -> &'static [Self] { &[Self::AllZeros, Self::AllOnes, Self::Random] } /// Return `RamState` as a `str`. #[must_use] pub const fn as_str(&self) -> &'static str { match self { Self::AllZeros => "all-zeros", Self::AllOnes => "all-ones", Self::Random => "random", } } /// Fills data slice based on `RamState`. pub fn fill(&self, data: &mut [u8]) { match self { RamState::AllZeros => data.fill(0x00), RamState::AllOnes => data.fill(0xFF), RamState::Random => { rand::rng().fill_bytes(data); } } } } impl From for RamState { fn from(value: usize) -> Self { match value { 0 => Self::AllZeros, 1 => Self::AllOnes, _ => Self::Random, } } } impl AsRef for RamState { fn as_ref(&self) -> &str { self.as_str() } } impl std::fmt::Display for RamState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = match self { Self::AllZeros => "All $00", Self::AllOnes => "All $FF", Self::Random => "Random", }; write!(f, "{s}") } } impl FromStr for RamState { type Err = &'static str; fn from_str(s: &str) -> Result { match s { "all-zeros" => Ok(Self::AllZeros), "all-ones" => Ok(Self::AllOnes), "random" => Ok(Self::Random), _ => Err("invalid RamState value. valid options: `all-zeros`, `all-ones`, or `random`"), } } } /// Represents allowed memory bank access. #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[must_use] pub enum BankAccess { None, Read, ReadWrite, } /// Represents a set of memory banks. #[derive(Clone, Serialize, Deserialize)] #[must_use] pub struct Banks { size: NonZeroUsize, window: NonZeroUsize, shift: usize, page_mask: usize, bank_mask: usize, banks: Box<[usize]>, access: Box<[BankAccess]>, page_count: usize, } #[derive(thiserror::Error, Debug)] #[must_use] pub enum Error { #[error("Memory Bank `window` must a non-zero power of two")] InvalidWindow, #[error("Memory Bank `size` must be non-zero")] InvalidSize, #[error("Memory Bank `capacity` must be non-zero")] InvalidCapacity, } impl Banks { pub fn new( start: usize, end: usize, capacity: usize, window: impl TryInto, ) -> Result { let window = window.try_into().map_err(|_| Error::InvalidWindow)?; if !window.is_power_of_two() { return Err(Error::InvalidWindow); } let size = NonZeroUsize::try_from(end - start).map_err(|_| Error::InvalidSize)?; if capacity == 0 { return Err(Error::InvalidCapacity); } let bank_count = (size.get() + 1) / window; let page_count = capacity / window; let mut banks = vec![0; bank_count].into_boxed_slice(); let access = vec![BankAccess::ReadWrite; bank_count].into_boxed_slice(); for (i, bank) in banks.iter_mut().enumerate() { *bank = (i * window.get()) % capacity; } Ok(Self { size, window, shift: window.trailing_zeros() as usize, page_mask: page_count.saturating_sub(1), bank_mask: bank_count.saturating_sub(1), banks, access, page_count, }) } #[inline(always)] pub fn set(&mut self, bank: usize, page: usize) { let bank = bank & self.bank_mask; self.banks[bank] = (page & self.page_mask) << self.shift; debug_assert!( self.banks[bank] <= self.page_count * self.window.get(), "memory page {} is out of bounds (max: {})", self.banks[bank], self.page_count * self.window.get() ); } #[inline(always)] pub fn set_range(&mut self, start: usize, end: usize, page: usize) { let mut new_addr = (page & self.page_mask) << self.shift; debug_assert!( end < self.banks.len(), "end memory bank {end} is out of bounds (max {})", self.banks.len() ); for bank in start..=end { self.banks[bank] = new_addr; debug_assert!( self.banks[bank] <= self.page_count * self.window.get(), "memory page {} is out of bounds (max: {})", self.banks[bank], self.page_count * self.window.get() ); new_addr += self.window.get(); } } #[inline(always)] pub fn set_access(&mut self, bank: usize, access: BankAccess) { self.access[bank & self.bank_mask] = access; } #[inline(always)] pub fn set_access_range(&mut self, start: usize, end: usize, access: BankAccess) { for slot in start..=end { self.set_access(slot, access); } } #[inline(always)] pub const fn readable(&self, addr: u16) -> bool { matches!( self.access[self.get(addr) & self.bank_mask], BankAccess::Read | BankAccess::ReadWrite ) } #[inline(always)] pub const fn writable(&self, addr: u16) -> bool { matches!( self.access[self.get(addr) & self.bank_mask], BankAccess::ReadWrite ) } #[inline(always)] #[must_use] pub const fn last(&self) -> usize { self.page_count.saturating_sub(1) } #[inline(always)] #[must_use] pub const fn banks_len(&self) -> usize { self.banks.len() } #[inline(always)] #[must_use] pub const fn get(&self, addr: u16) -> usize { (addr as usize & self.size.get()) >> self.shift } #[inline(always)] #[must_use] pub const fn translate(&self, addr: u16) -> usize { (self.banks[self.get(addr) & self.bank_mask]) | (addr as usize) & (self.window.get() - 1) } #[inline(always)] #[must_use] pub const fn page(&self, bank: usize) -> usize { self.banks[bank] >> self.shift } #[inline(always)] #[must_use] pub const fn page_offset(&self, bank: usize) -> usize { self.banks[bank] } #[inline(always)] #[must_use] pub const fn page_count(&self) -> usize { self.page_count } } impl std::fmt::Debug for Banks { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("Bank") .field("size", &format_args!("${:04X}", self.size)) .field("window", &format_args!("${:04X}", self.window)) .field("shift", &self.shift) .field("mask", &self.shift) .field("banks", &self.banks) .field("page_count", &self.page_count) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn get_bank() { let banks = Banks::new( 0x8000, 0xFFFF, 128 * 1024, NonZeroUsize::new(0x4000).unwrap(), ) .unwrap(); assert_eq!(banks.get(0x8000), 0); assert_eq!(banks.get(0x9FFF), 0); assert_eq!(banks.get(0xA000), 0); assert_eq!(banks.get(0xBFFF), 0); assert_eq!(banks.get(0xC000), 1); assert_eq!(banks.get(0xDFFF), 1); assert_eq!(banks.get(0xE000), 1); assert_eq!(banks.get(0xFFFF), 1); } #[test] fn bank_translate() { let mut banks = Banks::new( 0x8000, 0xFFFF, 128 * 1024, NonZeroUsize::new(0x2000).unwrap(), ) .unwrap(); let last_bank = banks.last(); assert_eq!(last_bank, 15, "bank count"); assert_eq!(banks.translate(0x8000), 0x0000); banks.set(0, 1); assert_eq!(banks.translate(0x8000), 0x2000); banks.set(0, 2); assert_eq!(banks.translate(0x8000), 0x4000); banks.set(0, 0); assert_eq!(banks.translate(0x8000), 0x0000); banks.set(0, banks.last()); assert_eq!(banks.translate(0x8000), 0x1E000); } } ================================================ FILE: tetanes-core/src/ppu/ctrl.rs ================================================ //! PPUCTRL register implementation. //! //! See: use crate::common::{Reset, ResetKind}; use bitflags::bitflags; use serde::{Deserialize, Serialize}; /// PPUCTRL register. /// /// See: #[derive(Default, Serialize, Deserialize, Debug, Copy, Clone)] #[must_use] pub struct Ctrl { pub bg_select: u16, pub spr_select: u16, pub spr_height: u16, pub vram_increment: bool, pub master_slave: u8, pub nmi_enabled: bool, pub bits: Bits, } bitflags! { // $2000 PPUCTRL // // https://wiki.nesdev.org/w/index.php/PPU_registers#PPUCTRL // VPHB SINN // |||| ||++- Nametable Select: 0b00 = $2000 (upper-left); 0b01 = $2400 (upper-right); // |||| || 0b10 = $2800 (lower-left); 0b11 = $2C00 (lower-right) // |||| |||+- Also For PPUSCROLL: 1 = Add 256 to X scroll // |||| ||+-- Also For PPUSCROLL: 1 = Add 240 to Y scroll // |||| |+--- VRAM Increment Mode: 0 = add 1, going across; 1 = add 32, going down // |||| +---- Sprite Pattern Select for 8x8: 0 = $0000, 1 = $1000, ignored in 8x16 mode // |||+------ Background Pattern Select: 0 = $0000, 1 = $1000 // ||+------- Sprite Height: 0 = 8x8, 1 = 8x16 // |+-------- PPU Master/Slave: 0 = read from EXT, 1 = write to EXT // +--------- NMI Enable: NMI at next vblank: 0 = off, 1: on #[derive(Default, Serialize, Deserialize, Debug, Copy, Clone)] #[must_use] pub struct Bits: u8 { const NAMETABLE1 = 0x01; const NAMETABLE2 = 0x02; const VRAM_INCREMENT = 0x04; const SPR_SELECT = 0x08; const BG_SELECT = 0x10; const SPR_HEIGHT = 0x20; const MASTER_SLAVE = 0x40; const NMI_ENABLE = 0x80; } } impl Ctrl { pub fn new() -> Self { let mut ctrl = Self::default(); ctrl.write(0); ctrl } pub const fn write(&mut self, val: u8) { self.bits = Bits::from_bits_truncate(val); // 0x1000 or 0x0000 self.spr_select = self.bits.contains(Bits::SPR_SELECT) as u16 * 0x1000; // 0x1000 or 0x0000 self.bg_select = self.bits.contains(Bits::BG_SELECT) as u16 * 0x1000; // 16 or 8 self.spr_height = self.bits.contains(Bits::SPR_HEIGHT) as u16 * 8 + 8; // 1 or 0 self.master_slave = self.bits.contains(Bits::MASTER_SLAVE) as u8; self.nmi_enabled = self.bits.contains(Bits::NMI_ENABLE); // 32 or 1 self.vram_increment = self.bits.contains(Bits::VRAM_INCREMENT); } } impl Reset for Ctrl { // https://www.nesdev.org/wiki/PPU_power_up_state fn reset(&mut self, _kind: ResetKind) { self.write(0); } } ================================================ FILE: tetanes-core/src/ppu/frame.rs ================================================ //! PPU frame implementation. use crate::{ common::{Reset, ResetKind}, mem::ConstArray, ppu::{self, Ppu}, }; use serde::{Deserialize, Serialize}; use std::ops::{Deref, DerefMut}; /// PPU frame. #[derive(Clone, Serialize, Deserialize)] #[serde(transparent)] #[repr(transparent)] #[must_use] pub struct Buffer(Box>); impl std::fmt::Debug for Buffer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Buffer({} elements)", self.0.len()) } } impl Default for Buffer { fn default() -> Self { Self(Box::new(ConstArray::new())) } } impl Deref for Buffer { type Target = [u16; ppu::size::FRAME]; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for Buffer { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } /// PPU frame. #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] #[repr(C)] pub struct Frame { #[serde(skip)] pub buffer: Buffer, pub count: u32, } impl Default for Frame { fn default() -> Self { Self::new() } } impl Frame { pub fn new() -> Self { Self { count: 0, buffer: Buffer::default(), } } #[inline(always)] pub const fn increment(&mut self) { self.count = self.count.wrapping_add(1); } #[inline(always)] #[must_use] pub fn pixel(&self, x: u16, y: u16) -> u16 { self.buffer[usize::from(x) + (usize::from(y) << 8)] } #[inline(always)] pub fn set_pixel(&mut self, x: u16, y: u16, color: u16) { self.buffer[usize::from(x) + (usize::from(y) << 8)] = color; } #[must_use] pub fn pixel_brightness(&self, x: u16, y: u16) -> u32 { let pixel = self.pixel(x, y); let index = (pixel as usize) * 3; let red = Ppu::NTSC_PALETTE[index]; let green = Ppu::NTSC_PALETTE[index + 1]; let blue = Ppu::NTSC_PALETTE[index + 2]; u32::from(red) + u32::from(green) + u32::from(blue) } #[inline(always)] #[must_use] pub const fn number(&self) -> u32 { self.count } #[inline(always)] pub const fn is_odd(&self) -> bool { self.count & 0x01 == 0x01 } #[inline(always)] #[must_use] pub fn buffer(&self) -> &[u16; ppu::size::FRAME] { &self.buffer } } impl Reset for Frame { fn reset(&mut self, _kind: ResetKind) { self.count = 0; self.buffer = Buffer::default(); } } ================================================ FILE: tetanes-core/src/ppu/mask.rs ================================================ //! PPUMASK register implementation. //! //! See: use crate::common::{Clock, NesRegion, Reset, ResetKind}; use bitflags::bitflags; use serde::{Deserialize, Serialize}; /// PPUMASK register. /// /// See: #[derive(Default, Serialize, Deserialize, Debug, Copy, Clone)] #[must_use] pub struct Mask { pub emphasis: u16, pub grayscale: u8, pub rendering_enabled: bool, pub prev_rendering_enabled: bool, pub pending_rendering_update: bool, pub show_left_bg: bool, pub show_left_spr: bool, pub show_bg: bool, pub show_spr: bool, pub bits: Bits, pub region: NesRegion, } bitflags! { // $2001 PPUMASK // // https://wiki.nesdev.org/w/index.php/PPU_registers#PPUMASK // BGRs bMmG // |||| |||+- Grayscale (0: normal color, 1: produce a grayscale display) // |||| ||+-- 1: Show background in leftmost 8 pixels of screen, 0: Hide // |||| |+--- 1: Show sprites in leftmost 8 pixels of screen, 0: Hide // |||| +---- 1: Show background // |||+------ 1: Show sprites // ||+------- Emphasize red // |+-------- Emphasize green // +--------- Emphasize blue #[derive(Default, Serialize, Deserialize, Debug, Copy, Clone)] #[must_use] pub struct Bits: u8 { const GRAYSCALE = 0x01; const SHOW_LEFT_BG = 0x02; const SHOW_LEFT_SPR = 0x04; const SHOW_BG = 0x08; const SHOW_SPR = 0x10; const EMPHASIZE_RED = 0x20; const EMPHASIZE_GREEN = 0x40; const EMPHASIZE_BLUE = 0x80; } } impl Mask { pub fn new(region: NesRegion) -> Self { let mut mask = Self { region, ..Default::default() }; mask.write(0); mask } #[inline] pub fn write(&mut self, val: u8) { self.bits = Bits::from_bits_truncate(val); self.grayscale = if self.bits.contains(Bits::GRAYSCALE) { 0x30 } else { 0x3F }; self.show_left_bg = self.bits.contains(Bits::SHOW_LEFT_BG); self.show_left_spr = self.bits.contains(Bits::SHOW_LEFT_SPR); self.show_bg = self.bits.contains(Bits::SHOW_BG); self.show_spr = self.bits.contains(Bits::SHOW_SPR); self.pending_rendering_update = self.rendering_enabled != (self.show_bg || self.show_spr); self.update_emphasis(); } pub fn update_emphasis(&mut self) { self.emphasis = u16::from( match self.region { NesRegion::Auto | NesRegion::Ntsc => self.bits.intersection( Bits::EMPHASIZE_RED | Bits::EMPHASIZE_GREEN | Bits::EMPHASIZE_BLUE, ), NesRegion::Pal | NesRegion::Dendy => { // Red/Green are swapped for PAL/Dendy let mut emphasis = self.bits.intersection(Bits::EMPHASIZE_BLUE); emphasis.set( Bits::EMPHASIZE_GREEN, self.bits.contains(Bits::EMPHASIZE_RED), ); emphasis.set( Bits::EMPHASIZE_RED, self.bits.contains(Bits::EMPHASIZE_GREEN), ); emphasis } } .bits(), ) << 1; } #[inline] pub fn set_region(&mut self, region: NesRegion) { self.region = region; self.update_emphasis(); } } impl Reset for Mask { // https://www.nesdev.org/wiki/PPU_power_up_state fn reset(&mut self, _kind: ResetKind) { self.write(0); } } impl Clock for Mask { fn clock(&mut self) { // Rendering enabled flag is set with a 1 cycle delay (setting it at cycle N won't take // effect until cycle N+2) if self.pending_rendering_update { self.pending_rendering_update = false; self.prev_rendering_enabled = self.rendering_enabled; self.rendering_enabled = self.show_bg || self.show_spr; self.pending_rendering_update = self.prev_rendering_enabled != self.rendering_enabled; } } } ================================================ FILE: tetanes-core/src/ppu/scroll.rs ================================================ //! PPUSCROLL register implementation. //! //! See: use crate::common::{Reset, ResetKind}; use serde::{Deserialize, Serialize}; /// PPUSCROLL register. /// /// See: #[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct Scroll { pub v: u16, // Subject to ADDR_MIRROR pub delay_v: u16, pub t: u16, // Temporary v - Also the addr of top-left onscreen tile pub fine_x: u16, pub fine_y: u16, pub write_latch: bool, // 1st or 2nd write toggle pub delay_v_cycles: u8, } impl Scroll { // PPUSCROLL masks // 1 00 00000 00000 // yyy NN YYYYY XXXXX // ||| || ||||| +++++- 5 bit coarse X // ||| || +++++------- 5 bit coarse Y // ||| |+------------- Nametable X offset // ||| +-------------- Nametable Y offset // +++---------------- 3 bit fine Y pub const COARSE_X_MASK: u16 = 0x001F; pub const COARSE_Y_MASK: u16 = 0x03E0; pub const NT_X_MASK: u16 = 0x0400; pub const NT_Y_MASK: u16 = 0x0800; pub const FINE_Y_MASK: u16 = 0x7000; const X_MAX_COL: u16 = 31; // last column of tiles - 255 pixel width / 8 pixel wide tiles const Y_MAX_COL: u16 = 29; // last row of tiles - (240 pixel height / 8 pixel tall tiles) - 1 const Y_OVER_COL: u16 = 31; // overscan row const Y_INCREMENT: u16 = 0x1000; // Increment y in bit 12 const ATTR_START: u16 = 0x23C0; const ADDR_MIRROR: u16 = 0x3FFF; // 15 bits: yyy NN YYYYY XXXXX pub const fn new() -> Self { Self { v: 0x0000, t: 0x0000, fine_x: 0x00, fine_y: 0x00, write_latch: false, delay_v_cycles: 0, delay_v: 0x0000, } } // https://wiki.nesdev.org/w/index.php/PPU_scrolling#Tile_and_attribute_fetching // NN 1111 YYY XXXXX // || |||| ||| +++-- high 3 bits of coarse X (x/4) // || |||| +++------ high 3 bits of coarse Y (y/4) // || ++++---------- attribute offset (960 bytes) // ++--------------- nametable select #[inline(always)] #[must_use] pub const fn attr_addr(&self) -> u16 { let nametable_select = self.v & (Self::NT_X_MASK | Self::NT_Y_MASK); let y_bits = (self.v >> 4) & 0x38; let x_bits = (self.v >> 2) & 0x07; Self::ATTR_START | nametable_select | y_bits | x_bits } #[inline(always)] #[must_use] pub const fn attr_shift(&self) -> u16 { (self.v & 0x02) | ((self.v >> 4) & 0x04) } #[inline(always)] #[must_use] pub const fn addr(&self) -> u16 { self.v & Self::ADDR_MIRROR // Only the lower 14 bits are valid } // Writes to PPUSCROLL affect v and t // 1st write writes X // 2nd write writes Y #[inline] pub fn write(&mut self, val: u8) { let val = u16::from(val); let lo_5_bit_mask: u16 = 0x1F; let fine_mask: u16 = 0x07; let fine_rshift = 3; if self.write_latch { // Write Y on second write // lo 3 bits goes into fine y, remaining 5 bits go into t for coarse y // val: HGFEDCBA // t: .CBA..HG FED..... let coarse_y_lshift = 5; let fine_y_lshift = 12; self.t = self.t & !(Self::FINE_Y_MASK | Self::COARSE_Y_MASK) // Empty Y | (((val >> fine_rshift) & lo_5_bit_mask) << coarse_y_lshift) // Set coarse Y | ((val & fine_mask) << fine_y_lshift); // Set fine Y } else { // Write X on first write // lo 3 bits goes into fine x, remaining 5 bits go into t for coarse x // val: HGFEDCBA // t: ........ ...HGFED // x: CBA self.t = self.t & !Self::COARSE_X_MASK // Empty coarse X | ((val >> fine_rshift) & lo_5_bit_mask); // Set coarse X self.fine_x = val & fine_mask; // Set fine X } self.write_latch = !self.write_latch; } // Write to PPUADDR affect v and t // 1st write writes hi 6 bits // 2nd write writes lo 8 bits // Total size is a 14 bit addr #[inline] pub fn write_addr(&mut self, val: u8) { if self.write_latch { // Write lo address on second write let lo_bits_mask = 0x7F00; // val: HGFEDCBA // t: ........ HGFEDCBA // v: t self.t = (self.t & lo_bits_mask) | u16::from(val); // PPUADDR update is apparently delayed by 2-3 PPU cycles (based on Visual NES findings) // A 3-cycle delay causes issues with the scanline test. self.delay_v_cycles = 2; self.delay_v = self.t; } else { // Write hi address on first write let hi_bits_mask = 0x00FF; let six_bits_mask = 0x003F; // val: ..FEDCBA // FEDCBA98 76543210 // t: 00FEDCBA ........ self.t = (self.t & hi_bits_mask) | ((u16::from(val) & six_bits_mask) << 8); } self.write_latch = !self.write_latch; } #[inline(always)] pub const fn set_v(&mut self, val: u16) { self.v = val; self.fine_y = self.v >> 12; } // Delayed update for PPUADDR after 2 PPU cycles (based on Visual NES findings) // Returns true when it was updated so the PPU can inform mappers monitoring $2006 reads and // writes. e.g. MMC3 clocks using A12 #[inline(always)] pub const fn delayed_update(&mut self) -> bool { if self.delay_v_cycles > 0 { self.delay_v_cycles -= 1; if self.delay_v_cycles == 0 { self.set_v(self.delay_v); return true; } } false } // Increment PPUADDR v by either 1 (going across) or 32 (going down) // Address wraps around #[inline(always)] pub const fn increment(&mut self, val: u16) { self.set_v(self.v.wrapping_add(val)); } // Copy Coarse X from register t and add it to PPUADDR v #[inline(always)] pub const fn copy_x(&mut self) { // .....N.. ...XXXXX // t: .....F.. ...EDCBA // v: .....F.. ...EDCBA let x_mask = Self::NT_X_MASK | Self::COARSE_X_MASK; self.set_v((self.v & !x_mask) | (self.t & x_mask)); } // Copy Fine y and Coarse Y from register t and add it to PPUADDR v #[inline(always)] pub const fn copy_y(&mut self) { // .yyyN.YY YYY..... // t: .IHGF.ED CBA..... // v: .IHGF.ED CBA..... let y_mask = Self::FINE_Y_MASK | Self::NT_Y_MASK | Self::COARSE_Y_MASK; self.set_v((self.v & !y_mask) | (self.t & y_mask)); } // Increment Coarse X // 0-4 bits are incremented, with overflow toggling bit 10 which switches the horizontal // nametable // https://wiki.nesdev.org/w/index.php/PPU_scrolling#Wrapping_around #[inline] pub const fn increment_x(&mut self) { // let v = self.v; // If we've reached the last column, toggle horizontal nametable if (self.v & Self::COARSE_X_MASK) == Self::X_MAX_COL { self.set_v((self.v & !Self::COARSE_X_MASK) ^ Self::NT_X_MASK); // toggles X nametable } else { self.set_v(self.v + 1); } } // Increment Fine Y // Bits 12-14 are incremented for Fine Y, with overflow incrementing coarse Y in bits 5-9 with // overflow toggling bit 11 which switches the vertical nametable // https://wiki.nesdev.org/w/index.php/PPU_scrolling#Wrapping_around #[inline] pub const fn increment_y(&mut self) { if (self.v & Self::FINE_Y_MASK) == Self::FINE_Y_MASK { self.set_v(self.v & !Self::FINE_Y_MASK); // set fine y = 0 and overflow into coarse y let mut y = (self.v & Self::COARSE_Y_MASK) >> 5; // Get 5 bits of coarse y if y == Self::Y_MAX_COL { y = 0; // switches vertical nametable self.set_v(self.v ^ Self::NT_Y_MASK); } else if y == Self::Y_OVER_COL { // Out of bounds. Does not switch nametable // Some games use this y = 0; } else { y += 1; // increment coarse y } self.set_v((self.v & !Self::COARSE_Y_MASK) | (y << 5)); // put coarse y back into v } else { // If fine y < 7 (0b111), increment self.set_v(self.v + Self::Y_INCREMENT); } } #[inline(always)] pub const fn reset_latch(&mut self) { self.write_latch = false; } #[inline(always)] pub fn write_nametable_select(&mut self, val: u8) { let nt_mask = Self::NT_Y_MASK | Self::NT_X_MASK; // val: ......BA // t: ....BA.. ........ self.t = (self.t & !nt_mask) | ((u16::from(val) & 0x03) << 10); // take lo 2 bits and set NN } } impl Reset for Scroll { // https://www.nesdev.org/wiki/PPU_power_up_state fn reset(&mut self, kind: ResetKind) { if kind == ResetKind::Hard { // v is not cleared on a a soft reset self.v = 0x0000; } self.fine_x = 0x00; self.write_latch = false; self.delay_v_cycles = 0; self.delay_v = 0x0000; } } ================================================ FILE: tetanes-core/src/ppu/sprite.rs ================================================ //! PPU OAM Sprite implementation. //! //! See: use serde::{Deserialize, Serialize}; use std::fmt; /// PPU OAM Sprite entry. /// /// See: #[derive(Copy, Clone, Serialize, Deserialize)] #[must_use] pub struct Sprite { pub x: u16, pub y: u16, pub tile_addr: u16, pub tile_lo: u8, pub tile_hi: u8, pub palette: u8, pub bg_priority: bool, pub flip_horizontal: bool, } impl Sprite { pub const fn new() -> Self { Self { x: 0xFF, y: 0xFF, tile_addr: 0x0000, tile_lo: 0x00, tile_hi: 0x00, palette: 0x07, bg_priority: true, flip_horizontal: true, } } } impl Default for Sprite { fn default() -> Self { Self::new() } } impl fmt::Debug for Sprite { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Sprite") .field("x", &self.x) .field("y", &self.y) .field("tile_addr", &format_args!("${:04X}", &self.tile_addr)) .field("tile_lo", &format_args!("${:02X}", &self.tile_lo)) .field("tile_hi", &format_args!("${:02X}", &self.tile_hi)) .field("palette", &format_args!("${:02X}", &self.palette)) .field("bg_priority", &self.bg_priority) .field("flip_horizontal", &self.flip_horizontal) .finish() } } ================================================ FILE: tetanes-core/src/ppu/status.rs ================================================ //! PPUSTATUS register implementation. //! //! See: use crate::common::{Reset, ResetKind}; use bitflags::bitflags; use serde::{Deserialize, Serialize}; /// PPUSTATUS register. /// /// See: #[derive(Default, Serialize, Deserialize, Debug, Copy, Clone)] #[must_use] pub struct Status { pub in_vblank: bool, pub spr_zero_hit: bool, pub spr_overflow: bool, pub bits: Bits, } bitflags! { // $2002 PPUSTATUS // // https://wiki.nesdev.org/w/index.php/PPU_registers#PPUSTATUS // VSO. .... // |||+-++++- PPU open bus. Returns stale PPU bus contents. // ||+------- Sprite overflow. The intent was for this flag to be set // || whenever more than eight sprites appear on a scanline, but a // || hardware bug causes the actual behavior to be more complicated // || and generate false positives as well as false negatives; see // || PPU sprite evaluation. This flag is set during sprite // || evaluation and cleared at dot 1 (the second dot) of the // || pre-render line. // |+-------- Sprite 0 Hit. Set when a nonzero pixel of sprite 0 overlaps // | a nonzero background pixel; cleared at dot 1 of the pre-render // | line. Used for raster timing. // +--------- Vertical blank has started (0: not in vblank; 1: in vblank) // Set at dot 1 of line 241 (the line *after* the post-render // line); cleared after reading $2002 and at dot 1 of the // pre-render line. #[derive(Default, Serialize, Deserialize, Debug, Copy, Clone)] #[must_use] pub struct Bits: u8 { const UNUSED1 = 0x01; const UNUSED2 = 0x02; const UNUSED3 = 0x04; const UNUSED4 = 0x08; const UNUSED5 = 0x10; const SPR_OVERFLOW = 0x20; const SPR_ZERO_HIT = 0x40; const VBLANK_STARTED = 0x80; } } impl Status { pub fn new() -> Self { let mut status = Self::default(); status.write(0); status } #[inline] pub const fn write(&mut self, val: u8) { self.bits = Bits::from_bits_truncate(val); self.spr_overflow = self.bits.contains(Bits::SPR_ZERO_HIT); self.spr_zero_hit = self.bits.contains(Bits::SPR_ZERO_HIT); self.in_vblank = self.bits.contains(Bits::VBLANK_STARTED); } #[inline(always)] #[must_use] pub const fn read(&self) -> u8 { self.bits.bits() } #[inline(always)] pub fn set_spr_overflow(&mut self, val: bool) { self.bits.set(Bits::SPR_OVERFLOW, val); self.spr_overflow = val; } #[inline(always)] pub fn set_spr_zero_hit(&mut self, val: bool) { self.bits.set(Bits::SPR_ZERO_HIT, val); self.spr_zero_hit = val; } #[inline(always)] pub fn set_in_vblank(&mut self, val: bool) { self.bits.set(Bits::VBLANK_STARTED, val); self.in_vblank = val; } #[inline(always)] pub fn reset_in_vblank(&mut self) { self.bits.remove(Bits::VBLANK_STARTED); self.in_vblank = false; } } impl Reset for Status { // https://www.nesdev.org/wiki/PPU_power_up_state fn reset(&mut self, kind: ResetKind) { if kind == ResetKind::Hard { self.set_in_vblank(false); // Technically random self.set_spr_zero_hit(false); self.set_spr_overflow(false); // Technically random } } } ================================================ FILE: tetanes-core/src/ppu.rs ================================================ //! NES PPU (Picture Processing Unit) implementation. use crate::{ common::{Clock, NesRegion, Regional, Reset, ResetKind}, debug::PpuDebugger, mapper::{Map, Mapper}, mem::{ConstArray, Read, Write}, ppu::frame::Frame, }; use ctrl::Ctrl; use mask::Mask; use scroll::Scroll; use serde::{Deserialize, Serialize}; use sprite::Sprite; use status::Status; use std::{ cmp::Ordering, ops::{Index, IndexMut}, }; use tracing::{error, trace}; pub mod ctrl; pub mod frame; pub mod mask; pub mod scroll; pub mod sprite; pub mod status; /// Nametable Mirroring Mode /// /// #[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)] #[must_use] pub enum Mirroring { Vertical = 0, #[default] Horizontal = 1, SingleScreenA = 2, SingleScreenB = 3, FourScreen = 4, } /// Palette RAM which enforces mirroring. #[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[must_use] #[repr(transparent)] pub struct PaletteRam(ConstArray); impl PaletteRam { /// Return palette address, mirrored. #[inline(always)] const fn mirror(addr: u16) -> usize { const PALETTE_MIRROR: [u8; 32] = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 17, 18, 19, 4, 21, 22, 23, 8, 25, 26, 27, 12, 29, 30, 31, ]; PALETTE_MIRROR[(addr & 0x1F) as usize] as usize } } impl Read for PaletteRam { #[inline(always)] fn peek(&self, addr: u16) -> u8 { self.0[Self::mirror(addr)] } } impl Write for PaletteRam { #[inline(always)] fn write(&mut self, addr: u16, val: u8) { self.0[Self::mirror(addr)] = val; } } /// Console-Internal RAM (VRAM) which enforces mirroring. #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] #[repr(transparent)] pub struct CIRam(Box>); impl CIRam { // Maps addresses to nametable pages based on mirroring mode // // Vram: [ A ] [ B ] // // Horizontal: [ A ] [ a ] // [ B ] [ b ] // // Vertical: [ A ] [ B ] // [ a ] [ b ] // // Single Screen A: [ A ] [ a ] // [ a ] [ a ] // // Single Screen B: [ b ] [ B ] // [ b ] [ b ] // // Fourscreen should not use this method and instead should rely on mapper translation. #[inline(always)] pub const fn mirror(addr: u16, mirroring: Mirroring) -> usize { let nametable = (addr >> mirroring as u16) & size::NAMETABLE; (nametable | (!nametable & addr & 0x03FF)) as usize } #[inline(always)] pub fn read(&mut self, addr: u16, mirroring: Mirroring) -> u8 { self.0[Self::mirror(addr, mirroring)] } #[inline(always)] pub fn peek(&self, addr: u16, mirroring: Mirroring) -> u8 { self.0[Self::mirror(addr, mirroring)] } #[inline(always)] pub fn write(&mut self, addr: u16, val: u8, mirroring: Mirroring) { self.0[Self::mirror(addr, mirroring)] = val } } impl Index for CIRam { type Output = u8; #[inline] fn index(&self, index: usize) -> &Self::Output { self.0.index(index) } } impl IndexMut for CIRam { #[inline] fn index_mut(&mut self, index: usize) -> &mut Self::Output { self.0.index_mut(index) } } pub trait PpuAddr { /// Returns whether this value can be used to fetch a nametable attribute byte. fn is_attr(&self) -> bool; /// Returns whether this value is a palette address. fn is_palette(&self) -> bool; } impl PpuAddr for u16 { #[inline(always)] fn is_attr(&self) -> bool { (*self & (size::NAMETABLE - 1)) >= addr::ATTR_OFFSET } #[inline(always)] fn is_palette(&self) -> bool { *self >= addr::PALETTE_START } } /// NES PPU. /// /// See: #[derive(Debug, Clone, Serialize, Deserialize)] #[must_use] #[repr(C)] pub struct Ppu { /// Master clock synced to Cpu master clock. pub master_clock: u32, /// (0, 340) cycles per scanline. pub cycle: u16, /// (0, 261) NTSC or (0, 311) PAL/Dendy scanlines per frame. pub scanline: u16, /// $2001 PPUMASK (write-only). pub mask: Mask, // === 20 === /// $2000 PPUCTRL (write-only). pub ctrl: Ctrl, // === 30 === /// $2005 PPUSCROLL and $2006 PPUADDR (write-only). pub scroll: Scroll, /// Scanline that Vertical Blank (VBlank) starts on. pub vblank_scanline: u16, /// Scanline that Prerender starts on. pub prerender_scanline: u16, /// Tile shift low byte. pub tile_shift_lo: u16, /// Tile shift high byte. pub tile_shift_hi: u16, /// Tile address. pub tile_addr: u16, /// Tile fetch buffer low byte. pub tile_lo: u8, /// Tile fetch buffer high byte. pub tile_hi: u8, /// Master clock divider. pub clock_divider: u8, /// Whatever was last read or written to to the Ppu. pub open_bus: u8, /// Internal signal that clears status registers and prevents writes and cleared at the end of /// VBlank. /// See: pub reset_signal: bool, /// Current tile palette. pub curr_palette: u8, /// Previous tile palette. pub prev_palette: u8, /// Next tile palette. pub next_palette: u8, /// Whether PPU is skipping rendering (used for /// [`HeadlessMode`](crate::control_deck::HeadlessMode)). pub skip_rendering: bool, /// Scanline is visible. pub is_visible_scanline: bool, /// Scanline is a pre-render scanline. pub is_prerender_scanline: bool, /// Scanline is a render scanline. pub is_render_scanline: bool, // === 64 : end of cache line === /// $2002 PPUSTATUS (read-only). pub status: Status, /// Scanline is a PAL sprite evaluation scanline. pub is_pal_spr_eval_scanline: bool, // Sprite/OAM evaluation. /// Sprite is in scanline range. pub spr_in_range: bool, /// Sprite 0 is in scanline range. pub spr_zero_in_range: bool, /// Secondary OAM address. pub secondary_oamaddr: u8, /// OAM evaluation is complete for scanline. pub oam_eval_done: bool, /// OAM address low byte. pub oamaddr_lo: u8, /// OAM address high byte. pub oamaddr_hi: u8, /// OAM data fetch buffer. pub oam_fetch: u8, /// $2003 OAM addr (write-only). pub oamaddr: u8, /// Sprite 0 is visible. pub spr_zero_visible: bool, /// Number of sprites on the current scanline. pub spr_count: u8, /// Sprite overflow count (> 8 on a scanline). pub overflow_count: u8, /// Current PPU frame buffer. pub frame: Frame, /// Console-Internal RAM (CIRAM). pub ciram: CIRam, // === 128 : end of cache line === // Palette RAM pub palette: PaletteRam, /// Secondary OAM data on a given scanline. pub secondary_oamdata: ConstArray, // === 192 : end of cache line === /// Each scanline can hold 8 sprites at a time before the `spr_overflow` flag is set. pub sprites: Box<[Sprite; 8]>, /// Whether a sprite is present at the given x-coordinate. Used for `spr_zero_hit` detection. // This is a per-frame optimization, shouldn't need to be saved #[serde(skip)] pub spr_present: ConstArray, // === 384 : end of cache line /// $2004 Object Attribute Memory (OAM) data (read/write). pub oamdata: ConstArray, // === 640 : end of cache line /// Mapper. pub mapper: Mapper, /// NMI pending. pub nmi_pending: bool, /// $2007 PPUDATA buffer. pub vram_buffer: u8, /// Prevents VBL from being triggered this frame. pub prevent_vbl: bool, /// Current NesRegion. pub region: NesRegion, /// Whether to emulate PPU warmup on power up. pub emulate_warmup: bool, /// Attached Ppu Debugger. // Don't save debug state #[serde(skip)] pub debugger: PpuDebugger, } impl Default for Ppu { fn default() -> Self { Self::new(NesRegion::default()) } } pub mod addr { //! Address constants. pub const NAMETABLE_START: u16 = 0x2000; pub const ATTR_OFFSET: u16 = 0x03C0; pub const PALETTE_START: u16 = 0x3F00; pub const PALETTE_END: u16 = 0x3F20; } pub mod size { //! Memory size constants. pub const WIDTH: u16 = 256; pub const HEIGHT: u16 = 240; pub const FRAME: usize = (WIDTH * HEIGHT) as usize; pub const NAMETABLE: u16 = 0x0400; pub const OAM: usize = 256; // 64 4-byte sprites per frame pub const SECONDARY_OAM: usize = 32; // 8 4-byte sprites per scanline pub const VRAM: usize = 0x0800; // Two 1k Nametables pub const PALETTE: usize = 32; // 32 possible colors at a time } pub mod cycle { //! Cycle constants. //! use std::ops::RangeInclusive; pub const START: u16 = 0; pub const ODD_SKIP: u16 = 339; // Odd frames skip the last cycle pub const END: u16 = 340; pub const VISIBLE_START: u16 = 1; // Tile data fetching starts pub const VISIBLE_END: u16 = 256; // 2 cycles each for 4 fetches = 32 tiles pub const VBLANK: u16 = VISIBLE_START; // When VBlank flag gets set/cleared pub const OAM_CLEAR_START: u16 = 1; pub const OAM_CLEAR_END: u16 = 64; pub const SPR_EVAL_START: u16 = 65; pub const SPR_EVAL_START1: u16 = 66; // Used to split up match arms pub const SPR_EVAL_END0: u16 = 255; // Used to split up match arms pub const SPR_EVAL_END: u16 = 256; pub const SPR_FETCH_START: u16 = 257; // Sprites for next scanline fetch starts pub const SPR_FETCH_END: u16 = 320; // 2 cycles each for 4 fetches = 8 sprites pub const SPR_FETCH_RANGE: RangeInclusive = SPR_FETCH_START..=SPR_FETCH_END; pub const BG_PREFETCH_START: u16 = 321; // Tile data for next scanline fetched pub const BG_PREFETCH_END: u16 = 336; // 2 cycles each for 4 fetches = 2 tiles pub const BG_PREFETCH_RANGE: RangeInclusive = BG_PREFETCH_START..=BG_PREFETCH_END; pub const BG_DUMMY_START: u16 = 337; // Dummy fetches - use is unknown pub const BG_DUMMY_END: u16 = END; pub const INC_Y: u16 = 256; // Increase Y scroll when it reaches end of the screen pub const COPY_Y_START: u16 = 280; // Copy Y scroll start pub const COPY_Y_END: u16 = 304; // Copy Y scroll stop pub const COPY_Y_RANGE: RangeInclusive = COPY_Y_START..=COPY_Y_END; // Clock dividers pub const DIVIDER_NTSC: u8 = 4; pub const DIVIDER_PAL: u8 = 5; pub const DIVIDER_DENDY: u8 = DIVIDER_PAL; } pub mod scanline { //! Scanline constants. //! pub const START: u16 = 0; pub const VISIBLE_START: u16 = START; pub const VISIBLE_END: u16 = 239; pub const POSTRENDER: u16 = 240; pub const PRERENDER_NTSC: u16 = 261; pub const PRERENDER_PAL: u16 = 311; pub const PRERENDER_DENDY: u16 = PRERENDER_PAL; pub const VBLANK_NTSC: u16 = 241; pub const VBLANK_PAL: u16 = VBLANK_NTSC; pub const VBLANK_DENDY: u16 = 291; } impl Ppu { pub const NTSC_PALETTE: &'static [u8] = include_bytes!("../ntscpalette.pal"); /// NES PPU System Palette /// 64 total possible colors, though only 32 can be loaded at a time #[rustfmt::skip] pub const SYSTEM_PALETTE: [(u8,u8,u8); 64] = [ // 0x00 (0x54, 0x54, 0x54), (0x00, 0x1E, 0x74), (0x08, 0x10, 0x90), (0x30, 0x00, 0x88), // $00-$03 (0x44, 0x00, 0x64), (0x5C, 0x00, 0x30), (0x54, 0x04, 0x00), (0x3C, 0x18, 0x00), // $04-$07 (0x20, 0x2A, 0x00), (0x08, 0x3A, 0x00), (0x00, 0x40, 0x00), (0x00, 0x3C, 0x00), // $08-$0B (0x00, 0x32, 0x3C), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00), // $0C-$0F // 0x10 (0x98, 0x96, 0x98), (0x08, 0x4C, 0xC4), (0x30, 0x32, 0xEC), (0x5C, 0x1E, 0xE4), // $10-$13 (0x88, 0x14, 0xB0), (0xA0, 0x14, 0x64), (0x98, 0x22, 0x20), (0x78, 0x3C, 0x00), // $14-$17 (0x54, 0x5A, 0x00), (0x28, 0x72, 0x00), (0x08, 0x7C, 0x00), (0x00, 0x76, 0x28), // $18-$1B (0x00, 0x66, 0x78), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00), // $1C-$1F // 0x20 (0xEC, 0xEE, 0xEC), (0x4C, 0x9A, 0xEC), (0x78, 0x7C, 0xEC), (0xB0, 0x62, 0xEC), // $20-$23 (0xE4, 0x54, 0xEC), (0xEC, 0x58, 0xB4), (0xEC, 0x6A, 0x64), (0xD4, 0x88, 0x20), // $24-$27 (0xA0, 0xAA, 0x00), (0x74, 0xC4, 0x00), (0x4C, 0xD0, 0x20), (0x38, 0xCC, 0x6C), // $28-$2B (0x38, 0xB4, 0xCC), (0x3C, 0x3C, 0x3C), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00), // $2C-$2F // 0x30 (0xEC, 0xEE, 0xEC), (0xA8, 0xCC, 0xEC), (0xBC, 0xBC, 0xEC), (0xD4, 0xB2, 0xEC), // $30-$33 (0xEC, 0xAE, 0xEC), (0xEC, 0xAE, 0xD4), (0xEC, 0xB4, 0xB0), (0xE4, 0xC4, 0x90), // $34-$37 (0xCC, 0xD2, 0x78), (0xB4, 0xDE, 0x78), (0xA8, 0xE2, 0x90), (0x98, 0xE2, 0xB4), // $38-$3B (0xA0, 0xD6, 0xE4), (0xA0, 0xA2, 0xA0), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00), // $3C-$3F ]; /// Create a new PPU instance. pub fn new(region: NesRegion) -> Self { let mut ppu = Self { master_clock: 0, clock_divider: 0, cycle: 0, scanline: 0, vblank_scanline: 0, prerender_scanline: 0, is_visible_scanline: true, is_prerender_scanline: false, is_render_scanline: true, is_pal_spr_eval_scanline: false, open_bus: 0x00, mask: Mask::new(region), scroll: Scroll::new(), ctrl: Ctrl::new(), // NOTE: PPU RAM is a bit more predictable at power on - games like Huge Insect don't // properly initialize both nametables, which can result in garbage sprites when // randomizing CIRAM. palette: PaletteRam(ConstArray::new()), mapper: Mapper::none(), ciram: CIRam(Box::new(ConstArray::new())), prev_palette: 0x00, curr_palette: 0x00, next_palette: 0x00, tile_shift_lo: 0x0000, tile_shift_hi: 0x0000, tile_lo: 0x00, tile_hi: 0x00, tile_addr: 0x0000, status: Status::new(), nmi_pending: false, oam_fetch: 0x00, oamaddr: 0x0000, oamaddr_lo: 0x00, oamaddr_hi: 0x00, oam_eval_done: false, secondary_oamaddr: 0x0000, overflow_count: 0, spr_in_range: false, spr_zero_in_range: false, spr_zero_visible: false, spr_count: 0, vram_buffer: 0x00, oamdata: ConstArray::new(), secondary_oamdata: ConstArray::new(), sprites: [Sprite::new(); 8].into(), spr_present: ConstArray::new(), prevent_vbl: false, frame: Frame::new(), region, skip_rendering: false, reset_signal: false, emulate_warmup: false, debugger: Default::default(), }; ppu.set_region(ppu.region); ppu } /// Read a byte from CHR-ROM/RAM/CIRAM at a given address. #[inline(always)] fn chr_read(&mut self, addr: u16) -> u8 { self.mapper.chr_read(addr, &self.ciram) } /// Peek a byte from CHR-ROM/RAM/CIRAM at a given address. #[inline(always)] fn chr_peek(&self, addr: u16) -> u8 { self.mapper.chr_peek(addr, &self.ciram) } /// Write a byte to CHR-RAM/CIRAM at a given address. #[inline(always)] fn chr_write(&mut self, addr: u16, val: u8) { self.mapper.chr_write(addr, val, &mut self.ciram) } /// Read from `addr` on Ppu bus. #[inline] fn bus_read(&mut self, addr: u16) -> u8 { self.open_bus = match addr { 0x0000..=0x3EFF => self.chr_read(addr), 0x3F00..=0x3FFF => self.palette.read(addr), _ => { error!("unexpected PPU memory access at ${:04X}", addr); 0x00 } }; self.open_bus } #[inline] fn bus_peek(&self, addr: u16) -> u8 { match addr { 0x0000..=0x3EFF => self.chr_peek(addr), 0x3F00..=0x3FFF => self.palette.peek(addr), _ => { error!("unexpected PPU memory access at ${:04X}", addr); 0x00 } } } /// Write `val` to `addr` on Ppu bus. #[inline] fn bus_write(&mut self, addr: u16, val: u8) { self.open_bus = val; match addr { 0x0000..=0x3EFF => self.chr_write(addr, val), 0x3F00..=0x3FFF => self.palette.write(addr, val), _ => error!("unexpected PPU memory access at ${:04X}", addr), } } /// Return the current frame buffer. #[inline] #[must_use] pub fn frame_buffer(&self) -> &[u16] { self.frame.buffer() } /// Return the current frame number. #[inline(always)] #[must_use] pub const fn frame_number(&self) -> u32 { self.frame.number() } /// Get the pixel pixel brightness at the given coordinates. #[inline] #[must_use] pub fn pixel_brightness(&self, x: u16, y: u16) -> u32 { self.frame.pixel_brightness(x, y) } /// Load a Mapper into the PPU. #[inline] pub fn load_mapper(&mut self, mapper: Mapper) { self.mapper = mapper; } /// Return the current Nametable mirroring mode. #[inline] pub fn mirroring(&self) -> Mirroring { self.mapper.mirroring() } /// Snapshot the PPU state, excluding internal transient state, the current frame buffer. pub fn snapshot(&self) -> Self { Self { master_clock: self.master_clock, clock_divider: self.clock_divider, cycle: self.cycle, scanline: self.scanline, vblank_scanline: self.vblank_scanline, prerender_scanline: self.prerender_scanline, is_visible_scanline: self.is_visible_scanline, is_prerender_scanline: self.is_prerender_scanline, is_render_scanline: self.is_render_scanline, is_pal_spr_eval_scanline: self.is_pal_spr_eval_scanline, open_bus: self.open_bus, mask: self.mask, scroll: self.scroll, ctrl: self.ctrl, palette: self.palette, ciram: self.ciram.clone(), mapper: self.mapper.clone(), curr_palette: self.curr_palette, status: self.status, secondary_oamaddr: self.secondary_oamaddr, oamdata: self.oamdata, secondary_oamdata: self.secondary_oamdata, sprites: self.sprites.clone(), ..Default::default() } } /// Load the passed given buffer with RGBA pixels from the current nametables. pub fn load_nametables(&self, nametables: &mut [u8]) { for i in 0..4 { let base_addr = addr::NAMETABLE_START + i * size::NAMETABLE; let x_offset = (i % 2) * size::WIDTH; let y_offset = (i / 2) * size::HEIGHT; for addr in base_addr..(base_addr + size::NAMETABLE - 64) { let x_scroll = addr & Scroll::COARSE_X_MASK; let y_scroll = (addr & Scroll::COARSE_Y_MASK) >> 5; let base_nametable_addr = addr::NAMETABLE_START | (addr & (Scroll::NT_X_MASK | Scroll::NT_Y_MASK)); let base_attr_addr = base_nametable_addr + addr::ATTR_OFFSET; let tile_index = u16::from(self.chr_peek(addr)); let tile_addr = self.ctrl.bg_select | (tile_index << 4); let supertile = ((y_scroll & 0xFC) << 1) + (x_scroll >> 2); let attr = u16::from(self.chr_peek(base_attr_addr + supertile)); let attr_shift = (x_scroll & 0x02) | ((y_scroll & 0x02) << 1); let palette_addr = ((attr >> attr_shift) & 0x03) << 2; let tile_num = x_scroll + (y_scroll << 5); let tile_x = (tile_num % 32) << 3; let tile_y = (tile_num / 32) << 3; for y in 0..8 { let tile_addr = tile_addr + y; let tile_lo = self.chr_peek(tile_addr); let tile_hi = self.chr_peek(tile_addr + 8); for x in 0..8 { let tile_palette = (((tile_hi >> x) & 1) << 1) | (tile_lo >> x) & 1; let palette = palette_addr | u16::from(tile_palette); let color = self .palette .peek(addr::PALETTE_START | ((palette & 0x03 > 0) as u16 * palette)); let x = tile_x + (7 - x); let y = tile_y + y; Self::set_pixel( u16::from(color & self.mask.grayscale) | self.mask.emphasis, x + x_offset, y + y_offset, 2 * size::WIDTH, nametables, ); } } } } } /// Load the given buffer with RGBA pixels from the current pattern tables. pub fn load_pattern_tables(&self, pattern_tables: &mut [u8]) { for i in 0..2 { let start = i * 0x1000; let end = start + 0x1000; let x_offset = (i % 2) * size::WIDTH / 2; for tile_addr in (start..end).step_by(16) { let tile_x = ((tile_addr % 0x1000) % 256) / 2; let tile_y = ((tile_addr % 0x1000) / 256) * 8; for y in 0..8 { let tile_lo = u16::from(self.chr_peek(tile_addr + y)); let tile_hi = u16::from(self.chr_peek(tile_addr + y + 8)); for x in 0..8 { let palette = (((tile_hi >> x) & 0x01) << 1) | ((tile_lo >> x) & 0x01); let color = u16::from(self.palette.peek(addr::PALETTE_START | palette)); let x = tile_x + (7 - x); let y = tile_y + y; Self::set_pixel(color, x + x_offset, y, size::WIDTH, pattern_tables); } } } } } /// Load the given buffer with RGBA pixels from the current pattern tables. pub fn load_oam( &self, oam_table: &mut [u8], sprite_nametable: &mut [u8], sprites: &mut [Sprite], ) { // TODO: de-duplicate this with load_sprites for (i, oamdata) in self.oamdata.chunks(4).enumerate() { if let [y, tile_index, attr, x] = oamdata { let sprite_x = u16::from(*x); let sprite_y = u16::from(*y); let tile_index = u16::from(*tile_index); let palette = ((attr & 0x03) << 2) | 0x10; let bg_priority = (attr & 0x20) == 0x20; let flip_horizontal = (attr & 0x40) == 0x40; let flip_vertical = (attr & 0x80) == 0x80; let height = self.ctrl.spr_height; let tile_addr = if height == 16 { // Use bit 0 of tile index to determine pattern table ((tile_index & 0x01) * 0x1000) | ((tile_index & 0xFE) << 4) } else { self.ctrl.spr_select | (tile_index << 4) }; sprites[i] = Sprite { x: sprite_x, y: sprite_y, tile_addr, palette, bg_priority, flip_horizontal, ..Sprite::default() }; let tile_x = (i % 8) as u16 * 8; let tile_y = (i / 8) as u16 * 8; for y in 0..8 { let mut line_offset = if flip_vertical { (height) - 1 - y } else { y }; if height == 16 && line_offset >= 8 { line_offset += 8; } let tile_lo = self.chr_peek(tile_addr + line_offset); let tile_hi = self.chr_peek(tile_addr + line_offset + 8); for x in 0..8 { let spr_color = if flip_horizontal { (((tile_hi >> x) & 0x01) << 1) | ((tile_lo >> x) & 0x01) } else { (((tile_hi << x) & 0x80) >> 6) | (((tile_lo << x) & 0x80) >> 7) }; let palette = palette + spr_color; let color = self.palette.peek( addr::PALETTE_START | ((palette & 0x03 > 0) as u16 * u16::from(palette)), ); Self::set_pixel(u16::from(color), tile_x + x, tile_y + y, 64, oam_table); let x = sprite_x + x; let y = sprite_y + y; let show_left_bg = self.mask.show_left_bg; let show_left_spr = self.mask.show_left_spr; let show_bg = self.mask.show_bg; let show_spr = self.mask.show_spr; let fine_x = self.scroll.fine_x; let left_clip_bg = x < 8 && !show_left_bg; let bg_color = if show_bg && !left_clip_bg { ((((self.tile_shift_hi << fine_x) & 0x8000) >> 14) | (((self.tile_shift_lo << fine_x) & 0x8000) >> 15)) as u8 } else { 0 }; let left_clip_spr = x < 8 && !show_left_spr; if show_spr && !left_clip_spr && x < size::WIDTH && y < size::HEIGHT { let color = if bg_color == 0 || !bg_priority { color } else if (fine_x + (x & 0x07)) < 8 { self.prev_palette + bg_color } else { self.curr_palette + bg_color }; Self::set_pixel(u16::from(color), x, y, size::WIDTH, sprite_nametable); } } } } } } /// Load the given buffer with RGBA pixels from the current palettes. pub fn load_palettes(&self, palettes: &mut [u8], colors: &mut [u8]) { for addr in addr::PALETTE_START..addr::PALETTE_END { let offset = addr - addr::PALETTE_START; let x = offset % 16; let y = offset / 16; let color = self.palette.peek(addr); colors[usize::from(offset)] = color; Self::set_pixel(u16::from(color), x, y, 16, palettes); } } fn set_pixel(color: u16, x: u16, y: u16, width: u16, pixels: &mut [u8]) { let index = (color as usize) * 3; let idx = 4 * (usize::from(x) + usize::from(y) * usize::from(width)); assert!(Ppu::NTSC_PALETTE.len() > index + 2); assert!(pixels.len() > 2); assert!(idx + 2 < pixels.len()); pixels[idx] = Ppu::NTSC_PALETTE[index]; pixels[idx + 1] = Ppu::NTSC_PALETTE[index + 1]; pixels[idx + 2] = Ppu::NTSC_PALETTE[index + 2]; pixels[idx + 3] = 0xFF; } #[inline(always)] const fn increment_vram_addr(&mut self) { // During rendering, v increments coarse X and coarse Y simultaneously if self.scanline > scanline::VISIBLE_END || !self.mask.rendering_enabled { self.scroll .increment(self.ctrl.vram_increment as u16 * 31 + 1); } else { self.scroll.increment_x(); self.scroll.increment_y(); } } fn start_vblank(&mut self) { trace!("Start VBL - PPU:{:3},{:3}", self.cycle, self.scanline); if !self.prevent_vbl { self.status.set_in_vblank(true); if self.ctrl.nmi_enabled { self.nmi_pending = true; trace!("VBL NMI - PPU:{:3},{:3}", self.cycle, self.scanline,); } } self.prevent_vbl = false; } fn stop_vblank(&mut self) { trace!( "Stop VBL, Sprite0 Hit, Overflow - PPU:{:3},{:3}", self.cycle, self.scanline ); self.status.set_spr_zero_hit(false); self.status.set_spr_overflow(false); self.status.reset_in_vblank(); self.nmi_pending = false; self.reset_signal = false; self.open_bus = 0; // Clear open bus every frame } /// Fetch BG nametable byte. /// /// See: #[inline] fn fetch_bg_nt_byte(&mut self) { self.prev_palette = self.curr_palette; self.curr_palette = self.next_palette; self.tile_shift_lo |= u16::from(self.tile_lo); self.tile_shift_hi |= u16::from(self.tile_hi); let nametable_addr_mask = 0x0FFF; // Only need lower 12 bits let addr = addr::NAMETABLE_START | (self.scroll.addr() & nametable_addr_mask); let tile_index = u16::from(self.chr_read(addr)); self.tile_addr = self.ctrl.bg_select | (tile_index << 4) | self.scroll.fine_y; } /// Fetch BG attribute byte. /// /// See: #[inline(always)] fn fetch_bg_attr_byte(&mut self) { let addr = self.scroll.attr_addr(); let shift = self.scroll.attr_shift(); self.next_palette = ((self.chr_read(addr) >> shift) & 0x03) << 2; } /// Fetch 4 tiles and write out shift registers every 8th cycle. /// Each tile fetch takes 2 cycles. /// /// See: #[inline] fn bg_fetch_cycle(&mut self) { let phase = self.cycle & 0x07; if self.mask.prev_rendering_enabled && phase == 0 { // Increment Coarse X every 8 cycles (e.g. 8 pixels) since sprites are 8x wide self.scroll.increment_x(); // 256, Increment Fine Y when we reach the end of the screen if self.cycle == cycle::INC_Y { self.scroll.increment_y(); } return; } match phase { 1 => self.fetch_bg_nt_byte(), 3 => self.fetch_bg_attr_byte(), 5 => self.tile_lo = self.chr_read(self.tile_addr), 7 => self.tile_hi = self.chr_read(self.tile_addr + 8), _ => (), } } fn oam_eval_cycle(&mut self) { if self.cycle & 0x01 == 0x01 { // Odd cycles are reads from OAM self.oam_fetch = self.oamdata[self.oamaddr as usize]; } else { // Local variables improve cache locality let scanline = self.scanline; let mut oam_eval_done = self.oam_eval_done; let mut secondary_oamaddr = self.secondary_oamaddr; let mut oam_fetch = self.oam_fetch; let mut spr_in_range = self.spr_in_range; let mut spr_zero_in_range = self.spr_zero_in_range; let mut oamaddr_hi = self.oamaddr_hi; let mut oamaddr_lo = self.oamaddr_lo; let secondary_oamindex = secondary_oamaddr as usize & 0x1F; debug_assert!(secondary_oamindex < self.secondary_oamdata.len()); // oamaddr rolled over, so we're done reading if oam_eval_done { oamaddr_hi = (oamaddr_hi + 1) & 0x3F; if secondary_oamaddr >= 0x20 { oam_fetch = self.secondary_oamdata[secondary_oamindex]; } } else { // If previously not in range, interpret this byte as y let y = u16::from(oam_fetch); let height = self.ctrl.spr_height; spr_in_range |= !spr_in_range && (y..y + height).contains(&scanline); // Even cycles are writes to Secondary OAM if secondary_oamaddr < 0x20 { self.secondary_oamdata[secondary_oamindex] = oam_fetch; if spr_in_range { oamaddr_lo += 1; secondary_oamaddr += 1; spr_zero_in_range |= oamaddr_hi == 0x00; if oamaddr_lo == 0x04 { spr_in_range = false; oamaddr_lo = 0x00; oamaddr_hi = (oamaddr_hi + 1) & 0x3F; oam_eval_done |= oamaddr_hi == 0x00; } } else { oamaddr_hi = (oamaddr_hi + 1) & 0x3F; oam_eval_done |= oamaddr_hi == 0x00; } } else { oam_fetch = self.secondary_oamdata[secondary_oamindex]; if spr_in_range { self.status.set_spr_overflow(true); oamaddr_lo += 1; if oamaddr_lo == 0x04 { oamaddr_lo = 0x00; oamaddr_hi = (oamaddr_hi + 1) & 0x3F; } match self.overflow_count.cmp(&0) { Ordering::Equal => self.overflow_count = 3, Ordering::Greater => { self.overflow_count -= 1; let no_overflow = self.overflow_count == 0; oam_eval_done |= no_overflow; if no_overflow { oamaddr_lo = 0; } } Ordering::Less => (), } } else { oamaddr_hi = (oamaddr_hi + 1) & 0x3F; oamaddr_lo = (oamaddr_lo + 1) & 0x03; oam_eval_done |= oamaddr_hi == 0x00; } } } self.oamaddr = (oamaddr_hi << 2) | (oamaddr_lo & 0x03); self.oamaddr_hi = oamaddr_hi; self.oamaddr_lo = oamaddr_lo; self.oam_eval_done = oam_eval_done; self.secondary_oamaddr = secondary_oamaddr; self.oam_fetch = oam_fetch; self.spr_in_range = spr_in_range; self.spr_zero_in_range = spr_zero_in_range; } } fn spr_eval_cycle(&mut self) { // Local variables improve cache locality match self.cycle { // 1. Clear Secondary OAM // 1..=64 cycle::OAM_CLEAR_START..=cycle::OAM_CLEAR_END => { self.oam_fetch = 0xFF; self.secondary_oamdata = ConstArray::filled(0xFF); } // 2. Read OAM to find first eight sprites on this scanline // 3. With > 8 sprites, check (wrongly) for more sprites to set overflow flag // 64..=256 cycle::SPR_EVAL_START => { self.spr_in_range = false; self.spr_zero_in_range = false; self.secondary_oamaddr = 0x00; self.oam_eval_done = false; self.oamaddr_hi = (self.oamaddr >> 2) & 0x3F; self.oamaddr_lo = self.oamaddr & 0x03; self.oam_eval_cycle(); } cycle::SPR_EVAL_END => { self.spr_zero_visible = self.spr_zero_in_range; self.spr_count = self.secondary_oamaddr >> 2; self.oam_eval_cycle(); } cycle::SPR_EVAL_START1..=cycle::SPR_EVAL_END0 => self.oam_eval_cycle(), _ => (), } } fn load_sprites(&mut self) { // Local variables improve cache locality let cycle = self.cycle; let scanline = self.scanline; let spr_count = usize::from(self.spr_count); let idx = (cycle - cycle::SPR_FETCH_START) as usize / 8; let oam_idx = idx << 2; if let [y, tile_index, attr, x] = self.secondary_oamdata[oam_idx..=oam_idx + 3] { let x = u16::from(x); let y = u16::from(y); let mut tile_index = u16::from(tile_index); let flip_vertical = (attr & 0x80) == 0x80; let height = self.ctrl.spr_height; // Should be in the range 0..=7 or 0..=15 depending on sprite height let mut line_offset = if (y..y + height).contains(&scanline) { scanline - y } else { 0 }; if flip_vertical { line_offset = height - 1 - line_offset; } if idx >= spr_count { line_offset = 0; tile_index = 0xFF; } let tile_addr = if height == 16 { // Use bit 0 of tile index to determine pattern table let sprite_select = (tile_index & 0x01) * 0x1000; if line_offset >= 8 { line_offset += 8; } sprite_select | ((tile_index & 0xFE) << 4) | line_offset } else { self.ctrl.spr_select | (tile_index << 4) | line_offset }; if idx < spr_count { self.sprites[idx] = Sprite { x, y, tile_addr, tile_lo: self.chr_read(tile_addr), tile_hi: self.chr_read(tile_addr + 8), palette: ((attr & 0x03) << 2) | 0x10, bg_priority: (attr & 0x20) == 0x20, flip_horizontal: (attr & 0x40) == 0x40, }; let cycle = usize::from(x + 1); self.spr_present[cycle..(cycle + 8).min(256)].fill(true); } else { // Fetches for remaining sprites/hidden fetch tile $FF // Required for accurate MMC3 IRQ let _ = self.chr_read(tile_addr); let _ = self.chr_read(tile_addr + 8); } } } // https://wiki.nesdev.org/w/index.php/PPU_OAM #[inline] fn spr_fetch_cycle(&mut self) { // OAMADDR set to $00 on prerender and visible scanlines self.write_oamaddr(0x00); match self.cycle & 0x07 { // Garbage NT sprite fetch (257, 265, 273, etc.) // Required for proper MC-ACC IRQs (MMC3 clone) 1 => self.fetch_bg_nt_byte(), // Garbage NT fetch 3 => self.fetch_bg_attr_byte(), // Garbage attr fetch // Cycle 260, 268, etc. This is an approximation (each tile is actually loaded in 8 // steps (e.g from 257 to 264)) 4 => self.load_sprites(), _ => (), } } #[inline] fn pixel_palette(&mut self) -> u8 { let cycle = self.cycle; let x = cycle - 1; let show_left_bg = self.mask.show_left_bg; let show_left_spr = self.mask.show_left_spr; let show_bg = self.mask.show_bg; let show_spr = self.mask.show_spr; let fine_x = self.scroll.fine_x; let bg_shift = 15 - fine_x; let min_render_x = x >= 8; let bg_mask = u8::from(show_bg & (show_left_bg | min_render_x)); let bg_color = bg_mask * ((((self.tile_shift_hi >> bg_shift) & 0x01) << 1) | ((self.tile_shift_lo >> bg_shift) & 0x01)) as u8; let count = usize::from(self.spr_count); if (count > 0) & (show_spr & (show_left_spr | min_render_x)) & self.spr_present[usize::from(cycle)] { for (i, sprite) in self.sprites.iter().take(count).enumerate() { let spr_shift = x.wrapping_sub(sprite.x); if spr_shift <= 7 { let spr_shift = if sprite.flip_horizontal { spr_shift } else { 7 - spr_shift }; let spr_color = (((sprite.tile_hi >> spr_shift) & 0x01) << 1) | ((sprite.tile_lo >> spr_shift) & 0x01); if spr_color != 0 { if self.mask.rendering_enabled & !self.status.spr_zero_hit & self.spr_zero_visible & (cycle != 256) & (i == 0) & (bg_color != 0) { self.status.set_spr_zero_hit(true); } if !sprite.bg_priority | (bg_color == 0) { return sprite.palette + spr_color; } break; } } } } let palette_mask = u8::from((fine_x + (x & 0x07)) < 8); let palette = palette_mask * self.prev_palette + (1 - palette_mask) * self.curr_palette; palette + bg_color } #[inline] fn headless_sprite_zero_hit(&mut self) { if !self.spr_zero_visible || self.status.spr_zero_hit { return; } let cycle = self.cycle; let show_left_bg = self.mask.show_left_bg; let show_left_spr = self.mask.show_left_spr; let show_bg = self.mask.show_bg; let show_spr = self.mask.show_spr; let min_render_x = cycle >= 9; let bg_mask = u8::from(show_bg & (show_left_bg | min_render_x)); if (bg_mask == 0) | !(show_spr & (show_left_spr | min_render_x)) | (cycle == 256) | !self.spr_present[usize::from(cycle)] { return; } let bg_shift = 15 - self.scroll.fine_x; let bg_color = bg_mask * ((((self.tile_shift_hi >> bg_shift) & 0x01) << 1) | ((self.tile_shift_lo >> bg_shift) & 0x01)) as u8; if bg_color == 0 { return; } let sprite = &self.sprites[0]; let spr_shift = cycle.wrapping_sub(sprite.x).wrapping_sub(1); if spr_shift <= 7 { let spr_shift = if sprite.flip_horizontal { spr_shift } else { 7 - spr_shift }; let spr_color = (((sprite.tile_hi >> spr_shift) & 0x01) << 1) | ((sprite.tile_lo >> spr_shift) & 0x01); if spr_color != 0 { self.status.set_spr_zero_hit(true); } } } #[inline(always)] fn render_pixel(&mut self) { let addr = self.scroll.addr(); let color = if self.mask.rendering_enabled || !addr.is_palette() { let palette = u16::from(self.pixel_palette()); self.palette .read(addr::PALETTE_START | ((palette & 0x03 > 0) as u16 * palette)) } else { self.palette.read(addr) }; self.frame.set_pixel( self.cycle - 1, self.scanline, u16::from(color & self.mask.grayscale) | self.mask.emphasis, ); } #[inline(always)] pub fn clock_to(&mut self, clock: u32) { let divider = u32::from(self.clock_divider); while self.master_clock + divider <= clock { self.clock(); self.master_clock += divider; } } // $2000 | RW | PPUCTRL // | 0-1 | Name Table to show: // | | // | | +-----------+-----------+ // | | | 2 ($2800) | 3 ($2C00) | // | | +-----------+-----------+ // | | | 0 ($2000) | 1 ($2400) | // | | +-----------+-----------+ // | | // | | Remember, though, that because of the mirroring, there are // | | only 2 real Name Tables, not 4. // | 2 | Vertical Write, 1 = PPU memory address increments by 32: // | | // | | Name Table, VW=0 Name Table, VW=1 // | | +----------------+ +----------------+ // | | |----> write | | | write | // | | | | | V | // | | // | 3 | Sprite Pattern Table address, 1 = $1000, 0 = $0000 // | 4 | Screen Pattern Table address, 1 = $1000, 0 = $0000 // | 5 | Sprite Size, 1 = 8x16, 0 = 8x8 // | 6 | Hit Switch, 1 = generate interrupts on Hit (incorrect ???) // | 7 | VBlank Switch, 1 = generate interrupts on VBlank pub fn write_ctrl(&mut self, val: u8) { self.open_bus = val; if self.reset_signal { return; } self.ctrl.write(val); self.scroll.write_nametable_select(val); // MMC5 tracks changes to PPUCTRL self.mapper.ppu_write(0x2000, val); trace!( "$2000 NMI Enabled: {} - PPU:{:3},{:3}", self.ctrl.nmi_enabled, self.cycle, self.scanline, ); // By toggling NMI (bit 7) during VBlank without reading $2002, /NMI can be pulled low // multiple times, causing multiple NMIs to be generated. if !self.ctrl.nmi_enabled { self.nmi_pending = false; } else if self.status.in_vblank { trace!( "$2000 NMI During VBL - PPU:{:3},{:3}", self.cycle, self.scanline ); self.nmi_pending = true; } } // $2001 | RW | PPUMASK // | 0 | Unknown (???) // | 1 | BG Mask, 0 = don't show background in left 8 columns // | 2 | Sprite Mask, 0 = don't show sprites in left 8 columns // | 3 | BG Switch, 1 = show background, 0 = hide background // | 4 | Sprites Switch, 1 = show sprites, 0 = hide sprites // | 5-7 | Unknown (???) #[inline(always)] pub fn write_mask(&mut self, val: u8) { self.open_bus = val; if self.reset_signal { return; } self.mask.write(val); // MMC5 tracks changes to PPUMASK self.mapper.ppu_write(0x2001, val); } // $2002 | R | PPUSTATUS // | 0-5 | Unknown (???) // | 6 | Sprite0 Hit Flag, 1 = PPU rendering has hit sprite #0 // | | This flag resets to 0 when VBlank starts, or CPU reads $2002 // | 7 | VBlank Flag, 1 = PPU is generating a Vertical Blanking Impulse // | | This flag resets to 0 when VBlank ends, or CPU reads $2002 pub fn read_status(&mut self) -> u8 { let status = self.peek_status(); // Top three bits ignored for open bus self.open_bus |= status & 0xE0; if self.nmi_pending { trace!("$2002 NMI Ack - PPU:{:3},{:3}", self.cycle, self.scanline); } self.nmi_pending = false; self.status.reset_in_vblank(); self.scroll.reset_latch(); if self.scanline == self.vblank_scanline && self.cycle == cycle::START { // Reading PPUSTATUS one clock before the start of vertical blank will read as clear // and never set the flag or generate an NMI for that frame trace!( "$2002 Prevent VBL - PPU:{:3},{:3}", self.cycle, self.scanline ); self.prevent_vbl = true; } status } // $2002 | R | PPUSTATUS // | 0-5 | Unknown (???) // | 6 | Sprite0 Hit Flag, 1 = PPU rendering has hit sprite #0 // | | This flag resets to 0 when VBlank starts, or CPU reads $2002 // | 7 | VBlank Flag, 1 = PPU is generating a Vertical Blanking Impulse // | | This flag resets to 0 when VBlank ends, or CPU reads $2002 // // Non-mutating version of `read_status`. #[inline(always)] pub const fn peek_status(&self) -> u8 { // Only upper 3 bits are connected for this register (self.status.read() & 0xE0) | (self.open_bus & 0x1F) } // $2003 | W | OAMADDR // | | Used to set the address in the 256-byte Sprite Memory to be // | | accessed via $2004. This address will increment by 1 after // | | each access to $2004. The Sprite Memory contains coordinates, // | | colors, and other attributes of the sprites. #[inline(always)] pub const fn write_oamaddr(&mut self, val: u8) { self.open_bus = val; self.oamaddr = val; } // $2004 | RW | OAMDATA // | | Used to read the Sprite Memory. The address is set via // | | $2003 and increments after each access. The Sprite Memory // | | contains coordinates, colors, and other attributes of the // | | sprites. #[inline(always)] pub fn read_oamdata(&mut self) -> u8 { self.open_bus = self.peek_oamdata(); self.open_bus } // $2004 | RW | OAMDATA // | | Used to read the Sprite Memory. The address is set via // | | $2003 and increments after each access. The Sprite Memory // | | contains coordinates, colors, and other attributes of the // | | sprites. // Non-mutating version of `read_oamdata`. #[inline(always)] pub fn peek_oamdata(&self) -> u8 { // Reading OAMDATA during rendering will expose OAM accesses during sprite evaluation and loading if self.scanline <= scanline::VISIBLE_END && self.mask.rendering_enabled && cycle::SPR_FETCH_RANGE.contains(&self.cycle) { self.secondary_oamdata[self.secondary_oamaddr as usize] } else { self.oamdata[self.oamaddr as usize] } } // $2004 | RW | OAMDATA // | | Used to write the Sprite Memory. The address is set via // | | $2003 and increments after each access. The Sprite Memory // | | contains coordinates, colors, and other attributes of the // | | sprites. pub fn write_oamdata(&mut self, mut val: u8) { self.open_bus = val; if self.mask.rendering_enabled && (self.is_visible_scanline || self.is_prerender_scanline || self.is_pal_spr_eval_scanline) { // https://www.nesdev.org/wiki/PPU_registers#OAMDATA // Writes to OAMDATA during rendering do not modify values, but do perform a glitch // increment of OAMADDR, bumping only the high 6 bits self.oamaddr = self.oamaddr.wrapping_add(4); } else { if self.oamaddr & 0x03 == 0x02 { // Bits 2-4 of sprite attr (byte 2) are unimplemented and always read back as 0 val &= 0xE3; } self.oamdata[self.oamaddr as usize] = val; self.oamaddr = self.oamaddr.wrapping_add(1); } } // $2005 | W | PPUSCROLL // | | There are two scroll registers, vertical and horizontal, // | | which are both written via this port. The first value written // | | will go into the Vertical Scroll Register (unless it is >239, // | | then it will be ignored). The second value will appear in the // | | Horizontal Scroll Register. The Name Tables are assumed to be // | | arranged in the following way: // | | // | | +-----------+-----------+ // | | | 2 ($2800) | 3 ($2C00) | // | | +-----------+-----------+ // | | | 0 ($2000) | 1 ($2400) | // | | +-----------+-----------+ // | | // | | When scrolled, the picture may span over several Name Tables. // | | Remember, though, that because of the mirroring, there are // | | only 2 real Name Tables, not 4. #[inline(always)] pub fn write_scroll(&mut self, val: u8) { self.open_bus = val; if self.reset_signal { return; } self.scroll.write(val); } // $2006 | W | PPUADDR #[inline(always)] pub fn write_addr(&mut self, val: u8) { self.open_bus = val; if self.reset_signal { return; } self.scroll.write_addr(val); } // $2007 | RW | PPUDATA pub fn read_data(&mut self) -> u8 { let addr = self.scroll.addr(); self.increment_vram_addr(); // Buffering quirk resulting in a dummy read for the CPU // for reading pre-palette data in $0000 - $3EFF let prev_open_bus = self.open_bus; let val = self.bus_read(addr); // MMC3 clocks using A12 self.mapper.ppu_read(self.scroll.addr()); self.open_bus = if addr < addr::PALETTE_START { let buffer = self.vram_buffer; self.vram_buffer = val; buffer } else { // Set internal buffer with mirrors of nametable when reading palettes // Since we're reading from > $3EFF subtract $1000 to fill // buffer with nametable mirror data self.vram_buffer = self.bus_read(addr - 0x1000); // Hi 2 bits of palette should be open bus val | (prev_open_bus & 0xC0) }; trace!( "PPU $2007 read: {:02X} - PPU:{:3},{:3}", self.open_bus, self.cycle, self.scanline ); self.open_bus } // $2007 | RW | PPUDATA // // Non-mutating version of `read_data`. pub fn peek_data(&self) -> u8 { let addr = self.scroll.addr(); if addr < addr::PALETTE_START { self.vram_buffer } else { // Since we're reading from > $3EFF subtract $1000 // Hi 2 bits of palette should be open bus self.bus_peek(addr - 0x1000) | (self.open_bus & 0xC0) } } // $2007 | RW | PPUDATA pub fn write_data(&mut self, val: u8) { let addr = self.scroll.addr(); trace!( "PPU $2007 write: ${addr:04X} -> {val:02X} - PPU:{:3},{:3}", self.cycle, self.scanline ); self.increment_vram_addr(); self.bus_write(addr, val); // MMC3 clocks using A12 self.mapper.ppu_read(self.scroll.addr()); } } impl Clock for Ppu { fn clock(&mut self) { // === SCANLINE TRANSITION (cycle 340) === if self.cycle >= cycle::END { self.cycle = 0; self.scanline += 1; // === POST-RENDER (240/261) === match self.scanline { s if s == self.vblank_scanline - 1 => { self.frame.increment(); } s if s > self.prerender_scanline => { // Wrap scanline back to 0 self.scanline = 0; // Force prerender scanline sprite fetches to load the dummy $FF tiles (fixes // shaking in Ninja Gaiden 3 stage 1 after beating boss) self.spr_count = 0; } _ => (), } self.is_visible_scanline = self.scanline <= scanline::VISIBLE_END; self.is_prerender_scanline = self.scanline == self.prerender_scanline; self.is_render_scanline = self.is_visible_scanline | self.is_prerender_scanline; // PAL refreshes OAM later due to extended vblank to avoid OAM decay self.is_pal_spr_eval_scanline = self.region.is_pal() && self.scanline >= self.vblank_scanline + 24; if self.scanline == self.debugger.scanline && self.cycle == self.debugger.cycle { (*self.debugger.callback)(self.snapshot()); } return; } self.cycle += 1; // === RENDER LINE (scanlins 0-239, 261) === if self.mask.rendering_enabled { if self.is_render_scanline { if self.cycle <= cycle::VISIBLE_END { if self.is_visible_scanline { self.spr_eval_cycle(); } self.bg_fetch_cycle(); if self.is_prerender_scanline && self.cycle <= 8 && self.oamaddr >= 0x08 { // If OAMADDR is not less than eight when rendering starts, the eight bytes // starting at OAMADDR & 0xF8 are copied to the first eight bytes of OAM let addr = (self.cycle as usize) - 1; let oamindex = (self.oamaddr as usize & 0xF8) + addr; self.oamdata[addr] = self.oamdata[oamindex]; } } else if self.cycle <= cycle::SPR_FETCH_END { if self.mask.prev_rendering_enabled && self.cycle == cycle::SPR_FETCH_START { // Copy X bits at the start of a new line since we're going to start writing // new x values to t self.scroll.copy_x(); self.spr_present = ConstArray::new(); } // 280..=304 if self.is_prerender_scanline && cycle::COPY_Y_RANGE.contains(&self.cycle) { // Y scroll bits are supposed to be reloaded during this pixel range of PRERENDER // if rendering is enabled // https://wiki.nesdev.org/w/index.php/PPU_rendering#Pre-render_scanline_.28-1.2C_261.29 self.scroll.copy_y(); } self.spr_fetch_cycle(); } else { // 336 if self.cycle <= cycle::BG_PREFETCH_END { self.bg_fetch_cycle(); } else { // 337..=340 self.fetch_bg_nt_byte(); } self.oam_fetch = self.secondary_oamdata[0]; if self.region.is_ntsc() && self.is_prerender_scanline && self.cycle == cycle::ODD_SKIP && self.frame.is_odd() { // NTSC behavior while rendering - each odd PPU frame is one clock shorter // (skipping from 339 over 340 to 0) trace!( "Skipped odd frame cycle: {} - PPU:{:3},{:3}", self.frame_number(), self.cycle, self.scanline ); self.cycle = cycle::END; } } } else if self.is_pal_spr_eval_scanline { self.spr_eval_cycle(); // 257..=320 if cycle::SPR_FETCH_RANGE.contains(&self.cycle) { self.write_oamaddr(0x00); } } } self.mask.clock(); if self.scroll.delayed_update() && (!self.mask.rendering_enabled || self.scanline > scanline::VISIBLE_END) { // MMC3 clocks using A12 self.mapper.ppu_read(self.scroll.addr()); } // Pixels should be put even if rendering is disabled, as this is what blanks out the // screen. Rendering disabled just means we don't evaluate/read bg/sprite info if self.is_visible_scanline && self.cycle <= cycle::VISIBLE_END { if self.skip_rendering { self.headless_sprite_zero_hit(); } else { self.render_pixel(); } } if self.cycle <= cycle::VISIBLE_END || cycle::BG_PREFETCH_RANGE.contains(&self.cycle) { self.tile_shift_lo <<= 1; self.tile_shift_hi <<= 1; } // === VBLANK / IDLE === if self.scanline == self.vblank_scanline && self.cycle == cycle::VBLANK { self.start_vblank(); } else if self.is_prerender_scanline && self.cycle == cycle::VBLANK { self.stop_vblank(); } if self.scanline == self.debugger.scanline && self.cycle == self.debugger.cycle { (*self.debugger.callback)(self.snapshot()); } } } impl Regional for Ppu { fn region(&self) -> NesRegion { self.region } fn set_region(&mut self, region: NesRegion) { // https://www.nesdev.org/wiki/Cycle_reference_chart let (clock_divider, vblank_scanline, prerender_scanline) = match region { NesRegion::Auto | NesRegion::Ntsc => ( cycle::DIVIDER_NTSC, scanline::VBLANK_NTSC, scanline::PRERENDER_NTSC, ), NesRegion::Pal => ( cycle::DIVIDER_PAL, scanline::VBLANK_PAL, scanline::PRERENDER_PAL, ), NesRegion::Dendy => ( cycle::DIVIDER_DENDY, scanline::VBLANK_DENDY, scanline::PRERENDER_DENDY, ), }; self.region = region; self.clock_divider = clock_divider; self.vblank_scanline = vblank_scanline; self.prerender_scanline = prerender_scanline; self.mask.set_region(region); } } impl Reset for Ppu { fn reset(&mut self, kind: ResetKind) { self.master_clock = 0; self.cycle = 0; self.scanline = 0; self.is_visible_scanline = true; self.is_prerender_scanline = false; self.is_render_scanline = true; self.is_pal_spr_eval_scanline = false; self.open_bus = 0x00; self.mask.reset(kind); self.scroll.reset(kind); self.ctrl.reset(kind); self.mapper.reset(kind); self.status.reset(kind); self.nmi_pending = false; self.oam_fetch = 0x00; self.oam_eval_done = false; self.secondary_oamaddr = 0x0000; self.overflow_count = 0; self.spr_in_range = false; self.spr_zero_in_range = false; self.spr_zero_visible = false; self.spr_count = 0; self.vram_buffer = 0x00; if kind == ResetKind::Hard { self.oamaddr = 0x0000; self.oamdata = ConstArray::new(); } else { self.reset_signal = self.emulate_warmup; } *self.sprites = [Sprite::new(); 8]; self.spr_present = ConstArray::new(); self.prevent_vbl = false; self.frame.reset(kind); } } #[cfg(test)] mod tests { use super::*; use crate::{ cart::Cart, mapper::{Mmc1Revision, Sxrom}, mem::Memory, }; #[test] fn ciram_mirror_horizontal() { assert_eq!(CIRam::mirror(0x2000, Mirroring::Horizontal), 0x0000); assert_eq!(CIRam::mirror(0x2005, Mirroring::Horizontal), 0x0005); assert_eq!(CIRam::mirror(0x23FF, Mirroring::Horizontal), 0x03FF); assert_eq!(CIRam::mirror(0x2400, Mirroring::Horizontal), 0x0000); assert_eq!(CIRam::mirror(0x2405, Mirroring::Horizontal), 0x0005); assert_eq!(CIRam::mirror(0x27FF, Mirroring::Horizontal), 0x03FF); assert_eq!(CIRam::mirror(0x2800, Mirroring::Horizontal), 0x0400); assert_eq!(CIRam::mirror(0x2805, Mirroring::Horizontal), 0x0405); assert_eq!(CIRam::mirror(0x2BFF, Mirroring::Horizontal), 0x07FF); assert_eq!(CIRam::mirror(0x2C00, Mirroring::Horizontal), 0x0400); assert_eq!(CIRam::mirror(0x2C05, Mirroring::Horizontal), 0x0405); assert_eq!(CIRam::mirror(0x2FFF, Mirroring::Horizontal), 0x07FF); } #[test] fn ciram_mirror_vertical() { assert_eq!(CIRam::mirror(0x2000, Mirroring::Vertical), 0x0000); assert_eq!(CIRam::mirror(0x2005, Mirroring::Vertical), 0x0005); assert_eq!(CIRam::mirror(0x23FF, Mirroring::Vertical), 0x03FF); assert_eq!(CIRam::mirror(0x2800, Mirroring::Vertical), 0x0000); assert_eq!(CIRam::mirror(0x2805, Mirroring::Vertical), 0x0005); assert_eq!(CIRam::mirror(0x2BFF, Mirroring::Vertical), 0x03FF); assert_eq!(CIRam::mirror(0x2400, Mirroring::Vertical), 0x0400); assert_eq!(CIRam::mirror(0x2405, Mirroring::Vertical), 0x0405); assert_eq!(CIRam::mirror(0x27FF, Mirroring::Vertical), 0x07FF); assert_eq!(CIRam::mirror(0x2C00, Mirroring::Vertical), 0x0400); assert_eq!(CIRam::mirror(0x2C05, Mirroring::Vertical), 0x0405); assert_eq!(CIRam::mirror(0x2FFF, Mirroring::Vertical), 0x07FF); } #[test] fn ciram_mirror_single_screen_a() { assert_eq!(CIRam::mirror(0x2000, Mirroring::SingleScreenA), 0x0000); assert_eq!(CIRam::mirror(0x2005, Mirroring::SingleScreenA), 0x0005); assert_eq!(CIRam::mirror(0x23FF, Mirroring::SingleScreenA), 0x03FF); assert_eq!(CIRam::mirror(0x2800, Mirroring::SingleScreenA), 0x0000); assert_eq!(CIRam::mirror(0x2805, Mirroring::SingleScreenA), 0x0005); assert_eq!(CIRam::mirror(0x2BFF, Mirroring::SingleScreenA), 0x03FF); assert_eq!(CIRam::mirror(0x2400, Mirroring::SingleScreenA), 0x0000); assert_eq!(CIRam::mirror(0x2405, Mirroring::SingleScreenA), 0x0005); assert_eq!(CIRam::mirror(0x27FF, Mirroring::SingleScreenA), 0x03FF); assert_eq!(CIRam::mirror(0x2C00, Mirroring::SingleScreenA), 0x0000); assert_eq!(CIRam::mirror(0x2C05, Mirroring::SingleScreenA), 0x0005); assert_eq!(CIRam::mirror(0x2FFF, Mirroring::SingleScreenA), 0x03FF); } #[test] fn ciram_mirror_single_screen_b() { assert_eq!(CIRam::mirror(0x2000, Mirroring::SingleScreenB), 0x0400); assert_eq!(CIRam::mirror(0x2005, Mirroring::SingleScreenB), 0x0405); assert_eq!(CIRam::mirror(0x23FF, Mirroring::SingleScreenB), 0x07FF); assert_eq!(CIRam::mirror(0x2800, Mirroring::SingleScreenB), 0x0400); assert_eq!(CIRam::mirror(0x2805, Mirroring::SingleScreenB), 0x0405); assert_eq!(CIRam::mirror(0x2BFF, Mirroring::SingleScreenB), 0x07FF); assert_eq!(CIRam::mirror(0x2400, Mirroring::SingleScreenB), 0x0400); assert_eq!(CIRam::mirror(0x2405, Mirroring::SingleScreenB), 0x0405); assert_eq!(CIRam::mirror(0x27FF, Mirroring::SingleScreenB), 0x07FF); assert_eq!(CIRam::mirror(0x2C00, Mirroring::SingleScreenB), 0x0400); assert_eq!(CIRam::mirror(0x2C05, Mirroring::SingleScreenB), 0x0405); assert_eq!(CIRam::mirror(0x2FFF, Mirroring::SingleScreenB), 0x07FF); } #[test] fn vram_writes() { let mut ppu = Ppu::default(); ppu.write_addr(0x23); ppu.write_addr(0x05); // PPU writes to $2006 are delayed by 2 PPU clocks ppu.clock(); ppu.clock(); ppu.write_data(0x66); // write to $2305 assert_eq!(ppu.chr_read(0x2305), 0x66); } #[test] fn vram_reads() { let mut ppu = Ppu::default(); ppu.write_ctrl(0x00); ppu.bus_write(0x2305, 0x66); ppu.write_addr(0x23); ppu.write_addr(0x05); // PPU writes to $2006 are delayed by 2 PPU clocks ppu.clock(); ppu.clock(); ppu.read_data(); // buffer read assert_eq!(ppu.scroll.addr(), 0x2306); assert_eq!(ppu.read_data(), 0x66); assert_eq!(ppu.scroll.addr(), 0x2307); } #[test] fn vram_read_pagecross() { let mut ppu = Ppu::default(); ppu.write_ctrl(0x00); ppu.bus_write(0x21FF, 0x66); ppu.bus_write(0x2200, 0x77); ppu.write_addr(0x21); ppu.write_addr(0xFF); // PPU writes to $2006 are delayed by 2 PPU clocks ppu.clock(); ppu.clock(); ppu.read_data(); // buffer read assert_eq!(ppu.read_data(), 0x66); assert_eq!(ppu.read_data(), 0x77); } #[test] fn vram_read_vertical_increment() { let mut ppu = Ppu::default(); ppu.write_ctrl(0b100); ppu.bus_write(0x21FF, 0x66); ppu.bus_write(0x21FF + 32, 0x77); ppu.bus_write(0x21FF + 64, 0x88); ppu.write_addr(0x21); ppu.write_addr(0xFF); // PPU writes to $2006 are delayed by 2 PPU clocks ppu.clock(); ppu.clock(); ppu.read_data(); // buffer read assert_eq!(ppu.read_data(), 0x66); assert_eq!(ppu.read_data(), 0x77); assert_eq!(ppu.read_data(), 0x88); } // Horizontal: https://wiki.nesdev.org/w/index.php/Mirroring // [0x2000 A ] [0x2400 a ] // [0x2800 B ] [0x2C00 b ] #[test] fn vram_horizontal_mirror() { let mut ppu = Ppu::default(); ppu.write_addr(0x24); ppu.write_addr(0x05); // PPU writes to $2006 are delayed by 2 PPU clocks ppu.clock(); ppu.clock(); ppu.write_data(0x66); // write to a at $2405 ppu.write_addr(0x28); ppu.write_addr(0x05); // PPU writes to $2006 are delayed by 2 PPU clocks ppu.clock(); ppu.clock(); ppu.write_data(0x77); // write to B at $2805 ppu.write_addr(0x20); ppu.write_addr(0x05); // PPU writes to $2006 are delayed by 2 PPU clocks ppu.clock(); ppu.clock(); ppu.read_data(); // buffer read assert_eq!(ppu.read_data(), 0x66); // read A from $2005 ppu.write_addr(0x2C); ppu.write_addr(0x05); // PPU writes to $2006 are delayed by 2 PPU clocks ppu.clock(); ppu.clock(); ppu.read_data(); // buffer read assert_eq!(ppu.read_data(), 0x77); // read b from $2C05 } // Vertical: https://wiki.nesdev.org/w/index.php/Mirroring // [0x2000 A ] [0x2400 B ] // [0x2800 a ] [0x2C00 b ] #[test] fn vram_vertical_mirror() { let mut ppu = Ppu::default(); let mut cart = Cart::default(); cart.mapper = Sxrom::load( &cart, Memory::new(0x2000), Memory::new(0x4000), Mmc1Revision::BC, ) .unwrap(); // Set vertical mirroring mode via 5 writes let mut val = 0b00_00_00_01_00; for _ in 0..5 { cart.mapper.prg_write(0x8000, val & 0b11); cart.mapper.clock(); cart.mapper.clock(); val >>= 2; } ppu.load_mapper(cart.mapper); ppu.write_addr(0x20); ppu.write_addr(0x05); // PPU writes to $2006 are delayed by 2 PPU clocks ppu.clock(); ppu.clock(); ppu.write_data(0x66); // write to A at $2005 ppu.write_addr(0x2C); ppu.write_addr(0x05); // PPU writes to $2006 are delayed by 2 PPU clocks ppu.clock(); ppu.clock(); ppu.write_data(0x77); // write to b at $2C05 ppu.write_addr(0x28); ppu.write_addr(0x05); // PPU writes to $2006 are delayed by 2 PPU clocks ppu.clock(); ppu.clock(); ppu.read_data(); // buffer read assert_eq!(ppu.read_data(), 0x66); // read a from $2805 ppu.write_addr(0x24); ppu.write_addr(0x05); // PPU writes to $2006 are delayed by 2 PPU clocks ppu.clock(); ppu.clock(); ppu.read_data(); // buffer read assert_eq!(ppu.read_data(), 0x77); // read B from $2405 } #[test] fn read_status_resets_latch() { let mut ppu = Ppu::default(); ppu.bus_write(0x2305, 0x66); ppu.write_addr(0x21); ppu.write_addr(0x23); // PPU writes to $2006 are delayed by 2 PPU clocks ppu.clock(); ppu.clock(); ppu.write_addr(0x05); ppu.read_data(); // buffer read assert_ne!(ppu.read_data(), 0x66); ppu.read_status(); ppu.write_addr(0x23); ppu.write_addr(0x05); // PPU writes to $2006 are delayed by 2 PPU clocks ppu.clock(); ppu.clock(); ppu.read_data(); // buffer read assert_eq!(ppu.read_data(), 0x66); } #[test] fn vram_mirroring() { let mut ppu = Ppu::default(); ppu.write_ctrl(0); ppu.bus_write(0x2305, 0x66); ppu.write_addr(0x63); // 0x6305 mirrors to 0x2305 ppu.write_addr(0x05); // PPU writes to $2006 are delayed by 2 PPU clocks ppu.clock(); ppu.clock(); ppu.read_data(); // buffer read assert_eq!(ppu.scroll.addr(), 0x2306); assert_eq!(ppu.read_data(), 0x66); assert_eq!(ppu.scroll.addr(), 0x2307); } #[test] fn read_status_resets_vblank() { let mut ppu = Ppu::default(); ppu.status.set_in_vblank(true); let status = ppu.read_status(); assert_eq!(status >> 7, 1); assert_eq!(ppu.status.read() >> 7, 0); } #[test] fn sprite_zero_hit_headless_visible_cycle() { let mut ppu = Ppu::default(); ppu.write_mask(0x18); ppu.skip_rendering = true; ppu.scanline = 0; ppu.cycle = 10; ppu.scroll.fine_x = 0; ppu.tile_shift_lo = 0x8000; ppu.tile_shift_hi = 0x0000; ppu.spr_zero_visible = true; ppu.spr_present[9..17].fill(true); ppu.sprites[0].x = 8; ppu.sprites[0].tile_lo = 0b0100; ppu.sprites[0].tile_hi = 0b0000; ppu.sprites[0].flip_horizontal = true; ppu.sprites[0].bg_priority = false; ppu.clock(); assert!(ppu.status.spr_zero_hit); } #[test] fn oam_read_write() { let mut ppu = Ppu::default(); ppu.write_oamaddr(0x10); ppu.write_oamdata(0x66); ppu.write_oamdata(0x77); ppu.write_oamaddr(0x10); assert_eq!(ppu.read_oamdata(), 0x66); ppu.write_oamaddr(0x11); assert_eq!(ppu.read_oamdata(), 0x77); } } ================================================ FILE: tetanes-core/src/sys/fs/os.rs ================================================ //! OS-specific filesystem operations. use crate::fs::{Error, Result}; use std::{ fs::{File, create_dir_all, remove_dir_all}, io::{Read, Write}, path::Path, }; pub fn writer_impl(path: impl AsRef) -> Result { let path = path.as_ref(); let Some(directory) = path.parent() else { return Err(Error::InvalidPath(path.to_path_buf())); }; if !directory.exists() { create_dir_all(directory) .map_err(|err| Error::io(err, format!("failed to create directory {directory:?}")))?; } File::create(path) .map_err(|source| Error::io(source, format!("failed to create file {path:?}"))) } pub fn reader_impl(path: impl AsRef) -> Result { let path = path.as_ref(); File::open(path).map_err(|source| Error::io(source, format!("failed to open file {path:?}"))) } pub fn clear_dir_impl(path: impl AsRef) -> Result<()> { let path = path.as_ref(); if !path.exists() { return Ok(()); } remove_dir_all(path) .map_err(|source| Error::io(source, format!("failed to remove directory {path:?}"))) } pub fn exists_impl(path: impl AsRef) -> bool { let path = path.as_ref(); path.exists() } ================================================ FILE: tetanes-core/src/sys/fs/wasm.rs ================================================ //! Web-specific filesystem operations. use crate::fs::{Error, Result}; use std::{ io::{self, Read, Write}, mem, path::{Path, PathBuf}, }; use web_sys::js_sys; #[derive(Debug)] #[must_use] pub struct StoreWriter { path: PathBuf, data: Vec, } pub struct StoreReader { cursor: io::Cursor>, } pub fn local_storage() -> Result { let window = web_sys::window().ok_or_else(|| Error::custom("failed to get js window"))?; window .local_storage() .map_err(|err| { tracing::error!("failed to get local storage: {err:?}"); Error::custom("failed to get storage") })? .ok_or_else(|| Error::custom("no storage available")) } impl Write for StoreWriter { fn write(&mut self, buf: &[u8]) -> io::Result { self.data.extend_from_slice(buf); Ok(buf.len()) } fn flush(&mut self) -> io::Result<()> { let local_storage = local_storage().map_err(io::Error::other)?; let key = self.path.to_string_lossy(); let data = mem::take(&mut self.data); let value = match serde_json::to_string(&data) { Ok(value) => value, Err(err) => { self.data = data; tracing::error!("failed to serialize data: {err:?}"); return Err(io::Error::other("failed to serialize data")); } }; if let Err(err) = local_storage.set_item(&key, &value) { self.data = data; tracing::error!("failed to store data in local storage: {err:?}"); return Err(io::Error::other("failed to write data")); } Ok(()) } } impl Read for StoreReader { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.cursor.read(buf) } } pub fn writer_impl(path: impl AsRef) -> Result { let path = path.as_ref(); Ok(StoreWriter { path: path.to_path_buf(), data: Vec::new(), }) } pub fn reader_impl(path: impl AsRef) -> Result { let path = path.as_ref(); let local_storage = local_storage()?; let key = path.to_string_lossy().into_owned(); let data = local_storage .get_item(&key) .map_err(|_| Error::custom("failed to find data for {key}"))? .map(|value| { serde_json::from_str(&value).map_err(|err| { tracing::error!("failed to deserialize data: {err:?}"); Error::custom("failed to deserialize data") }) }) .unwrap_or_else(|| Ok(Vec::new()))?; Ok(StoreReader { cursor: io::Cursor::new(data), }) } pub fn clear_dir_impl(path: impl AsRef) -> Result<()> { let path = path.as_ref().to_string_lossy(); let local_storage = local_storage()?; for key in js_sys::Object::keys(&local_storage) .iter() .filter_map(|key| key.as_string()) .filter(|key| key.starts_with(&*path)) { let _ = local_storage.remove_item(&key); } Ok(()) } pub fn exists_impl(path: impl AsRef) -> bool { let path = path.as_ref(); let Ok(local_storage) = local_storage() else { return false; }; let key = path.to_string_lossy(); matches!(local_storage.get_item(&key), Ok(Some(_))) } ================================================ FILE: tetanes-core/src/sys/fs.rs ================================================ //! Platform-specific filesystem methods. use cfg_if::cfg_if; cfg_if! { if #[cfg(target_arch = "wasm32")] { mod wasm; pub use wasm::*; } else { mod os; pub use os::*; } } ================================================ FILE: tetanes-core/src/sys/time.rs ================================================ //! Platform-specific time and date methods. use cfg_if::cfg_if; cfg_if! { if #[cfg(target_arch = "wasm32")] { pub use web_time::{Duration, Instant}; } else { pub use std::time::{Duration, Instant}; } } ================================================ FILE: tetanes-core/src/sys.rs ================================================ //! System-specific modules. pub mod fs; pub mod time; ================================================ FILE: tetanes-core/src/time.rs ================================================ //! Time and Date methods. pub use crate::sys::time::*; ================================================ FILE: tetanes-core/src/video.rs ================================================ //! Video output and filtering. use crate::ppu::{self, Ppu}; use serde::{Deserialize, Serialize}; use std::{ f64::consts::PI, ops::{Deref, DerefMut}, sync::OnceLock, }; use thiserror::Error; #[derive(Error, Debug)] #[must_use] #[error("failed to parse `VideoFilter`")] pub struct ParseVideoFilterError; #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[must_use] pub enum VideoFilter { Pixellate, #[default] Ntsc, } impl VideoFilter { pub const fn as_slice() -> &'static [Self] { &[Self::Pixellate, Self::Ntsc] } } impl AsRef for VideoFilter { fn as_ref(&self) -> &str { match self { Self::Pixellate => "Pixellate", Self::Ntsc => "NTSC", } } } impl TryFrom for VideoFilter { type Error = ParseVideoFilterError; fn try_from(value: usize) -> Result { Ok(match value { 0 => Self::Pixellate, 1 => Self::Ntsc, _ => return Err(ParseVideoFilterError), }) } } #[derive(Debug, Clone)] #[must_use] pub struct Frame(Vec); impl Frame { pub const SIZE: usize = ppu::size::FRAME * 4; /// Allocate a new frame for video output. pub fn new() -> Self { Self( [(); Self::SIZE / 4] .into_iter() .flat_map(|_| [0, 0, 0, 255]) .collect(), ) } } impl Default for Frame { fn default() -> Self { Self::new() } } impl Deref for Frame { type Target = Vec; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for Frame { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } #[derive(Clone)] #[must_use] pub struct Video { pub filter: VideoFilter, pub frame: Frame, } impl Default for Video { fn default() -> Self { Self::new() } } impl Video { /// Create a new Video decoder with the default filter. pub fn new() -> Self { Self::with_filter(VideoFilter::default()) } /// Create a new Video encoder with a filter. pub fn with_filter(filter: VideoFilter) -> Self { Self { filter, frame: Frame::new(), } } /// Applies the given filter to the given video buffer and returns the result. pub fn apply_filter(&mut self, buffer: &[u16], frame_number: u32) -> &[u8] { match self.filter { VideoFilter::Pixellate => Self::decode_buffer(buffer, &mut self.frame), VideoFilter::Ntsc => Self::apply_ntsc_filter(buffer, frame_number, &mut self.frame), } &self.frame } /// Applies the given filter to the given video buffer by coping into the provided buffer. pub fn apply_filter_into(&self, buffer: &[u16], frame_number: u32, output: &mut [u8]) { match self.filter { VideoFilter::Pixellate => Self::decode_buffer(buffer, output), VideoFilter::Ntsc => Self::apply_ntsc_filter(buffer, frame_number, output), } } /// Fills a fully rendered frame with RGB colors. pub fn decode_buffer(buffer: &[u16], output: &mut [u8]) { for (color, pixels) in buffer.iter().zip(output.chunks_exact_mut(4)) { let index = (*color as usize) * 3; assert!(Ppu::NTSC_PALETTE.len() > index + 2); assert!(pixels.len() > 2); pixels[0] = Ppu::NTSC_PALETTE[index]; pixels[1] = Ppu::NTSC_PALETTE[index + 1]; pixels[2] = Ppu::NTSC_PALETTE[index + 2]; } } /// Applies the NTSC filter to the given video buffer. /// /// Amazing implementation Bisqwit! Much faster than my original, but boy what a pain /// to translate it to Rust /// Source: /// See also: pub fn apply_ntsc_filter(buffer: &[u16], frame_number: u32, output: &mut [u8]) { let mut prev_color = 0; for (idx, (color, pixels)) in buffer.iter().zip(output.chunks_exact_mut(4)).enumerate() { let x = idx % 256; let rgba = if x == 0 { // Remove pixel 0 artifact from not having a valid previous pixel 0 } else { let y = idx / 256; let even_phase = if frame_number & 0x01 == 0x01 { 0 } else { 1 }; let phase = (2 + y * 341 + x + even_phase) % 3; NTSC_PALETTE.get_or_init(generate_ntsc_palette) [phase + ((prev_color & 0x3F) as usize) * 3 + (*color as usize) * 3 * 64] }; prev_color = u32::from(*color); assert!(pixels.len() > 2); pixels[0] = ((rgba >> 16) & 0xFF) as u8; pixels[1] = ((rgba >> 8) & 0xFF) as u8; pixels[2] = (rgba & 0xFF) as u8; // Alpha should always be 255 } } } impl std::fmt::Debug for Video { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Video") .field("filter", &self.filter) .finish() } } pub static NTSC_PALETTE: OnceLock> = OnceLock::new(); fn generate_ntsc_palette() -> Vec { // NOTE: There's lot's to clean up here -- too many magic numbers and duplication but // I'm afraid to touch it now that it works // Source: https://bisqwit.iki.fi/jutut/kuvat/programming_examples/nesemu1/nesemu1.cc // https://wiki.nesdev.org/w/index.php/NTSC_video // Calculate the luma and chroma by emulating the relevant circuits: const VOLTAGES: [i32; 16] = [ -6, -69, 26, -59, 29, -55, 73, -40, 68, -17, 125, 11, 68, 33, 125, 78, ]; let mut ntsc_palette = vec![0; 512 * 64 * 3]; // Helper functions for converting YIQ to RGB let gamma = 1.8; // Assumed display gamma let gammafix = |color: f64| { if color <= 0.0 { 0.0 } else { color.powf(2.2 / gamma) } }; let yiq_divider = f64::from(9 * 10u32.pow(6)); for palette_offset in 0..3 { for channel in 0..3 { for color0_offset in 0..512 { let emphasis = color0_offset / 64; for color1_offset in 0..64 { let mut y = 0; let mut i = 0; let mut q = 0; // 12 samples of NTSC signal constitute a color. for sample in 0..12 { let noise = (sample + palette_offset * 4) % 12; // Sample either the previous or the current pixel. // Use pixel=color0_offset to disable artifacts. let pixel = if noise < 5 - channel * 2 { color0_offset } else { color1_offset }; // Decode the color index. let chroma = pixel & 0x0F; // Forces luma to 0, 4, 8, or 12 for easy lookup let luma = if chroma < 0x0E { (pixel / 4) & 12 } else { 4 }; // NES NTSC modulator (square wave between up to four voltage levels): let limit = if (chroma + 8 + sample) % 12 < 6 { 12 } else { 0 }; let high = if chroma > limit { 1 } else { 0 }; let emp_effect = if (152_278 >> (sample / 2 * 3)) & emphasis > 0 { 0 } else { 2 }; let level = 40 + VOLTAGES[high + emp_effect + luma]; // Ideal TV NTSC demodulator: let (sin, cos) = (PI * sample as f64 / 6.0).sin_cos(); y += level; i += level * (cos * 5909.0) as i32; q += level * (sin * 5909.0) as i32; } // Store color at subpixel precision let y = f64::from(y) / 1980.0; let i = f64::from(i) / yiq_divider; let q = f64::from(q) / yiq_divider; let idx = palette_offset + color0_offset * 3 * 64 + color1_offset * 3; match channel { 2 => { let rgb = 255.0 * gammafix(q.mul_add(0.623_557, i.mul_add(0.946_882, y))); ntsc_palette[idx] += 0x10000 * rgb.clamp(0.0, 255.0) as u32; } 1 => { let rgb = 255.0 * gammafix(q.mul_add(-0.635_691, i.mul_add(-0.274_788, y))); ntsc_palette[idx] += 0x00100 * rgb.clamp(0.0, 255.0) as u32; } 0 => { let rgb = 255.0 * gammafix(q.mul_add(1.709_007, i.mul_add(-1.108_545, y))); ntsc_palette[idx] += rgb.clamp(0.0, 255.0) as u32; } _ => (), // invalid channel } } } } } ntsc_palette } ================================================ FILE: tetanes-core/test_roms/apu/blargg_readme.txt ================================================ NES APU Frame Counter Update ---------------------------- I have run more tests on the NES APU and come up with new information about the exact timing of the frame counter and length counter, and some subtle behavior. The information here either extends or contradicts what is stated in the NES APU reference and on the nesdev wiki. Not documented here is a delay when changing modes by writing to $4017. This is quite complex and I haven't fully worked out its exact operation. Once determined, documented, and tested, the information here should still be valid. This delay when changing modes involves the current mode running a few clocks before switching to the new mode, so it only affects the rare case where $4017 is written within a few clocks of a frame counter step. This delay does not cause the steps to occur any later than shown below; it only causes the first few clocks of the new mode to be transparent, allowing the previous mode to "show through". Also not documented is the exact operation of the envelope, sweep, and triangle's linear counter when register writes occur close to clocking. Refer to tests.txt for a description of the test ROMs included. I have not yet fully updated my APU emulator and tested it with this information, so report any problems you have with implementation. Shay (swap to e-mail) Clock Jitter ------------ Changes to the mode by writing to $4017 only occur on *even* internal APU clocks; if written on an odd clock, the first step of the mode is delayed by one clock. At power-up and reset, the APU is randomly in an odd or even cycle with respect to the first clock of the first instruction executed by the CPU. ; assume even APU and CPU clocks occur together lda #$00 sta $4017 ; mode begins in one clock sta <0 ; delay 3 clocks sta $4017 ; mode begins immediately Mode 0 Timing ------------- -5 lda #$00 -3 sta $4017 0 (write occurs here) 1 2 3 ... Step 1 7459 Clock linear ... Step 2 14915 Clock linear & length ... Step 3 22373 Clock linear ... Step 4 29830 Set frame irq 29831 Clock linear & length and set frame irq 29832 Set frame irq ... Step 1 37289 Clock linear ... etc. Mode 1 Timing ------------- -5 lda #$80 -3 sta $4017 0 (write occurs here) Step 0 1 Clock linear & length 2 ... Step 1 7459 Clock linear ... Step 2 14915 Clock linear & length ... Step 3 22373 Clock linear ... Step 4 29829 (do nothing) ... Step 0 37283 Clock linear & length ... etc. Length Halt ----------- Write to halt flag is delayed by one clock: $10->$4000 clear halt flag 0 $00->$4017 begin mode 0 14914 $30->$4000 set halt flag 14915 Length not clocked $10->$4000 clear halt flag 0 $00->$4017 begin mode 0 14915 $30->$4000 set halt flag Length clocked $30->$4000 set halt flag 0 $00->$4017 begin mode 0 14914 $10->$4000 clear halt flag 14915 Length clocked $30->$4000 set halt flag 0 $00->$4017 begin mode 0 14915 $10->$4000 clear halt flag Length not clocked Length Reload ------------- Length reload is completely ignored if written during length clocking and length counter is non-zero before clocking: $38->$4003 make length non-zero 0 $00->$4017 14914 Write to $4003 Length reloaded 14915 Length clocked $38->$4003 make length non-zero 0 $00->$4017 14915 Write to $4003 Length not reloaded Length clocked $00->$4015 clear length counter $01->$4015 0 $00->$4017 14915 Write to $4003 Length reloaded Length not clocked Misc ---- - The frame IRQ flag is cleared only when $4015 is read or $4017 is written with bit 6 set ($40 or $c0). - The IRQ handler is invoked at minimum 29833 clocks after writing $00 to $4017 (assuming the frame IRQ flag isn't already set, and nothing else generates an IRQ during that time). - After reset or power-up, APU acts as if $4017 were written with $00 from 9 to 12 clocks before first instruction begins. It is as if this occurs (this generates a 10 clock delay): lda #$00 sta $4017 ; 1 lda <0 ; 9 delay nop nop nop reset: ... - As shown, the frame irq flag is set three times in a row. Thus when polling it, always read $4015 an extra time after the flag is found to be set, to be sure it's clear afterwards, wait: bit $4015 ; V flag reflects frame IRQ flag bvc wait bit $4015 ; be sure irq flag is clear or better yet, clear it before polling it: bit $4015 ; clear flag first wait: bit $4015 ; V flag reflects frame IRQ flag bvc wait ================================================ FILE: tetanes-core/test_roms/apu/dpcmletterbox.txt ================================================ DPCM Letterbox This NES program demonstrates abusing the NTSC NES's sampled sound playback hardware as a scanline timer to split the screen twice without needing to use a mapper-generated IRQ. == How it works == The NES has sample playback hardware that can trigger when it finishes playing a differential pulse code modulated (DPCM) waveform. There are eight samples to a byte, and a waveform is 16n + 1 bytes long. There are sixteen valid rates for sample playback, numbered 0 to 15, and DPCM rate 15 has 54 CPU cycles per sample, or 54*3*8=1296 PPU dots per byte, or 3.8 scanlines per byte. But the time between NMI and the first sample data fetch drifts from frame to frame. So at the start of the frame, the program has to measure exactly how far apart the CPU and PPU are, and then the IRQ handler has to waste a corresponding amount of time before doing raster effects. This version has a slight visual artifact in the top overscan region because it uses sprite 0 as a timing reference so that it can be used even with an NMI handler whose execution time in CPU cycles varies. But a game with a cycle-timed NMI handler would not need sprite 0; it could use the end of the NMI handler as a reference. Reset handler: Set up screen data Enable IRQ Wait forever NMI handler: Set up sprite 0 hit at top of screen Disable sample playback Turn on background rendering at a fixed scroll position Wait for Sprite 0 off and on Turn off background rendering Clear number of elapsed IRQs Enable playback and IRQ Measure time until IRQ in 8-cycle units Convert this to an amount of time to waste Read the controllers Compute next scroll value Return IRQ handler: Add 1 to elapsed IRQs Restart sample playback If elapsed IRQs is at first threshold: Waste time in 8-cycle units Turn on background rendering If elapsed IRQs is at second threshold: Switch next sample playback to 17-byte mode If elapsed IRQs is at third threshold: Waste time in 8-cycle units Turn off background rendering == Legal == The following license applies to the source code, binary code, and this manual: Copyright 2010 Damian Yerrick Copying and distribution of this file, with or without modification, are permitted in any medium without royalty provided the copyright notice and this notice are preserved. This file is offered as-is, without any warranty. ================================================ FILE: tetanes-core/test_roms/apu/mixer.txt ================================================ NES APU Mixer Tests ------------------- These tests verify proper operation of the NES APU's sound channel mixer, including relative volumes of channels and non-linear mixing. Tests MUST be run from a freshly-powered NES, as this is the only way to ensure that the triangle wave doesn't interfere. All tests beep, play a test sound, then beep again. For all but the noise test, there should be near silence between the beeps. For the noise test, noise will fade in and out. There shouldn't be any noticeable tone when heard through a speaker (through headphones, faint tones might be audible). Internal operation ------------------ The tests have the channel under test generate a tone, then generate the inverse waveform using the DMC DAC, canceling to (near) silence if everything is correct. The DMC test verifies that non-linearity of the DMC DAC. The noise and triangle tests verify relative volume of the noise and triangle to the DMC, and that the DMC DAC affects attenuation of them properly. Finally, the square test verifies relative volume of the squares to the DMC, non-linearity of the square DACs, how one square affects the other (slightly), and that the square DAC non-linearity is separate from the DMC. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. -- Shay Green ================================================ FILE: tetanes-core/test_roms/apu/pal_readme.txt ================================================ PAL NES APU Tests ----------------- These tests verify the PAL APU's frame sequencer timing. They have been tested on a PAL NES and all give a passing result. Each .nes file runs several tests and reports the result on screen and by beeping a number of times. See below for the meaning of failure codes for each test. It's best to run the tests in order, because later tests depend on things tested by earlier tests and will give erroneous results if any earlier ones failed. Source code for each test is included, and most tests are clearly divided into sections. Support code is also included, but it runs on a custom devcart and assembler so it will require some effort to assemble. Contact me if you'd like assistance porting them to your setup. Frame sequencer timing ---------------------- See blargg_apu_2005.07.30 for more information about frame sequencer timing subtleties. This only lists timing differences. Mode 0: 4-step sequence Action Envelopes & Length Counter& Interrupt Delay to next Linear Counter Sweep Units Flag NTSC PAL - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $4017=$00 - - - 7459 8315 Step 1 Clock - - 7456 8314 Step 2 Clock Clock - 7458 8312 Step 3 Clock - - 7458 8314 Step 4 Clock Clock Set if enabled 7458 8314 Mode 1: 5-step sequence Action Envelopes & Length Counter& Interrupt Delay to next Linear Counter Sweep Units Flag NTSC PAL - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $4017=$80 - - - 1 1 Step 1 Clock Clock - 7458 8314 Step 2 Clock - - 7456 8314 Step 3 Clock Clock - 7458 8312 Step 4 Clock - - 7458 8314 Step 5 - - - 7452 8312 Note: the IRQ flag is actually effectively set three clocks in a row, starting one clock earlier than shown. NTSC and PAL times shown for comparison. 01.len_ctr ---------- Tests basic length counter operation 1) Passed tests 2) Problem with length counter load or $4015 3) Problem with length table, timing, or $4015 4) Writing $80 to $4017 should clock length immediately 5) Writing $00 to $4017 shouldn't clock length immediately 6) Clearing enable bit in $4015 should clear length counter 7) When disabled via $4015, length shouldn't allow reloading 8) Halt bit should suspend length clocking 02.len_table ------------ Tests all length table entries. 1) Passed 2) Failed. Prints four bytes $II $ee $cc $02 that indicate the length load value written (ll), the value that the emulator uses ($ee), and the correct value ($cc). 03.irq_flag ----------- Tests basic operation of frame irq flag. 1) Tests passed 2) Flag shouldn't be set in $4017 mode $40 3) Flag shouldn't be set in $4017 mode $80 4) Flag should be set in $4017 mode $00 5) Reading flag clears it 6) Writing $00 or $80 to $4017 doesn't affect flag 7) Writing $40 or $c0 to $4017 clears flag 04.clock_jitter --------------- Tests for APU clock jitter. Also tests basic timing of frame irq flag since it's needed to determine jitter. It's OK if you don't implement jitter, in which case you'll get error #5, but you can still run later tests without problem. 1) Passed tests 2) Frame irq is set too soon 3) Frame irq is set too late 4) Even jitter not handled properly 5) Odd jitter not handled properly 05.len_timing_mode0 ------------------- Tests length counter timing in mode 0. 1) Passed tests 2) First length is clocked too soon 3) First length is clocked too late 4) Second length is clocked too soon 5) Second length is clocked too late 6) Third length is clocked too soon 7) Third length is clocked too late 06.len_timing_mode1 ------------------- Tests length counter timing in mode 1. 1) Passed tests 2) First length is clocked too soon 3) First length is clocked too late 4) Second length is clocked too soon 5) Second length is clocked too late 6) Third length is clocked too soon 7) Third length is clocked too late 07.irq_flag_timing ------------------ Frame interrupt flag is set three times in a row 33255 clocks after writing $4017 with $00. 1) Success 2) Flag first set too soon 3) Flag first set too late 4) Flag last set too soon 5) Flag last set too late 08.irq_timing ------------- IRQ handler is invoked at minimum 33257 clocks after writing $00 to $4017. 1) Passed tests 2) Too soon 3) Too late 4) Never occurred 10.len_halt_timing ------------------ Changes to length counter halt occur after clocking length, not before. 1) Passed tests 2) Length shouldn't be clocked when halted at 16628 3) Length should be clocked when halted at 16629 4) Length should be clocked when unhalted at 16628 5) Length shouldn't be clocked when unhalted at 16629 11.len_reload_timing -------------------- Write to length counter reload should be ignored when made during length counter clocking and the length counter is not zero. 1) Passed tests 2) Reload just before length clock should work normally 3) Reload just after length clock should work normally 4) Reload during length clock when ctr = 0 should work normally 5) Reload during length clock when ctr > 0 should be ignored -- Shay Green ================================================ FILE: tetanes-core/test_roms/apu/readme.txt ================================================ NES APU Tests ------------- These ROMs test many aspects of the APU that are visible to the CPU. Really obsucre things are not tested here. 1-len_ctr --------- Tests length counter operation for the four main channels 2) Problem with length counter load or $4015 3) Problem with length table, timing, or $4015 4) Writing $80 to $4017 should clock length immediately 5) Writing 0 to $4017 shouldn't clock length immediately 6) Disabling via $4015 should clear length counter 7) When disabled via $4015, length shouldn't allow reloading 8) Halt bit should suspend length clocking 2-len_table ----------- Verifies all length table entries 3-irq_flag ---------- Verifies basic operation of frame irq flag 2) Flag shouldn't be set in $4017 mode $40 3) Flag shouldn't be set in $4017 mode $80 4) Flag should be set in $4017 mode $00 5) Reading flag should clear it 6) Writing $00 or $80 to $4017 shouldn't affect flag 7) Writing $40 or $C0 to $4017 should clear flag 4-jitter -------- Tests for APU clock jitter. Also tests basic timing of frame irq flag since it's needed to determine jitter. 3) Frame irq is set too late 4) Even jitter not handled properly 5) Odd jitter not handled properly 5-len_timing ------------ Verifies timing of length counter clocks in both modes 2) First length of mode 0 is too soon 3) First length of mode 0 is too late 4) Second length of mode 0 is too soon 5) Second length of mode 0 is too late 6) Third length of mode 0 is too soon 7) Third length of mode 0 is too late 8) First length of mode 1 is too soon 9) First length of mode 1 is too late 10) Second length of mode 1 is too soon 11) Second length of mode 1 is too late 12) Third length of mode 1 is too soon 13) Third length of mode 1 is too late 6-irq_flag_timing ----------------- Frame interrupt flag is set three times in a row 29831 clocks after writing $00 to $4017. 3) Flag first set too late 4) Flag last set too soon 5) Flag last set too late 7-dmc_basics ------------ Verifies basic DMC operation 2) DMC isn't working well enough to test further 3) Starting DMC should reload length from $4013 4) Writing $10 to $4015 should restart DMC if previous sample finished 5) Writing $10 to $4015 should not affect DMC if previous sample is still playing 6) Writing $00 to $4015 should stop current sample 7) Changing $4013 shouldn't affect current sample length 8) Shouldn't set DMC IRQ flag when flag is disabled 9) Should set IRQ flag when enabled and sample ends 10) Reading IRQ flag shouldn't clear it 11) Writing to $4015 should clear IRQ flag 12) Disabling IRQ flag should clear it 13) Looped sample shouldn't end until $00 is written to $4015 14) Looped sample shouldn't ever set IRQ flag 15) Clearing loop flag and then setting again shouldn't stop loop 16) Clearing loop flag should end sample once it reaches end 17) Looped sample should reload length from $4013 each time it reaches end 18) $4013=0 should give 1-byte sample 19) There should be a one-byte buffer that's filled immediately if empty 8-dmc_rates ----------- Verifies the DMC's 16 rates Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. -- Shay Green ================================================ FILE: tetanes-core/test_roms/apu/reset.txt ================================================ NES APU Reset Tests -------------------- These tests verify initial APU state at power, and the effect of reset. 4015_cleared ------------ At power and reset, $4015 is cleared. 2) At power, $4015 should be cleared 3) At reset, $4015 should be cleared 4017_timing ----------- At power, it is as if $00 were written to $4017, then a 9-12 clock delay, then execution from address in reset vector. At reset, same as above, except last value written to $4017 is written again, rather than $00. The delay from when $00 was written to $4017 is printed. Delay after NES being powered off for a minute is usually 9. 2) Frame IRQ flag should be set later after power/reset 3) Frame IRQ flag should be set sooner after power/reset 4017_written ------------ At power, $4017 = $00. At reset, $4017 mode is unchanged, but IRQ inhibit flag is sometimes cleared. 2) At power, $4017 should be written with $00 3) At reset, $4017 should should be rewritten with last value written irq_flag_cleared ---------------- At power and reset, IRQ flag is clear. 2) At power, flag should be clear 3) At reset, flag should be clear len_ctrs_enabled ---------------- At power and reset, length counters are enabled. 2) At power, length counters should be enabled 3) At reset, length counters should be enabled, triangle unaffected works_immediately ----------------- At power and reset, $4017, $4015, and length counters work immediately. 2) At power, writes should work immediately 3) At reset, writes should work immediately Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. -- Shay Green ================================================ FILE: tetanes-core/test_roms/apu/tests.json ================================================ [ { "name": "clock_jitter", "frames": [ { "number": 20, "hash": 951336095730925361 } ] }, { "name": "dmc_basics", "frames": [ { "number": 30, "hash": 11992147129660783512 } ] }, { "name": "dmc_dma_2007_read", "frames": [ { "number": 30, "hash": 11398877419118110174 } ] }, { "name": "dmc_dma_2007_write", "frames": [ { "number": 35, "hash": 10066428629761289066 } ] }, { "name": "dmc_dma_4016_read", "frames": [ { "number": 20, "hash": 12668862607518231523 } ] }, { "name": "dmc_dma_double_2007_read", "frames": [ { "number": 20, "hash": 3303528692302754891 } ] }, { "name": "dmc_dma_read_write_2007", "frames": [ { "number": 25, "hash": 10595698041155058010 } ] }, { "name": "dmc_rates", "frames": [ { "number": 30, "hash": 9588761239108989359 } ] }, { "name": "dpcmletterbox", "frames": [ { "number": 10, "hash": 9716310909309797997 } ] }, { "name": "irq_flag", "frames": [ { "number": 20, "hash": 951336095730925361 } ] }, { "name": "irq_flag_timing", "frames": [ { "number": 20, "hash": 951336095730925361 } ] }, { "name": "irq_timing", "frames": [ { "number": 20, "hash": 951336095730925361 } ] }, { "name": "len_ctr", "frames": [ { "number": 25, "hash": 951336095730925361 } ] }, { "name": "len_halt_timing", "frames": [ { "number": 20, "hash": 951336095730925361 } ] }, { "name": "len_reload_timing", "frames": [ { "number": 20, "hash": 951336095730925361 } ] }, { "name": "len_table", "frames": [ { "number": 20, "hash": 951336095730925361 } ] }, { "name": "len_timing", "frames": [ { "number": 20, "hash": 14878161860519346933 } ] }, { "name": "len_timing_mode0", "frames": [ { "number": 23, "hash": 951336095730925361 } ] }, { "name": "len_timing_mode1", "frames": [ { "number": 23, "hash": 951336095730925361 } ] }, { "name": "reset_len_ctrs_enabled", "frames": [ { "number": 20, "action": { "Reset": "Soft" } }, { "number": 40, "hash": 16014198644488276031 } ] }, { "name": "reset_timing", "frames": [ { "number": 20, "hash": 951336095730925361 } ] }, { "name": "test_1", "frames": [ { "number": 20, "hash": 12486489601112107272 } ] }, { "name": "test_2", "frames": [ { "number": 20, "hash": 12486489601112107272 } ] }, { "name": "test_3", "frames": [ { "number": 20, "hash": 12486489601112107272 } ] }, { "name": "test_4", "frames": [ { "number": 20, "hash": 12486489601112107272 } ] }, { "name": "test_5", "frames": [ { "number": 20, "hash": 12486489601112107272 } ] }, { "name": "test_6", "frames": [ { "number": 20, "hash": 12486489601112107272 } ] }, { "name": "test_7", "frames": [ { "number": 20, "hash": 12486489601112107272 } ] }, { "name": "test_8", "frames": [ { "number": 20, "hash": 12486489601112107272 } ] }, { "name": "test_9", "frames": [ { "number": 20, "hash": 12486489601112107272 } ] }, { "name": "test_10", "frames": [ { "number": 20, "hash": 12486489601112107272 } ] }, { "name": "pal_clock_jitter", "frames": [ { "number": 0, "action": { "SetNesRegion": "Pal" } }, { "number": 20, "hash": 13813501042665481329 } ] }, { "name": "pal_irq_flag_timing", "frames": [ { "number": 0, "action": { "SetNesRegion": "Pal" } }, { "number": 20, "hash": 15283534834975948636 } ] }, { "name": "pal_len_halt_timing", "frames": [ { "number": 0, "action": { "SetNesRegion": "Pal" } }, { "number": 20, "hash": 3223047209937034573 } ] }, { "name": "pal_len_reload_timing", "frames": [ { "number": 0, "action": { "SetNesRegion": "Pal" } }, { "number": 20, "hash": 14496598670261025093 } ] }, { "name": "pal_len_timing_mode0", "frames": [ { "number": 0, "action": { "SetNesRegion": "Pal" } }, { "number": 20, "hash": 15415456214152333503 } ] }, { "name": "pal_len_timing_mode1", "frames": [ { "number": 0, "action": { "SetNesRegion": "Pal" } }, { "number": 20, "hash": 16558599236285993505 } ] }, { "name": "reset_4015_cleared", "frames": [ { "number": 20, "action": { "Reset": "Soft" } }, { "number": 40, "hash": 6079776310151228335 } ] }, { "name": "reset_4017_timing", "frames": [ { "number": 20, "action": { "Reset": "Soft" } }, { "number": 40, "hash": 158964301043339763 } ] }, { "name": "reset_4017_written", "frames": [ { "number": 20, "action": { "Reset": "Soft" } }, { "number": 35, "action": { "Reset": "Soft" } }, { "number": 50, "hash": 16485921128453261162 } ] }, { "name": "reset_irq_flag_cleared", "frames": [ { "number": 15, "action": { "Reset": "Soft" } }, { "number": 20, "hash": 2054987767744312757 } ] }, { "name": "reset_works_immediately", "frames": [ { "number": 20, "action": { "Reset": "Soft" } }, { "number": 30, "hash": 15191020418456945175 } ] }, { "name": "pal_irq_flag", "frames": [ { "number": 0, "action": { "SetNesRegion": "Pal" } }, { "number": 20, "hash": 13747417966407999092 } ] }, { "name": "pal_irq_timing", "frames": [ { "number": 0, "action": { "SetNesRegion": "Pal" } }, { "number": 20, "hash": 7219648777380498854 } ] }, { "name": "pal_len_ctr", "frames": [ { "number": 0, "action": { "SetNesRegion": "Pal" } }, { "number": 25, "hash": 6833095462741382586 } ] }, { "name": "pal_len_table", "frames": [ { "number": 0, "action": { "SetNesRegion": "Pal" } }, { "number": 20, "hash": 2970567939215216479 } ] } ] ================================================ FILE: tetanes-core/test_roms/apu/volumes.txt ================================================ Volume tests for NES _____________________________________________________________________ Background The NES has four tone generator channels and one digital sample playback channel: 1. Rectangular pulse ("square") wave 2. Another square wave 3. 32-step triangle wave 4. Binary noise generated by a linear feedback shift register 5. Delta pulse code modulation; also allows writes of raw LPCM to the counter for use as a generic 7-bit DAC Unlike systems such as the GBA, which digitally add all channels before passing them to a DAC, the NES has a separate DAC for each channel and mixes them analog. Nonlinearities in this mixing can cause one channel to affect the volume of another channel. The NES uses an unsigned DAC, meaning that each channel can generate only positive signal values. Such a DAC generates a lot of DC, and the NES has a high-pass filter on its audio output to block the DC. Different emulators use different time constants on their DC filters, which human listeners generally can't perceive. So you can't just measure the maximum voltage; you have to measure the difference between the high and low values. Different emulators use different amounts of headroom in the 16-bit range, depending on what Famicom expansion audio chips are present. So you have to compare relative volumes, not absolute volumes. _____________________________________________________________________ The test pattern This program demonstrates the channel balance among implementations of the NES architecture. The pattern consists of a set of 12 tones, as close to 1000 Hz as the NES allows: 1. Channel 1, 1/8 duty 2. Channel 1, 1/4 duty 3. Channel 1, 1/2 duty 4. Channel 1, 3/4 duty 5. Channels 1 and 2, 1/8 duty 6. Channels 1 and 2, 1/4 duty 7. Channels 1 and 2, 1/2 duty 8. Channels 1 and 2, 3/4 duty 9. Channel 3 10. Channel 4, long LFSR period 11. Channel 4, short LFSR period 12. Channel 5, amplitude 30 When the user presses A on controller 1, the pattern plays three times, with channel 5 held steady at 0, 48, and 96. The high point of tone 12 each time is 30 units above the level for that time, that is, 30, 78, and 126 respectively. _____________________________________________________________________ Recordings The files in the 'recordings' folder are recordings of volumes.nes run in various environments. They were recorded at 44100 Hz and then encoded using OggDropXPd 1.90 (libvorbis 1.2.0) at -q 7.00. * nes-001.ogg: Nintendo Entertainment System (NTSC U/C) with PowerPak * nestopia.ogg: Nestopia 1.40 * nintendulator.ogg: Nintendulator snapshot 2009-02-28 * fceux.ogg: FCEUX 2.0.4-interim 2008-11-24 _____________________________________________________________________ Legal Copyright (c) 2009 Damian Yerrick The program and manual are under the following license: This work is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this work. Permission is granted to anyone to use this work for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this work must not be misrepresented; you must not claim that you wrote the original work. If you use this work in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original work. 3. This notice may not be removed or altered from any source distribution. The term "source" refers to the preferred form of a work for making changes to it. ================================================ FILE: tetanes-core/test_roms/cpu/branch.txt ================================================ NES 6502 Branch Timing Test ROMs -------------------------------- These ROMs test timing of the branch instruction, including edge cases which an emulator might get wrong. When run on a NES they all give a passing result. Each ROM runs several tests and reports the result on screen and by beeping a number of times. See below for the meaning of failure codes for each test. THE TESTS MUST BE RUN (*AND* *PASS*) IN ORDER, because some earlier ROMs test things that later ones assume will work properly. Source code for each test is included, and most tests are clearly divided into sections. Support code is also included, but it runs on a custom devcart and assembler so it will require some effort to assemble. Contact me if you'd like assistance porting them to your setup. Branch Timing Summary --------------------- An untaken branch takes 2 clocks. A taken branch takes 3 clocks. A taken branch that crosses a page takes 4 clocks. Page crossing occurs when the high byte of the branch target address is different than the high byte of address of the next instruction: branch_target: ... bne branch_target next_instruction: nop ... branch_target: 1.Branch_Basics --------------- Tests branch timing basics and PPU NMI timing, which is needed for the tests 2) NMI period is too short 3) NMI period is too too long 4) Branch not taken is too long 5) Branch not taken is too short 6) Branch taken is too long 7) Branch taken is too short 2.Backward_Branch ----------------- Tests backward (negative) branch timing. 2) Branch from $E4FD to $E4FC is too long 3) Branch from $E4FD to $E4FC is too short 4) Branch from $E5FE to $E5FD is too long 5) Branch from $E5FE to $E5FD is too short 6) Branch from $E700 to $E6FF is too long 7) Branch from $E700 to $E6FF is too short 8) Branch from $E801 to $E800 is too long 9) Branch from $E801 to $E800 is too short 3.Forward_Branch ---------------- Tests forward (positive) branch timing. 2) Branch from $E5FC to $E5FF is too long 3) Branch from $E5FC to $E5FF is too short 4) Branch from $E6FD to $E700 is too long 5) Branch from $E6FD to $E700 is too short 6) Branch from $E7FE to $E801 is too long 7) Branch from $E7FE to $E801 is too short 8) Branch from $E8FF to $E902 is too long 9) Branch from $E8FF to $E902 is too short -- Shay Green ================================================ FILE: tetanes-core/test_roms/cpu/dummy_writes.txt ================================================ NES Double-Write Behavior Tests ---------------------------------- These tests verify that the CPU is doing double-writes properly. Double-write is a side effect of the NES CPU when it is executing a read-modify-write instruction: It first reads the original value, then writes back the same value, and then writes the modified value. For example, the cycle by cycle listing of an absolute-addressing instruction such as INC is as follows (from 65doc.txt by John West and Marko Mkel): Read-Modify-Write instructions (ASL, LSR, ROL, ROR, INC, DEC, SLO, SRE, RLA, RRA, ISB, DCP) # address R/W description --- ------- --- ------------------------------------------ 1 PC R fetch opcode, increment PC 2 PC R fetch low byte of address, increment PC 3 PC R fetch high byte of address, increment PC 4 address R read from effective address 5 address W write the value back to effective address, and do the operation on it 6 address W write the new value to effective address Two sets of tests are provided: One that uses OAM data ($2004) for testing, and one that uses PPU memory ($2007). The OAM data testing is only valid on emulators. The actual NES console fails the test, because the OAM read port is not reliable on the real console. The PPUMEM test can be used on emulators and on the real NES. The PPUMEM test requires that the emulator implements open bus behavior properly. Without open bus behavior the testing will not work as expected. Because of that, an extensive set of tests is first performed for the open bus behavior. Tests in the OAM version: #2: OAM reading is too unreliable. #3: Writes to OAM should automatically increment SPRADDR #4: Reads from OAM should not automatically increment SPRADDR #5: Some opcodes failed the test. #6: OAM reads are unreliable. #7: ROM should not be writable. Fail codes #2 and #6 are basically the same thing, except #2 is given if #5 also fails. If #5 passes, but the OAM read test failed, #6 is given instead. Expected output in the OAM version: TEST: cpu_dummy_writes_oam This program verifies that the CPU does 2x writes properly. Any read-modify-write opcode should first write the origi- nal value; then the calculated value exactly 1 cycle later. Requirement: OAM memory reads MUST be reliable. This is often the case on emulators, but NOT on the real NES. Nevertheless, this test can be used to see if the CPU in the emulator is built properly. Testing OAM. The screen will go blank for a moment now. OK; Verifying opcodes... 0E2E4E6ECEEE 1E3E5E7EDEFE 0F2F4F6FCFEF 1F3F5F7FDFFF 03234363C3E3 13335373D3F3 1B3B5B7BDBFB Passed Tests in the PPUMEM version: #2: Non-palette PPU memory reads should have one-byte buffer #3: A single write to $2005 must not change the address used by $2007 when vblank is on. #4: Even two writes to $2005 must not change the address used by $2007 when vblank is on. #5: A single write to $2006 must not change the address used by $2007 when vblank is on. #6: A single write to $2005 must change the address toggle for both $2005 and $2006. #7: Sequential PPU memory read does not work #8: Sequential PPU memory write does not work #9: Some opcodes failed the test. #10: Open bus behavior is wrong. #11: ROM should not be writable. Expected output in the PPUMEM version: TEST: cpu_dummy_writes_ppumem This program verifies that the CPU does 2x writes properly. Any read-modify-write opcode should first write the origi- nal value; then the calculated value exactly 1 cycle later. Verifying open bus behavior. W- W- WR W- W- W- W- WR 2000+ 0 1 2 3 4 5 6 7 R0: 0- 0- 00 0- 0- 0- 0- 00 R1: 0- 0- 00 0- 0- 0- 0- 00 R3: 0- 0- 00 0- 0- 0- 0- 00 R5: 0- 0- 00 0- 0- 0- 0- 00 R6: 0- 0- 00 0- 0- 0- 0- 00 OK; Verifying opcodes... 0E2E4E6ECEEE 1E3E5E7EDEFE 0F2F4F6FCFEF 1F3F5F7FDFFF 03234363C3E3 13335373D3F3 1B3B5B7BDBFB Passed Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The text output may include ANSI color codes, which take the form of an esc character ($1B), an opening bracket ('['), and a sequence of numbers and semicolon characters, terminated by a non-digit character ('m'). The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 -- Shay Green Joel Yliluoma ================================================ FILE: tetanes-core/test_roms/cpu/exec_space.txt ================================================ NES Memory Execution Tests ---------------------------------- These tests verify that the CPU can execute code from any possible memory location, even if that is mapped as I/O space. In addition, two obscure side effects are tested: 1. The PPU open bus. Any write to PPU will update the open bus. Reading from 2002 updates the low 5 bits. Reading from 2007 updates 8 bits. The open bus is shown in any addresss/bit that the PPU does not write to. Read from 2000, you get open bus. Read from 2006, ditto. Read from 2002, you get that in high 3 bits. Additionally, the open bus decays automatically to zero in about one second if not refreshed. This test requires that a value written to $2003 can be read back from $2001 within a time window of one or two frames. 2. One-byte opcodes must issue a dummy read to the byte immediately following that opcode. The CPU always does a fetch of the second byte, before it has even begun executing the opcode in the first place. Additionally, the following PPU features must be working properly: 1. PPU memory writes and reads through $2006/$2007 2. The address high/low toggle reset at $2002 3. A single write through $2006 must not affect the address used by $2007 4. NMI should fire sometimes to salvage a broken program, if the JSR/JMP never reaches its intended destination. (Only required in the test IF the CPU and/or open bus are not working properly.) The test is done FIVE times: Once with JSR $2001, again with JMP $2001, and then with RTS (with target address of $2001), and then with a JMP that expects to return with an RTI opcode. Finally, with a regular JSR, but the return from the code is done through a BRK instruction. Tests and results: #2: PPU memory access through $2007 does not work properly. (Use other tests to determine the exact problem.) #3: PPU open bus implementation is missing or incomplete: A write to $2003, followed by a read from $2001 should return the same value as was written. #4: The RTS at $2001 was never executed. (If NMI has not been implemented in the emulator, the symptom of this failure is that the program crashes and does not output either "Fail" nor "Passed"). #5: An RTS opcode should still do a dummy fetch of the next opcode. (The same goes for all one-byte opcodes, really.) #6: I have no idea what happened, but the test did not work as supposed to. In any case, the problem is in the PPU. #7: A jump to $2001 should never execute code from $8001 / $9001 / $A001 / $B001 / $C001 / $D001 / $E001. #8: Okay, the test passed when JSR was used, but NOT when the opcode was JMP. I definitely did not think any emulator would trigger this result. #9: Your PPU is broken in mind-defyingly random ways. #10: RTS to $2001 never returned. This message never gets displayed. #11: The test passed when JSR was used, and when JMP was used, but NOT when RTS was used. Caught ya! Paranoia wins. #12: Your PPU gave up reason at the last moment. #13: JMP to $2001 never returned. Again, this message never gets displayed. #14: An RTI opcode should still do a dummy fetch of the next opcode. (The same goes for all one-byte opcodes, really.) #15: An RTI opcode should not destroy the PPU. Somehow that still appears to be the case here. #16: IRQ occurred uncalled #17: JSR to $2001 never returned. (Never displayed) #18: The BRK instruction should issue an automatic fetch of the byte that follows right after the BRK. (The same goes for all one-byte opcodes, but with BRK it should be a bit more obvious than with others.) #19: A BRK opcode should not destroy the PPU. Somehow that still appears to be the case here. Expected output: TEST:test_cpu_exec_space_ppuio This program verifies that the CPU can execute code from any possible location that it can address, including I/O space. In addition, it will be tested that an RTS instruction does a dummy read of the byte that immediately follows the instructions. JSR test OK JMP test OK RTS test OK JMP+RTI test OK BRK test OK Passed Expected output in the other test: TEST: test_cpu_exec_space_apu This program verifies that the CPU can execute code from any possible location that it can address, including I/O space. In this test, it is also verified that not only all write-only APU I/O ports return the open bus, but also the unallocated I/O space in $4018..$40FF. 40FF Passed Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The text output may include ANSI color codes, which take the form of an esc character ($1B), an opening bracket ('['), and a sequence of numbers and semicolon characters, terminated by a non-digit character ('m'). The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 -- Shay Green Joel Yliluoma ================================================ FILE: tetanes-core/test_roms/cpu/instr.txt ================================================ NES CPU Instruction Behavior Tests ---------------------------------- These tests verify most instruction behavior fairly thoroughly, including unofficial instructions. Failing instructions are listed by their opcode and name. Serious errors in behavior of basic opcodes might cause many false errors. These tests will NOT help you figure out what is wrong with your implementation of the failed instructions, simply whether you are failing any, and which those are. all_instrs.nes tests (almost) all instructions, including unofficial ones, while official_only.nes tests only official ("documented") instructions. The *_singles/ test all instructions, but test the official ones first, so you can tell whether you pass those even if your emulator hangs on the unofficial ones. The nsf_singles builds audibly report the opcodes of any failed instructions before the final result. Internal operation ------------------ Instructions are tested by setting many combinations of input values for registers, flags, and memory, running the instruction under test, then updating a running checksum with the resulting values. After trying all interesting input combinations, the checksum is compared with the correct one to find whether the instruction passed. This approach is used for all instructions, even those that shouldn't care the value of any registers or modify them. This catches an emulator incorrectly looking at or modifying registers in those instructions. This approach makes it very easy to write the tests, since the instructions don't have to be each coded for separately; instead, only the different addressing modes need separate tests. instrs: what opcodes to test, along with their names. instr_template: template for instructions. First byte is replaced with opcode. After executing instruction and anything after, it should jump to instr_done. operand: where to place byte operand for instruction. This value comes from the table of values to test, using an index separate from that used to set the other registers before executing the instruction. set_in: things to execute before the instruction. On entry, A is value put in operand, and Y is index used in table. check_out: things to execute after the instruction. values2: if defined, set of values to use for operand. Default uses same set as for other registers. test_values: routine to actually run the tests. test_normal does what's described above. correct_checksums: list of checksums for each instruction. Generated when CALIBRATE=1 is uncommented. Instructions ------------ U = Unofficial X = Freezes CPU, so not tested ? = Inconsistent/unknown behavior, so not tested 00 BRK #n 01 ORA (z,X) 02 X KIL 03 U SLO (z,X) 04 U DOP z 05 ORA z 06 ASL z 07 U SLO z 08 PHP 09 ORA #n 0A ASL A 0B U AAC #n 0C U TOP abs 0D ORA a 0E ASL a 0F U SLO abs 10 BPL r 11 ORA (z),Y 12 X KIL 13 U SLO (z),Y 14 U DOP z,X 15 ORA z,X 16 ASL z,X 17 U SLO z,X 18 CLC 19 ORA a,Y 1A U NOP 1B U SLO abs,Y 1C U TOP abs,X 1D ORA a,X 1E ASL a,X 1F U SLO abs,X 20 JSR a 21 AND (z,X) 22 X KIL 23 U RLA (z,X) 24 BIT z 25 AND z 26 ROL z 27 U RLA z 28 PLP 29 AND #n 2A ROL A 2B U AAC #n 2C BIT a 2D AND a 2E ROL a 2F U RLA abs 30 BMI r 31 AND (z),Y 32 X KIL 33 U RLA (z),Y 34 U DOP z,X 35 AND z,X 36 ROL z,X 37 U RLA z,X 38 SEC 39 AND a,Y 3A U NOP 3B U RLA abs,Y 3C U TOP abs,X 3D AND a,X 3E ROL a,X 3F U RLA abs,X 40 RTI 41 EOR (z,X) 42 X KIL 43 U SRE (z,X) 44 U DOP z 45 EOR z 46 LSR z 47 U SRE z 48 PHA 49 EOR #n 4A LSR A 4B U ASR #n 4C JMP a 4D EOR a 4E LSR a 4F U SRE abs 50 BVC r 51 EOR (z),Y 52 X KIL 53 U SRE (z),Y 54 U DOP z,X 55 EOR z,X 56 LSR z,X 57 U SRE z,X 58 CLI 59 EOR a,Y 5A U NOP 5B U SRE abs,Y 5C U TOP abs,X 5D EOR a,X 5E LSR a,X 5F U SRE abs,X 60 RTS 61 ADC (z,X) 62 X KIL 63 U RRA (z,X) 64 U DOP z 65 ADC z 66 ROR z 67 U RRA z 68 PLA 69 ADC #n 6A ROR A 6B U ARR #n 6C JMP (a) 6D ADC a 6E ROR a 6F U RRA abs 70 BVS r 71 ADC (z),Y 72 X KIL 73 U RRA (z),Y 74 U DOP z,X 75 ADC z,X 76 ROR z,X 77 U RRA z,X 78 SEI 79 ADC a,Y 7A U NOP 7B U RRA abs,Y 7C U TOP abs,X 7D ADC a,X 7E ROR a,X 7F U RRA abs,X 80 U DOP #n 81 STA (z,X) 82 U DOP #n 83 U AAX (z,X) 84 STY z 85 STA z 86 STX z 87 U AAX z 88 DEY 89 U DOP #n 8A TXA 8B ? XAA #n 8C STY a 8D STA a 8E STX a 8F U AAX abs 90 BCC r 91 STA (z),Y 92 X KIL 93 ? AXA (z),Y 94 STY z,X 95 STA z,X 96 STX z,Y 97 U AAX z,Y 98 TYA 99 STA a,Y 9A TXS 9B ? XAS abs,Y 9C U SYA abs,X 9D STA a,X 9E U SXA abs,Y 9F ? AXA abs,Y A0 LDY #n A1 LDA (z,X) A2 LDX #n A3 U LAX (z,X) A4 LDY z A5 LDA z A6 LDX z A7 U LAX z A8 TAY A9 LDA #n AA TAX AB U ATX #n AC LDY a AD LDA a AE LDX a AF U LAX abs B0 BCS r B1 LDA (z),Y B2 X KIL B3 U LAX (z),Y B4 LDY z,X B5 LDA z,X B6 LDX z,Y B7 U LAX z,Y B8 CLV B9 LDA a,Y BA TSX BB ? LAR abs,Y BC LDY a,X BD LDA a,X BE LDX a,Y BF U LAX abs,Y C0 CPY #n C1 CMP (z,X) C2 U DOP #n C3 U DCP (z,X) C4 CPY z C5 CMP z C6 DEC z C7 U DCP z C8 INY C9 CMP #n CA DEX CB U AXS #n CC CPY a CD CMP a CE DEC a CF U DCP abs D0 BNE r D1 CMP (z),Y D2 X KIL D3 U DCP (z),Y D4 U DOP z,X D5 CMP z,X D6 DEC z,X D7 U DCP z,X D8 CLD D9 CMP a,Y DA U NOP DB U DCP abs,Y DC U TOP abs,X DD CMP a,X DE DEC a,X DF U DCP abs,X E0 CPX #n E1 SBC (z,X) E2 U DOP #n E3 U ISC (z,X) E4 CPX z E5 SBC z E6 INC z E7 U ISC z E8 INX E9 SBC #n EA NOP EB U SBC #n EC CPX a ED SBC a EE INC a EF U ISC abs F0 BEQ r F1 SBC (z),Y F2 X KIL F3 U ISC (z),Y F4 U DOP z,X F5 SBC z,X F6 INC z,X F7 U ISC z,X F8 SED F9 SBC a,Y FA U NOP FB U ISC abs,Y FC U TOP abs,X FD SBC a,X FE INC a,X FF U ISC abs,X Multi-tests ----------- The NES/NSF builds in the main directory consist of multiple sub-tests. When run, they list the subtests as they are run. The final result code refers to the first sub-test that failed. For more information about any failed subtests, run them individually from rom_singles/ and nsf_singles/. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. -- Shay Green ================================================ FILE: tetanes-core/test_roms/cpu/instr_misc.txt ================================================ NES CPU Instruction Behavior Misc Tests ---------------------------------------- These tests verify miscellaneous instruction behavior. 01-abs_x_wrap ------------- Verifies that $FFFF wraps around to 0 for STA abs,X and LDA abs,X. 02-branch_wrap -------------- Verifies that branching past end or before beginning of RAM wraps around. 03-dummy_reads -------------- Tests some instructions that do dummy reads before the real read/write. Doesn't test all instructions. Tests LDA and STA with modes (ZP,X), (ZP),Y and ABS,X Dummy reads for the following cases are tested: LDA ABS,X or (ZP),Y when carry is generated from low byte STA ABS,X or (ZP),Y ROL ABS,X always 04-dummy_reads_apu ------------------ Tests dummy reads for (hopefully) ALL instructions which do them, including unofficial ones. Prints opcode(s) of failed instructions. Requires that APU implement $4015 IRQ flag reading. Multi-tests ----------- The NES/NSF builds in the main directory consist of multiple sub-tests. When run, they list the subtests as they are run. The final result code refers to the first sub-test that failed. For more information about any failed subtests, run them individually from rom_singles/ and nsf_singles/. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Text output ----------- Tests generally print information on screen, but also output information in other ways, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. When building as an NSF, the final result is reported as a series of beeps (see below). Any important diagnostic bytes are also reported as beeps, before the final result. All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. See the source code for more information about a particular test and why it might be failing. Each test has comments anout its operation. NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. In addition to the other text output methods described above, NSF builds report essential information bytes audibly, including the final result. A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason as listed in the source code by the corresponding set_code line. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 -- Shay Green ================================================ FILE: tetanes-core/test_roms/cpu/instr_timing.txt ================================================ NES CPU Instruction Timing Test ------------------------------- These tests verify timing of all NES CPU instructions, except the 12 that freeze the CPU. The individual tests report the opcode of any failed instructions. instr_timing prints the measured and correct times. branch_timing runs the branch instruction in 8 different situations: four not taken, and four taken. For each of these four, the first two are for a non-page-cross both negative and positive, and the second two cross a page. The correct times are 2 2 2 2 3 3 4 4. Requirements ------------ - Basic CPU instruction behavior - Basic APU length counter operation Internal operation ------------------ Each instruction is timed by setting up appropriate conditions, synchronizing to the APU length counter and then loading it with 2, executing the instruction in a loop that stops once the length counter expires. The number of loop iterations indicates how many clocks the instruction took. Multi-tests ----------- The NES/NSF builds in the main directory consist of multiple sub-tests. When run, they list the subtests as they are run. The final result code refers to the first sub-test that failed. For more information about any failed subtests, run them individually from rom_singles/ and nsf_singles/. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. -- Shay Green ================================================ FILE: tetanes-core/test_roms/cpu/interrupts.txt ================================================ NES CPU Interrupt Tests ----------------------- Tests behavior and timing of CPU in the presence of interrupts, both IRQ and NMI. CLI Latency Summary ------------------- The RTI instruction affects IRQ inhibition immediately. If an IRQ is pending and an RTI is executed that clears the I flag, the CPU will invoke the IRQ handler immediately after RTI finishes executing. The CLI, SEI, and PLP instructions effectively delay changes to the I flag until after the next instruction. For example, if an interrupt is pending and the I flag is currently set, executing CLI will execute the next instruction before the CPU invokes the IRQ handler. This delay only affects inhibition, not the value of the I flag itself; CLI followed by PHP will leave the I flag cleared in the saved status byte on the stack (bit 2), as expected. 1-cli_latency ------------- Tests the delay in CLI taking effect, and some basic aspects of IRQ handling and the APU frame IRQ (needed by the tests). It uses the APU's frame IRQ and first verifies that it works well enough for the tests. The later tests execute CLI followed by SEI and equivalent pairs of instructions (CLI, PLP, where the PLP sets the I flag). These should only allow at most one invocation of the IRQ handler, even if it doesn't acknowledge the source of the IRQ. RTI is also tested, which behaves differently. These tests also *don't* disable interrupts after the first IRQ, in order to test whether a pair of instructions allows only one interrupt or causes continuous interrupts that block the main code from continuing. 2) RTI should not adjust return address (as RTS does) 3) APU should generate IRQ when $4017 = $00 4) Exactly one instruction after CLI should execute before IRQ is taken 5) CLI SEI should allow only one IRQ just after SEI 6) In IRQ allowed by CLI SEI, I flag should be set in saved status flags 7) CLI PLP should allow only one IRQ just after PLP 8) PLP SEI should allow only one IRQ just after SEI 9) PLP PLP should allow only one IRQ just after PLP 10) CLI RTI should not allow any IRQs 11) Unacknowledged IRQ shouldn't let any mainline code run 12) RTI RTI shouldn't let any mainline code run 2-nmi_and_brk ------------- NMI behavior when it interrupts BRK. Occasionally fails on NES due to PPU-CPU synchronization. Result when run: NMI BRK -- 27 36 00 NMI before CLC 26 36 00 NMI after CLC 26 36 00 36 00 00 NMI interrupting BRK, with B bit set on stack 36 00 00 36 00 00 36 00 00 36 00 00 27 36 00 NMI after SEC at beginning of IRQ handler 27 36 00 3-nmi_and_irq ------------- NMI behavior when it interrupts IRQ vectoring. Result when run: NMI IRQ 23 00 NMI occurs before LDA #1 21 00 NMI occurs after LDA #1 (Z flag clear) 21 00 20 00 NMI occurs after CLC, interrupting IRQ 20 00 20 00 20 00 20 00 20 00 20 00 Same result for 7 clocks before IRQ is vectored 25 20 IRQ occurs, then NMI occurs after SEC in IRQ handler 25 20 4-irq_and_dma ------------- Has IRQ occur at various times around sprite DMA. First column refers to what instruction IRQ occurred after. Second column is time of IRQ, in CPU clocks relative to some arbitrary starting point. 0 +0 1 +1 1 +2 2 +3 2 +4 4 +5 4 +6 7 +7 7 +8 7 +9 7 +10 8 +11 8 +12 8 +13 ... 8 +524 8 +525 8 +526 9 +527 5-branch_delays_irq ------------------- A taken non-page-crossing branch ignores IRQ during its last clock, so that next instruction executes before the IRQ. Other instructions would execute the NMI before the next instruction. The same occurs for NMI, though that's not tested here. test_jmp T+ CK PC 00 02 04 NOP 01 01 04 02 03 07 JMP 03 02 07 04 01 07 05 02 08 NOP 06 01 08 07 03 08 JMP 08 02 08 09 01 08 test_branch_not_taken T+ CK PC 00 02 04 CLC 01 01 04 02 02 06 BCS 03 01 06 04 02 07 NOP 05 01 07 06 04 0A JMP 07 03 0A 08 02 0A 09 01 0A JMP test_branch_taken_pagecross T+ CK PC 00 02 0D CLC 01 01 0D 02 04 00 BCC 03 03 00 04 02 00 05 01 00 06 04 03 LDA $100 07 03 03 08 02 03 09 01 03 test_branch_taken T+ CK PC 00 02 04 CLC 01 01 04 02 03 07 BCC 03 02 07 04 05 0A LDA $100 *** This is the special case 05 04 0A 06 03 0A 07 02 0A 08 01 0A 09 03 0A JMP Multi-tests ----------- The NES/NSF builds in the main directory consist of multiple sub-tests. When run, they list the subtests as they are run. The final result code refers to the first sub-test that failed. For more information about any failed subtests, run them individually from rom_singles/ and nsf_singles/. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. -- Shay Green ================================================ FILE: tetanes-core/test_roms/cpu/nestest.txt ================================================ $C000:4C F5 C5 JMP $C5F5 A:00 X:00 Y:00 P:nvUbdIzc SP:FD PPU: 0, 0 CYC:7 $C5F5:A2 00 LDX #$00 A:00 X:00 Y:00 P:nvUbdIzc SP:FD PPU: 9, 0 CYC:10 $C5F7:86 00 STX $00 = #$00 A:00 X:00 Y:00 P:nvUbdIZc SP:FD PPU: 15, 0 CYC:12 $C5F9:86 10 STX $10 = #$00 A:00 X:00 Y:00 P:nvUbdIZc SP:FD PPU: 24, 0 CYC:15 $C5FB:86 11 STX $11 = #$00 A:00 X:00 Y:00 P:nvUbdIZc SP:FD PPU: 33, 0 CYC:18 $C5FD:20 2D C7 JSR $C72D A:00 X:00 Y:00 P:nvUbdIZc SP:FD PPU: 42, 0 CYC:21 $C72D:EA NOP A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU: 60, 0 CYC:27 $C72E:38 SEC A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU: 66, 0 CYC:29 $C72F:B0 04 BCS $C735 A:00 X:00 Y:00 P:nvUbdIZC SP:FB PPU: 72, 0 CYC:31 $C735:EA NOP A:00 X:00 Y:00 P:nvUbdIZC SP:FB PPU: 81, 0 CYC:34 $C736:18 CLC A:00 X:00 Y:00 P:nvUbdIZC SP:FB PPU: 87, 0 CYC:36 $C737:B0 03 BCS $C73C A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU: 93, 0 CYC:38 $C739:4C 40 C7 JMP $C740 A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU: 99, 0 CYC:40 $C740:EA NOP A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU:108, 0 CYC:43 $C741:38 SEC A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU:114, 0 CYC:45 $C742:90 03 BCC $C747 A:00 X:00 Y:00 P:nvUbdIZC SP:FB PPU:120, 0 CYC:47 $C744:4C 4B C7 JMP $C74B A:00 X:00 Y:00 P:nvUbdIZC SP:FB PPU:126, 0 CYC:49 $C74B:EA NOP A:00 X:00 Y:00 P:nvUbdIZC SP:FB PPU:135, 0 CYC:52 $C74C:18 CLC A:00 X:00 Y:00 P:nvUbdIZC SP:FB PPU:141, 0 CYC:54 $C74D:90 04 BCC $C753 A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU:147, 0 CYC:56 $C753:EA NOP A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU:156, 0 CYC:59 $C754:A9 00 LDA #$00 A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU:162, 0 CYC:61 $C756:F0 04 BEQ $C75C A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU:168, 0 CYC:63 $C75C:EA NOP A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU:177, 0 CYC:66 $C75D:A9 40 LDA #$40 A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU:183, 0 CYC:68 $C75F:F0 03 BEQ $C764 A:40 X:00 Y:00 P:nvUbdIzc SP:FB PPU:189, 0 CYC:70 $C761:4C 68 C7 JMP $C768 A:40 X:00 Y:00 P:nvUbdIzc SP:FB PPU:195, 0 CYC:72 $C768:EA NOP A:40 X:00 Y:00 P:nvUbdIzc SP:FB PPU:204, 0 CYC:75 $C769:A9 40 LDA #$40 A:40 X:00 Y:00 P:nvUbdIzc SP:FB PPU:210, 0 CYC:77 $C76B:D0 04 BNE $C771 A:40 X:00 Y:00 P:nvUbdIzc SP:FB PPU:216, 0 CYC:79 $C771:EA NOP A:40 X:00 Y:00 P:nvUbdIzc SP:FB PPU:225, 0 CYC:82 $C772:A9 00 LDA #$00 A:40 X:00 Y:00 P:nvUbdIzc SP:FB PPU:231, 0 CYC:84 $C774:D0 03 BNE $C779 A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU:237, 0 CYC:86 $C776:4C 7D C7 JMP $C77D A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU:243, 0 CYC:88 $C77D:EA NOP A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU:252, 0 CYC:91 $C77E:A9 FF LDA #$FF A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU:258, 0 CYC:93 $C780:85 01 STA $01 = #$00 A:FF X:00 Y:00 P:NvUbdIzc SP:FB PPU:264, 0 CYC:95 $C782:24 01 BIT $01 = #$FF A:FF X:00 Y:00 P:NvUbdIzc SP:FB PPU:273, 0 CYC:98 $C784:70 04 BVS $C78A A:FF X:00 Y:00 P:NVUbdIzc SP:FB PPU:282, 0 CYC:101 $C78A:EA NOP A:FF X:00 Y:00 P:NVUbdIzc SP:FB PPU:291, 0 CYC:104 $C78B:24 01 BIT $01 = #$FF A:FF X:00 Y:00 P:NVUbdIzc SP:FB PPU:297, 0 CYC:106 $C78D:50 03 BVC $C792 A:FF X:00 Y:00 P:NVUbdIzc SP:FB PPU:306, 0 CYC:109 $C78F:4C 96 C7 JMP $C796 A:FF X:00 Y:00 P:NVUbdIzc SP:FB PPU:312, 0 CYC:111 $C796:EA NOP A:FF X:00 Y:00 P:NVUbdIzc SP:FB PPU:321, 0 CYC:114 $C797:A9 00 LDA #$00 A:FF X:00 Y:00 P:NVUbdIzc SP:FB PPU:327, 0 CYC:116 $C799:85 01 STA $01 = #$FF A:00 X:00 Y:00 P:nVUbdIZc SP:FB PPU:333, 0 CYC:118 $C79B:24 01 BIT $01 = #$00 A:00 X:00 Y:00 P:nVUbdIZc SP:FB PPU: 1, 1 CYC:121 $C79D:50 04 BVC $C7A3 A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU: 10, 1 CYC:124 $C7A3:EA NOP A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU: 19, 1 CYC:127 $C7A4:24 01 BIT $01 = #$00 A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU: 25, 1 CYC:129 $C7A6:70 03 BVS $C7AB A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU: 34, 1 CYC:132 $C7A8:4C AF C7 JMP $C7AF A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU: 40, 1 CYC:134 $C7AF:EA NOP A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU: 49, 1 CYC:137 $C7B0:A9 00 LDA #$00 A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU: 55, 1 CYC:139 $C7B2:10 04 BPL $C7B8 A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU: 61, 1 CYC:141 $C7B8:EA NOP A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU: 70, 1 CYC:144 $C7B9:A9 80 LDA #$80 A:00 X:00 Y:00 P:nvUbdIZc SP:FB PPU: 76, 1 CYC:146 $C7BB:10 03 BPL $C7C0 A:80 X:00 Y:00 P:NvUbdIzc SP:FB PPU: 82, 1 CYC:148 $C7BD:4C D9 C7 JMP $C7D9 A:80 X:00 Y:00 P:NvUbdIzc SP:FB PPU: 88, 1 CYC:150 $C7D9:EA NOP A:80 X:00 Y:00 P:NvUbdIzc SP:FB PPU: 97, 1 CYC:153 $C7DA:60 RTS A:80 X:00 Y:00 P:NvUbdIzc SP:FB PPU:103, 1 CYC:155 $C600:20 DB C7 JSR $C7DB A:80 X:00 Y:00 P:NvUbdIzc SP:FD PPU:121, 1 CYC:161 $C7DB:EA NOP A:80 X:00 Y:00 P:NvUbdIzc SP:FB PPU:139, 1 CYC:167 $C7DC:A9 FF LDA #$FF A:80 X:00 Y:00 P:NvUbdIzc SP:FB PPU:145, 1 CYC:169 $C7DE:85 01 STA $01 = #$00 A:FF X:00 Y:00 P:NvUbdIzc SP:FB PPU:151, 1 CYC:171 $C7E0:24 01 BIT $01 = #$FF A:FF X:00 Y:00 P:NvUbdIzc SP:FB PPU:160, 1 CYC:174 $C7E2:A9 00 LDA #$00 A:FF X:00 Y:00 P:NVUbdIzc SP:FB PPU:169, 1 CYC:177 $C7E4:38 SEC A:00 X:00 Y:00 P:nVUbdIZc SP:FB PPU:175, 1 CYC:179 $C7E5:78 SEI A:00 X:00 Y:00 P:67 SP:FB PPU:181, 1 CYC:181 $C7E6:F8 SED A:00 X:00 Y:00 P:67 SP:FB PPU:187, 1 CYC:183 $C7E7:08 PHP A:00 X:00 Y:00 P:6F SP:FB PPU:193, 1 CYC:185 $C7E8:68 PLA A:00 X:00 Y:00 P:6F SP:FA PPU:202, 1 CYC:188 $C7E9:29 EF AND #$EF A:7F X:00 Y:00 P:6D SP:FB PPU:214, 1 CYC:192 $C7EB:C9 6F CMP #$6F A:6F X:00 Y:00 P:6D SP:FB PPU:220, 1 CYC:194 $C7ED:F0 04 BEQ $C7F3 A:6F X:00 Y:00 P:6F SP:FB PPU:226, 1 CYC:196 $C7F3:EA NOP A:6F X:00 Y:00 P:6F SP:FB PPU:235, 1 CYC:199 $C7F4:A9 40 LDA #$40 A:6F X:00 Y:00 P:6F SP:FB PPU:241, 1 CYC:201 $C7F6:85 01 STA $01 = #$FF A:40 X:00 Y:00 P:6D SP:FB PPU:247, 1 CYC:203 $C7F8:24 01 BIT $01 = #$40 A:40 X:00 Y:00 P:6D SP:FB PPU:256, 1 CYC:206 $C7FA:D8 CLD A:40 X:00 Y:00 P:6D SP:FB PPU:265, 1 CYC:209 $C7FB:A9 10 LDA #$10 A:40 X:00 Y:00 P:65 SP:FB PPU:271, 1 CYC:211 $C7FD:18 CLC A:10 X:00 Y:00 P:65 SP:FB PPU:277, 1 CYC:213 $C7FE:08 PHP A:10 X:00 Y:00 P:64 SP:FB PPU:283, 1 CYC:215 $C7FF:68 PLA A:10 X:00 Y:00 P:64 SP:FA PPU:292, 1 CYC:218 $C800:29 EF AND #$EF A:74 X:00 Y:00 P:64 SP:FB PPU:304, 1 CYC:222 $C802:C9 64 CMP #$64 A:64 X:00 Y:00 P:64 SP:FB PPU:310, 1 CYC:224 $C804:F0 04 BEQ $C80A A:64 X:00 Y:00 P:67 SP:FB PPU:316, 1 CYC:226 $C80A:EA NOP A:64 X:00 Y:00 P:67 SP:FB PPU:325, 1 CYC:229 $C80B:A9 80 LDA #$80 A:64 X:00 Y:00 P:67 SP:FB PPU:331, 1 CYC:231 $C80D:85 01 STA $01 = #$40 A:80 X:00 Y:00 P:E5 SP:FB PPU:337, 1 CYC:233 $C80F:24 01 BIT $01 = #$80 A:80 X:00 Y:00 P:E5 SP:FB PPU: 5, 2 CYC:236 $C811:F8 SED A:80 X:00 Y:00 P:A5 SP:FB PPU: 14, 2 CYC:239 $C812:A9 00 LDA #$00 A:80 X:00 Y:00 P:AD SP:FB PPU: 20, 2 CYC:241 $C814:38 SEC A:00 X:00 Y:00 P:2F SP:FB PPU: 26, 2 CYC:243 $C815:08 PHP A:00 X:00 Y:00 P:2F SP:FB PPU: 32, 2 CYC:245 $C816:68 PLA A:00 X:00 Y:00 P:2F SP:FA PPU: 41, 2 CYC:248 $C817:29 EF AND #$EF A:3F X:00 Y:00 P:2D SP:FB PPU: 53, 2 CYC:252 $C819:C9 2F CMP #$2F A:2F X:00 Y:00 P:2D SP:FB PPU: 59, 2 CYC:254 $C81B:F0 04 BEQ $C821 A:2F X:00 Y:00 P:2F SP:FB PPU: 65, 2 CYC:256 $C821:EA NOP A:2F X:00 Y:00 P:2F SP:FB PPU: 74, 2 CYC:259 $C822:A9 FF LDA #$FF A:2F X:00 Y:00 P:2F SP:FB PPU: 80, 2 CYC:261 $C824:48 PHA A:FF X:00 Y:00 P:AD SP:FB PPU: 86, 2 CYC:263 $C825:28 PLP A:FF X:00 Y:00 P:AD SP:FA PPU: 95, 2 CYC:266 $C826:D0 09 BNE $C831 A:FF X:00 Y:00 P:EF SP:FB PPU:107, 2 CYC:270 $C828:10 07 BPL $C831 A:FF X:00 Y:00 P:EF SP:FB PPU:113, 2 CYC:272 $C82A:50 05 BVC $C831 A:FF X:00 Y:00 P:EF SP:FB PPU:119, 2 CYC:274 $C82C:90 03 BCC $C831 A:FF X:00 Y:00 P:EF SP:FB PPU:125, 2 CYC:276 $C82E:4C 35 C8 JMP $C835 A:FF X:00 Y:00 P:EF SP:FB PPU:131, 2 CYC:278 $C835:EA NOP A:FF X:00 Y:00 P:EF SP:FB PPU:140, 2 CYC:281 $C836:A9 04 LDA #$04 A:FF X:00 Y:00 P:EF SP:FB PPU:146, 2 CYC:283 $C838:48 PHA A:04 X:00 Y:00 P:6D SP:FB PPU:152, 2 CYC:285 $C839:28 PLP A:04 X:00 Y:00 P:6D SP:FA PPU:161, 2 CYC:288 $C83A:F0 09 BEQ $C845 A:04 X:00 Y:00 P:nvUbdIzc SP:FB PPU:173, 2 CYC:292 $C83C:30 07 BMI $C845 A:04 X:00 Y:00 P:nvUbdIzc SP:FB PPU:179, 2 CYC:294 $C83E:70 05 BVS $C845 A:04 X:00 Y:00 P:nvUbdIzc SP:FB PPU:185, 2 CYC:296 $C840:B0 03 BCS $C845 A:04 X:00 Y:00 P:nvUbdIzc SP:FB PPU:191, 2 CYC:298 $C842:4C 49 C8 JMP $C849 A:04 X:00 Y:00 P:nvUbdIzc SP:FB PPU:197, 2 CYC:300 $C849:EA NOP A:04 X:00 Y:00 P:nvUbdIzc SP:FB PPU:206, 2 CYC:303 $C84A:F8 SED A:04 X:00 Y:00 P:nvUbdIzc SP:FB PPU:212, 2 CYC:305 $C84B:A9 FF LDA #$FF A:04 X:00 Y:00 P:2C SP:FB PPU:218, 2 CYC:307 $C84D:85 01 STA $01 = #$80 A:FF X:00 Y:00 P:AC SP:FB PPU:224, 2 CYC:309 $C84F:24 01 BIT $01 = #$FF A:FF X:00 Y:00 P:AC SP:FB PPU:233, 2 CYC:312 $C851:18 CLC A:FF X:00 Y:00 P:EC SP:FB PPU:242, 2 CYC:315 $C852:A9 00 LDA #$00 A:FF X:00 Y:00 P:EC SP:FB PPU:248, 2 CYC:317 $C854:48 PHA A:00 X:00 Y:00 P:6E SP:FB PPU:254, 2 CYC:319 $C855:A9 FF LDA #$FF A:00 X:00 Y:00 P:6E SP:FA PPU:263, 2 CYC:322 $C857:68 PLA A:FF X:00 Y:00 P:EC SP:FA PPU:269, 2 CYC:324 $C858:D0 09 BNE $C863 A:00 X:00 Y:00 P:6E SP:FB PPU:281, 2 CYC:328 $C85A:30 07 BMI $C863 A:00 X:00 Y:00 P:6E SP:FB PPU:287, 2 CYC:330 $C85C:50 05 BVC $C863 A:00 X:00 Y:00 P:6E SP:FB PPU:293, 2 CYC:332 $C85E:B0 03 BCS $C863 A:00 X:00 Y:00 P:6E SP:FB PPU:299, 2 CYC:334 $C860:4C 67 C8 JMP $C867 A:00 X:00 Y:00 P:6E SP:FB PPU:305, 2 CYC:336 $C867:EA NOP A:00 X:00 Y:00 P:6E SP:FB PPU:314, 2 CYC:339 $C868:A9 00 LDA #$00 A:00 X:00 Y:00 P:6E SP:FB PPU:320, 2 CYC:341 $C86A:85 01 STA $01 = #$FF A:00 X:00 Y:00 P:6E SP:FB PPU:326, 2 CYC:343 $C86C:24 01 BIT $01 = #$00 A:00 X:00 Y:00 P:6E SP:FB PPU:335, 2 CYC:346 $C86E:38 SEC A:00 X:00 Y:00 P:2E SP:FB PPU: 3, 3 CYC:349 $C86F:A9 FF LDA #$FF A:00 X:00 Y:00 P:2F SP:FB PPU: 9, 3 CYC:351 $C871:48 PHA A:FF X:00 Y:00 P:AD SP:FB PPU: 15, 3 CYC:353 $C872:A9 00 LDA #$00 A:FF X:00 Y:00 P:AD SP:FA PPU: 24, 3 CYC:356 $C874:68 PLA A:00 X:00 Y:00 P:2F SP:FA PPU: 30, 3 CYC:358 $C875:F0 09 BEQ $C880 A:FF X:00 Y:00 P:AD SP:FB PPU: 42, 3 CYC:362 $C877:10 07 BPL $C880 A:FF X:00 Y:00 P:AD SP:FB PPU: 48, 3 CYC:364 $C879:70 05 BVS $C880 A:FF X:00 Y:00 P:AD SP:FB PPU: 54, 3 CYC:366 $C87B:90 03 BCC $C880 A:FF X:00 Y:00 P:AD SP:FB PPU: 60, 3 CYC:368 $C87D:4C 84 C8 JMP $C884 A:FF X:00 Y:00 P:AD SP:FB PPU: 66, 3 CYC:370 $C884:60 RTS A:FF X:00 Y:00 P:AD SP:FB PPU: 75, 3 CYC:373 $C603:20 85 C8 JSR $C885 A:FF X:00 Y:00 P:AD SP:FD PPU: 93, 3 CYC:379 $C885:EA NOP A:FF X:00 Y:00 P:AD SP:FB PPU:111, 3 CYC:385 $C886:18 CLC A:FF X:00 Y:00 P:AD SP:FB PPU:117, 3 CYC:387 $C887:A9 FF LDA #$FF A:FF X:00 Y:00 P:AC SP:FB PPU:123, 3 CYC:389 $C889:85 01 STA $01 = #$00 A:FF X:00 Y:00 P:AC SP:FB PPU:129, 3 CYC:391 $C88B:24 01 BIT $01 = #$FF A:FF X:00 Y:00 P:AC SP:FB PPU:138, 3 CYC:394 $C88D:A9 55 LDA #$55 A:FF X:00 Y:00 P:EC SP:FB PPU:147, 3 CYC:397 $C88F:09 AA ORA #$AA A:55 X:00 Y:00 P:6C SP:FB PPU:153, 3 CYC:399 $C891:B0 0B BCS $C89E A:FF X:00 Y:00 P:EC SP:FB PPU:159, 3 CYC:401 $C893:10 09 BPL $C89E A:FF X:00 Y:00 P:EC SP:FB PPU:165, 3 CYC:403 $C895:C9 FF CMP #$FF A:FF X:00 Y:00 P:EC SP:FB PPU:171, 3 CYC:405 $C897:D0 05 BNE $C89E A:FF X:00 Y:00 P:6F SP:FB PPU:177, 3 CYC:407 $C899:50 03 BVC $C89E A:FF X:00 Y:00 P:6F SP:FB PPU:183, 3 CYC:409 $C89B:4C A2 C8 JMP $C8A2 A:FF X:00 Y:00 P:6F SP:FB PPU:189, 3 CYC:411 $C8A2:EA NOP A:FF X:00 Y:00 P:6F SP:FB PPU:198, 3 CYC:414 $C8A3:38 SEC A:FF X:00 Y:00 P:6F SP:FB PPU:204, 3 CYC:416 $C8A4:B8 CLV A:FF X:00 Y:00 P:6F SP:FB PPU:210, 3 CYC:418 $C8A5:A9 00 LDA #$00 A:FF X:00 Y:00 P:2F SP:FB PPU:216, 3 CYC:420 $C8A7:09 00 ORA #$00 A:00 X:00 Y:00 P:2F SP:FB PPU:222, 3 CYC:422 $C8A9:D0 09 BNE $C8B4 A:00 X:00 Y:00 P:2F SP:FB PPU:228, 3 CYC:424 $C8AB:70 07 BVS $C8B4 A:00 X:00 Y:00 P:2F SP:FB PPU:234, 3 CYC:426 $C8AD:90 05 BCC $C8B4 A:00 X:00 Y:00 P:2F SP:FB PPU:240, 3 CYC:428 $C8AF:30 03 BMI $C8B4 A:00 X:00 Y:00 P:2F SP:FB PPU:246, 3 CYC:430 $C8B1:4C B8 C8 JMP $C8B8 A:00 X:00 Y:00 P:2F SP:FB PPU:252, 3 CYC:432 $C8B8:EA NOP A:00 X:00 Y:00 P:2F SP:FB PPU:261, 3 CYC:435 $C8B9:18 CLC A:00 X:00 Y:00 P:2F SP:FB PPU:267, 3 CYC:437 $C8BA:24 01 BIT $01 = #$FF A:00 X:00 Y:00 P:2E SP:FB PPU:273, 3 CYC:439 $C8BC:A9 55 LDA #$55 A:00 X:00 Y:00 P:EE SP:FB PPU:282, 3 CYC:442 $C8BE:29 AA AND #$AA A:55 X:00 Y:00 P:6C SP:FB PPU:288, 3 CYC:444 $C8C0:D0 09 BNE $C8CB A:00 X:00 Y:00 P:6E SP:FB PPU:294, 3 CYC:446 $C8C2:50 07 BVC $C8CB A:00 X:00 Y:00 P:6E SP:FB PPU:300, 3 CYC:448 $C8C4:B0 05 BCS $C8CB A:00 X:00 Y:00 P:6E SP:FB PPU:306, 3 CYC:450 $C8C6:30 03 BMI $C8CB A:00 X:00 Y:00 P:6E SP:FB PPU:312, 3 CYC:452 $C8C8:4C CF C8 JMP $C8CF A:00 X:00 Y:00 P:6E SP:FB PPU:318, 3 CYC:454 $C8CF:EA NOP A:00 X:00 Y:00 P:6E SP:FB PPU:327, 3 CYC:457 $C8D0:38 SEC A:00 X:00 Y:00 P:6E SP:FB PPU:333, 3 CYC:459 $C8D1:B8 CLV A:00 X:00 Y:00 P:6F SP:FB PPU:339, 3 CYC:461 $C8D2:A9 F8 LDA #$F8 A:00 X:00 Y:00 P:2F SP:FB PPU: 4, 4 CYC:463 $C8D4:29 EF AND #$EF A:F8 X:00 Y:00 P:AD SP:FB PPU: 10, 4 CYC:465 $C8D6:90 0B BCC $C8E3 A:E8 X:00 Y:00 P:AD SP:FB PPU: 16, 4 CYC:467 $C8D8:10 09 BPL $C8E3 A:E8 X:00 Y:00 P:AD SP:FB PPU: 22, 4 CYC:469 $C8DA:C9 E8 CMP #$E8 A:E8 X:00 Y:00 P:AD SP:FB PPU: 28, 4 CYC:471 $C8DC:D0 05 BNE $C8E3 A:E8 X:00 Y:00 P:2F SP:FB PPU: 34, 4 CYC:473 $C8DE:70 03 BVS $C8E3 A:E8 X:00 Y:00 P:2F SP:FB PPU: 40, 4 CYC:475 $C8E0:4C E7 C8 JMP $C8E7 A:E8 X:00 Y:00 P:2F SP:FB PPU: 46, 4 CYC:477 $C8E7:EA NOP A:E8 X:00 Y:00 P:2F SP:FB PPU: 55, 4 CYC:480 $C8E8:18 CLC A:E8 X:00 Y:00 P:2F SP:FB PPU: 61, 4 CYC:482 $C8E9:24 01 BIT $01 = #$FF A:E8 X:00 Y:00 P:2E SP:FB PPU: 67, 4 CYC:484 $C8EB:A9 5F LDA #$5F A:E8 X:00 Y:00 P:EC SP:FB PPU: 76, 4 CYC:487 $C8ED:49 AA EOR #$AA A:5F X:00 Y:00 P:6C SP:FB PPU: 82, 4 CYC:489 $C8EF:B0 0B BCS $C8FC A:F5 X:00 Y:00 P:EC SP:FB PPU: 88, 4 CYC:491 $C8F1:10 09 BPL $C8FC A:F5 X:00 Y:00 P:EC SP:FB PPU: 94, 4 CYC:493 $C8F3:C9 F5 CMP #$F5 A:F5 X:00 Y:00 P:EC SP:FB PPU:100, 4 CYC:495 $C8F5:D0 05 BNE $C8FC A:F5 X:00 Y:00 P:6F SP:FB PPU:106, 4 CYC:497 $C8F7:50 03 BVC $C8FC A:F5 X:00 Y:00 P:6F SP:FB PPU:112, 4 CYC:499 $C8F9:4C 00 C9 JMP $C900 A:F5 X:00 Y:00 P:6F SP:FB PPU:118, 4 CYC:501 $C900:EA NOP A:F5 X:00 Y:00 P:6F SP:FB PPU:127, 4 CYC:504 $C901:38 SEC A:F5 X:00 Y:00 P:6F SP:FB PPU:133, 4 CYC:506 $C902:B8 CLV A:F5 X:00 Y:00 P:6F SP:FB PPU:139, 4 CYC:508 $C903:A9 70 LDA #$70 A:F5 X:00 Y:00 P:2F SP:FB PPU:145, 4 CYC:510 $C905:49 70 EOR #$70 A:70 X:00 Y:00 P:2D SP:FB PPU:151, 4 CYC:512 $C907:D0 09 BNE $C912 A:00 X:00 Y:00 P:2F SP:FB PPU:157, 4 CYC:514 $C909:70 07 BVS $C912 A:00 X:00 Y:00 P:2F SP:FB PPU:163, 4 CYC:516 $C90B:90 05 BCC $C912 A:00 X:00 Y:00 P:2F SP:FB PPU:169, 4 CYC:518 $C90D:30 03 BMI $C912 A:00 X:00 Y:00 P:2F SP:FB PPU:175, 4 CYC:520 $C90F:4C 16 C9 JMP $C916 A:00 X:00 Y:00 P:2F SP:FB PPU:181, 4 CYC:522 $C916:EA NOP A:00 X:00 Y:00 P:2F SP:FB PPU:190, 4 CYC:525 $C917:18 CLC A:00 X:00 Y:00 P:2F SP:FB PPU:196, 4 CYC:527 $C918:24 01 BIT $01 = #$FF A:00 X:00 Y:00 P:2E SP:FB PPU:202, 4 CYC:529 $C91A:A9 00 LDA #$00 A:00 X:00 Y:00 P:EE SP:FB PPU:211, 4 CYC:532 $C91C:69 69 ADC #$69 A:00 X:00 Y:00 P:6E SP:FB PPU:217, 4 CYC:534 $C91E:30 0B BMI $C92B A:69 X:00 Y:00 P:2C SP:FB PPU:223, 4 CYC:536 $C920:B0 09 BCS $C92B A:69 X:00 Y:00 P:2C SP:FB PPU:229, 4 CYC:538 $C922:C9 69 CMP #$69 A:69 X:00 Y:00 P:2C SP:FB PPU:235, 4 CYC:540 $C924:D0 05 BNE $C92B A:69 X:00 Y:00 P:2F SP:FB PPU:241, 4 CYC:542 $C926:70 03 BVS $C92B A:69 X:00 Y:00 P:2F SP:FB PPU:247, 4 CYC:544 $C928:4C 2F C9 JMP $C92F A:69 X:00 Y:00 P:2F SP:FB PPU:253, 4 CYC:546 $C92F:EA NOP A:69 X:00 Y:00 P:2F SP:FB PPU:262, 4 CYC:549 $C930:38 SEC A:69 X:00 Y:00 P:2F SP:FB PPU:268, 4 CYC:551 $C931:F8 SED A:69 X:00 Y:00 P:2F SP:FB PPU:274, 4 CYC:553 $C932:24 01 BIT $01 = #$FF A:69 X:00 Y:00 P:2F SP:FB PPU:280, 4 CYC:555 $C934:A9 01 LDA #$01 A:69 X:00 Y:00 P:ED SP:FB PPU:289, 4 CYC:558 $C936:69 69 ADC #$69 A:01 X:00 Y:00 P:6D SP:FB PPU:295, 4 CYC:560 $C938:30 0B BMI $C945 A:6B X:00 Y:00 P:2C SP:FB PPU:301, 4 CYC:562 $C93A:B0 09 BCS $C945 A:6B X:00 Y:00 P:2C SP:FB PPU:307, 4 CYC:564 $C93C:C9 6B CMP #$6B A:6B X:00 Y:00 P:2C SP:FB PPU:313, 4 CYC:566 $C93E:D0 05 BNE $C945 A:6B X:00 Y:00 P:2F SP:FB PPU:319, 4 CYC:568 $C940:70 03 BVS $C945 A:6B X:00 Y:00 P:2F SP:FB PPU:325, 4 CYC:570 $C942:4C 49 C9 JMP $C949 A:6B X:00 Y:00 P:2F SP:FB PPU:331, 4 CYC:572 $C949:EA NOP A:6B X:00 Y:00 P:2F SP:FB PPU:340, 4 CYC:575 $C94A:D8 CLD A:6B X:00 Y:00 P:2F SP:FB PPU: 5, 5 CYC:577 $C94B:38 SEC A:6B X:00 Y:00 P:nvUbdIZC SP:FB PPU: 11, 5 CYC:579 $C94C:B8 CLV A:6B X:00 Y:00 P:nvUbdIZC SP:FB PPU: 17, 5 CYC:581 $C94D:A9 7F LDA #$7F A:6B X:00 Y:00 P:nvUbdIZC SP:FB PPU: 23, 5 CYC:583 $C94F:69 7F ADC #$7F A:7F X:00 Y:00 P:25 SP:FB PPU: 29, 5 CYC:585 $C951:10 0B BPL $C95E A:FF X:00 Y:00 P:NVUbdIzc SP:FB PPU: 35, 5 CYC:587 $C953:B0 09 BCS $C95E A:FF X:00 Y:00 P:NVUbdIzc SP:FB PPU: 41, 5 CYC:589 $C955:C9 FF CMP #$FF A:FF X:00 Y:00 P:NVUbdIzc SP:FB PPU: 47, 5 CYC:591 $C957:D0 05 BNE $C95E A:FF X:00 Y:00 P:67 SP:FB PPU: 53, 5 CYC:593 $C959:50 03 BVC $C95E A:FF X:00 Y:00 P:67 SP:FB PPU: 59, 5 CYC:595 $C95B:4C 62 C9 JMP $C962 A:FF X:00 Y:00 P:67 SP:FB PPU: 65, 5 CYC:597 $C962:EA NOP A:FF X:00 Y:00 P:67 SP:FB PPU: 74, 5 CYC:600 $C963:18 CLC A:FF X:00 Y:00 P:67 SP:FB PPU: 80, 5 CYC:602 $C964:24 01 BIT $01 = #$FF A:FF X:00 Y:00 P:nVUbdIZc SP:FB PPU: 86, 5 CYC:604 $C966:A9 7F LDA #$7F A:FF X:00 Y:00 P:NVUbdIzc SP:FB PPU: 95, 5 CYC:607 $C968:69 80 ADC #$80 A:7F X:00 Y:00 P:64 SP:FB PPU:101, 5 CYC:609 $C96A:10 0B BPL $C977 A:FF X:00 Y:00 P:NvUbdIzc SP:FB PPU:107, 5 CYC:611 $C96C:B0 09 BCS $C977 A:FF X:00 Y:00 P:NvUbdIzc SP:FB PPU:113, 5 CYC:613 $C96E:C9 FF CMP #$FF A:FF X:00 Y:00 P:NvUbdIzc SP:FB PPU:119, 5 CYC:615 $C970:D0 05 BNE $C977 A:FF X:00 Y:00 P:nvUbdIZC SP:FB PPU:125, 5 CYC:617 $C972:70 03 BVS $C977 A:FF X:00 Y:00 P:nvUbdIZC SP:FB PPU:131, 5 CYC:619 $C974:4C 7B C9 JMP $C97B A:FF X:00 Y:00 P:nvUbdIZC SP:FB PPU:137, 5 CYC:621 $C97B:EA NOP A:FF X:00 Y:00 P:nvUbdIZC SP:FB PPU:146, 5 CYC:624 $C97C:38 SEC A:FF X:00 Y:00 P:nvUbdIZC SP:FB PPU:152, 5 CYC:626 $C97D:B8 CLV A:FF X:00 Y:00 P:nvUbdIZC SP:FB PPU:158, 5 CYC:628 $C97E:A9 7F LDA #$7F A:FF X:00 Y:00 P:nvUbdIZC SP:FB PPU:164, 5 CYC:630 $C980:69 80 ADC #$80 A:7F X:00 Y:00 P:25 SP:FB PPU:170, 5 CYC:632 $C982:D0 09 BNE $C98D A:00 X:00 Y:00 P:nvUbdIZC SP:FB PPU:176, 5 CYC:634 $C984:30 07 BMI $C98D A:00 X:00 Y:00 P:nvUbdIZC SP:FB PPU:182, 5 CYC:636 $C986:70 05 BVS $C98D A:00 X:00 Y:00 P:nvUbdIZC SP:FB PPU:188, 5 CYC:638 $C988:90 03 BCC $C98D A:00 X:00 Y:00 P:nvUbdIZC SP:FB PPU:194, 5 CYC:640 $C98A:4C 91 C9 JMP $C991 A:00 X:00 Y:00 P:nvUbdIZC SP:FB PPU:200, 5 CYC:642 $C991:EA NOP A:00 X:00 Y:00 P:nvUbdIZC SP:FB PPU:209, 5 CYC:645 $C992:38 SEC A:00 X:00 Y:00 P:nvUbdIZC SP:FB PPU:215, 5 CYC:647 $C993:B8 CLV A:00 X:00 Y:00 P:nvUbdIZC SP:FB PPU:221, 5 CYC:649 $C994:A9 9F LDA #$9F A:00 X:00 Y:00 P:nvUbdIZC SP:FB PPU:227, 5 CYC:651 $C996:F0 09 BEQ $C9A1 A:9F X:00 Y:00 P:A5 SP:FB PPU:233, 5 CYC:653 $C998:10 07 BPL $C9A1 A:9F X:00 Y:00 P:A5 SP:FB PPU:239, 5 CYC:655 $C99A:70 05 BVS $C9A1 A:9F X:00 Y:00 P:A5 SP:FB PPU:245, 5 CYC:657 $C99C:90 03 BCC $C9A1 A:9F X:00 Y:00 P:A5 SP:FB PPU:251, 5 CYC:659 $C99E:4C A5 C9 JMP $C9A5 A:9F X:00 Y:00 P:A5 SP:FB PPU:257, 5 CYC:661 $C9A5:EA NOP A:9F X:00 Y:00 P:A5 SP:FB PPU:266, 5 CYC:664 $C9A6:18 CLC A:9F X:00 Y:00 P:A5 SP:FB PPU:272, 5 CYC:666 $C9A7:24 01 BIT $01 = #$FF A:9F X:00 Y:00 P:NvUbdIzc SP:FB PPU:278, 5 CYC:668 $C9A9:A9 00 LDA #$00 A:9F X:00 Y:00 P:NVUbdIzc SP:FB PPU:287, 5 CYC:671 $C9AB:D0 09 BNE $C9B6 A:00 X:00 Y:00 P:nVUbdIZc SP:FB PPU:293, 5 CYC:673 $C9AD:30 07 BMI $C9B6 A:00 X:00 Y:00 P:nVUbdIZc SP:FB PPU:299, 5 CYC:675 $C9AF:50 05 BVC $C9B6 A:00 X:00 Y:00 P:nVUbdIZc SP:FB PPU:305, 5 CYC:677 $C9B1:B0 03 BCS $C9B6 A:00 X:00 Y:00 P:nVUbdIZc SP:FB PPU:311, 5 CYC:679 $C9B3:4C BA C9 JMP $C9BA A:00 X:00 Y:00 P:nVUbdIZc SP:FB PPU:317, 5 CYC:681 $C9BA:EA NOP A:00 X:00 Y:00 P:nVUbdIZc SP:FB PPU:326, 5 CYC:684 $C9BB:24 01 BIT $01 = #$FF A:00 X:00 Y:00 P:nVUbdIZc SP:FB PPU:332, 5 CYC:686 $C9BD:A9 40 LDA #$40 A:00 X:00 Y:00 P:E6 SP:FB PPU: 0, 6 CYC:689 $C9BF:C9 40 CMP #$40 A:40 X:00 Y:00 P:64 SP:FB PPU: 6, 6 CYC:691 $C9C1:30 09 BMI $C9CC A:40 X:00 Y:00 P:67 SP:FB PPU: 12, 6 CYC:693 $C9C3:90 07 BCC $C9CC A:40 X:00 Y:00 P:67 SP:FB PPU: 18, 6 CYC:695 $C9C5:D0 05 BNE $C9CC A:40 X:00 Y:00 P:67 SP:FB PPU: 24, 6 CYC:697 $C9C7:50 03 BVC $C9CC A:40 X:00 Y:00 P:67 SP:FB PPU: 30, 6 CYC:699 $C9C9:4C D0 C9 JMP $C9D0 A:40 X:00 Y:00 P:67 SP:FB PPU: 36, 6 CYC:701 $C9D0:EA NOP A:40 X:00 Y:00 P:67 SP:FB PPU: 45, 6 CYC:704 $C9D1:B8 CLV A:40 X:00 Y:00 P:67 SP:FB PPU: 51, 6 CYC:706 $C9D2:C9 3F CMP #$3F A:40 X:00 Y:00 P:nvUbdIZC SP:FB PPU: 57, 6 CYC:708 $C9D4:F0 09 BEQ $C9DF A:40 X:00 Y:00 P:25 SP:FB PPU: 63, 6 CYC:710 $C9D6:30 07 BMI $C9DF A:40 X:00 Y:00 P:25 SP:FB PPU: 69, 6 CYC:712 $C9D8:90 05 BCC $C9DF A:40 X:00 Y:00 P:25 SP:FB PPU: 75, 6 CYC:714 $C9DA:70 03 BVS $C9DF A:40 X:00 Y:00 P:25 SP:FB PPU: 81, 6 CYC:716 $C9DC:4C E3 C9 JMP $C9E3 A:40 X:00 Y:00 P:25 SP:FB PPU: 87, 6 CYC:718 $C9E3:EA NOP A:40 X:00 Y:00 P:25 SP:FB PPU: 96, 6 CYC:721 $C9E4:C9 41 CMP #$41 A:40 X:00 Y:00 P:25 SP:FB PPU:102, 6 CYC:723 $C9E6:F0 07 BEQ $C9EF A:40 X:00 Y:00 P:NvUbdIzc SP:FB PPU:108, 6 CYC:725 $C9E8:10 05 BPL $C9EF A:40 X:00 Y:00 P:NvUbdIzc SP:FB PPU:114, 6 CYC:727 $C9EA:10 03 BPL $C9EF A:40 X:00 Y:00 P:NvUbdIzc SP:FB PPU:120, 6 CYC:729 $C9EC:4C F3 C9 JMP $C9F3 A:40 X:00 Y:00 P:NvUbdIzc SP:FB PPU:126, 6 CYC:731 $C9F3:EA NOP A:40 X:00 Y:00 P:NvUbdIzc SP:FB PPU:135, 6 CYC:734 $C9F4:A9 80 LDA #$80 A:40 X:00 Y:00 P:NvUbdIzc SP:FB PPU:141, 6 CYC:736 $C9F6:C9 00 CMP #$00 A:80 X:00 Y:00 P:NvUbdIzc SP:FB PPU:147, 6 CYC:738 $C9F8:F0 07 BEQ $CA01 A:80 X:00 Y:00 P:A5 SP:FB PPU:153, 6 CYC:740 $C9FA:10 05 BPL $CA01 A:80 X:00 Y:00 P:A5 SP:FB PPU:159, 6 CYC:742 $C9FC:90 03 BCC $CA01 A:80 X:00 Y:00 P:A5 SP:FB PPU:165, 6 CYC:744 $C9FE:4C 05 CA JMP $CA05 A:80 X:00 Y:00 P:A5 SP:FB PPU:171, 6 CYC:746 $CA05:EA NOP A:80 X:00 Y:00 P:A5 SP:FB PPU:180, 6 CYC:749 $CA06:C9 80 CMP #$80 A:80 X:00 Y:00 P:A5 SP:FB PPU:186, 6 CYC:751 $CA08:D0 07 BNE $CA11 A:80 X:00 Y:00 P:nvUbdIZC SP:FB PPU:192, 6 CYC:753 $CA0A:30 05 BMI $CA11 A:80 X:00 Y:00 P:nvUbdIZC SP:FB PPU:198, 6 CYC:755 $CA0C:90 03 BCC $CA11 A:80 X:00 Y:00 P:nvUbdIZC SP:FB PPU:204, 6 CYC:757 $CA0E:4C 15 CA JMP $CA15 A:80 X:00 Y:00 P:nvUbdIZC SP:FB PPU:210, 6 CYC:759 $CA15:EA NOP A:80 X:00 Y:00 P:nvUbdIZC SP:FB PPU:219, 6 CYC:762 $CA16:C9 81 CMP #$81 A:80 X:00 Y:00 P:nvUbdIZC SP:FB PPU:225, 6 CYC:764 $CA18:B0 07 BCS $CA21 A:80 X:00 Y:00 P:NvUbdIzc SP:FB PPU:231, 6 CYC:766 $CA1A:F0 05 BEQ $CA21 A:80 X:00 Y:00 P:NvUbdIzc SP:FB PPU:237, 6 CYC:768 $CA1C:10 03 BPL $CA21 A:80 X:00 Y:00 P:NvUbdIzc SP:FB PPU:243, 6 CYC:770 $CA1E:4C 25 CA JMP $CA25 A:80 X:00 Y:00 P:NvUbdIzc SP:FB PPU:249, 6 CYC:772 $CA25:EA NOP A:80 X:00 Y:00 P:NvUbdIzc SP:FB PPU:258, 6 CYC:775 $CA26:C9 7F CMP #$7F A:80 X:00 Y:00 P:NvUbdIzc SP:FB PPU:264, 6 CYC:777 $CA28:90 07 BCC $CA31 A:80 X:00 Y:00 P:25 SP:FB PPU:270, 6 CYC:779 $CA2A:F0 05 BEQ $CA31 A:80 X:00 Y:00 P:25 SP:FB PPU:276, 6 CYC:781 $CA2C:30 03 BMI $CA31 A:80 X:00 Y:00 P:25 SP:FB PPU:282, 6 CYC:783 $CA2E:4C 35 CA JMP $CA35 A:80 X:00 Y:00 P:25 SP:FB PPU:288, 6 CYC:785 $CA35:EA NOP A:80 X:00 Y:00 P:25 SP:FB PPU:297, 6 CYC:788 $CA36:24 01 BIT $01 = #$FF A:80 X:00 Y:00 P:25 SP:FB PPU:303, 6 CYC:790 $CA38:A0 40 LDY #$40 A:80 X:00 Y:00 P:E5 SP:FB PPU:312, 6 CYC:793 $CA3A:C0 40 CPY #$40 A:80 X:00 Y:40 P:65 SP:FB PPU:318, 6 CYC:795 $CA3C:D0 09 BNE $CA47 A:80 X:00 Y:40 P:67 SP:FB PPU:324, 6 CYC:797 $CA3E:30 07 BMI $CA47 A:80 X:00 Y:40 P:67 SP:FB PPU:330, 6 CYC:799 $CA40:90 05 BCC $CA47 A:80 X:00 Y:40 P:67 SP:FB PPU:336, 6 CYC:801 $CA42:50 03 BVC $CA47 A:80 X:00 Y:40 P:67 SP:FB PPU: 1, 7 CYC:803 $CA44:4C 4B CA JMP $CA4B A:80 X:00 Y:40 P:67 SP:FB PPU: 7, 7 CYC:805 $CA4B:EA NOP A:80 X:00 Y:40 P:67 SP:FB PPU: 16, 7 CYC:808 $CA4C:B8 CLV A:80 X:00 Y:40 P:67 SP:FB PPU: 22, 7 CYC:810 $CA4D:C0 3F CPY #$3F A:80 X:00 Y:40 P:nvUbdIZC SP:FB PPU: 28, 7 CYC:812 $CA4F:F0 09 BEQ $CA5A A:80 X:00 Y:40 P:25 SP:FB PPU: 34, 7 CYC:814 $CA51:30 07 BMI $CA5A A:80 X:00 Y:40 P:25 SP:FB PPU: 40, 7 CYC:816 $CA53:90 05 BCC $CA5A A:80 X:00 Y:40 P:25 SP:FB PPU: 46, 7 CYC:818 $CA55:70 03 BVS $CA5A A:80 X:00 Y:40 P:25 SP:FB PPU: 52, 7 CYC:820 $CA57:4C 5E CA JMP $CA5E A:80 X:00 Y:40 P:25 SP:FB PPU: 58, 7 CYC:822 $CA5E:EA NOP A:80 X:00 Y:40 P:25 SP:FB PPU: 67, 7 CYC:825 $CA5F:C0 41 CPY #$41 A:80 X:00 Y:40 P:25 SP:FB PPU: 73, 7 CYC:827 $CA61:F0 07 BEQ $CA6A A:80 X:00 Y:40 P:NvUbdIzc SP:FB PPU: 79, 7 CYC:829 $CA63:10 05 BPL $CA6A A:80 X:00 Y:40 P:NvUbdIzc SP:FB PPU: 85, 7 CYC:831 $CA65:10 03 BPL $CA6A A:80 X:00 Y:40 P:NvUbdIzc SP:FB PPU: 91, 7 CYC:833 $CA67:4C 6E CA JMP $CA6E A:80 X:00 Y:40 P:NvUbdIzc SP:FB PPU: 97, 7 CYC:835 $CA6E:EA NOP A:80 X:00 Y:40 P:NvUbdIzc SP:FB PPU:106, 7 CYC:838 $CA6F:A0 80 LDY #$80 A:80 X:00 Y:40 P:NvUbdIzc SP:FB PPU:112, 7 CYC:840 $CA71:C0 00 CPY #$00 A:80 X:00 Y:80 P:NvUbdIzc SP:FB PPU:118, 7 CYC:842 $CA73:F0 07 BEQ $CA7C A:80 X:00 Y:80 P:A5 SP:FB PPU:124, 7 CYC:844 $CA75:10 05 BPL $CA7C A:80 X:00 Y:80 P:A5 SP:FB PPU:130, 7 CYC:846 $CA77:90 03 BCC $CA7C A:80 X:00 Y:80 P:A5 SP:FB PPU:136, 7 CYC:848 $CA79:4C 80 CA JMP $CA80 A:80 X:00 Y:80 P:A5 SP:FB PPU:142, 7 CYC:850 $CA80:EA NOP A:80 X:00 Y:80 P:A5 SP:FB PPU:151, 7 CYC:853 $CA81:C0 80 CPY #$80 A:80 X:00 Y:80 P:A5 SP:FB PPU:157, 7 CYC:855 $CA83:D0 07 BNE $CA8C A:80 X:00 Y:80 P:nvUbdIZC SP:FB PPU:163, 7 CYC:857 $CA85:30 05 BMI $CA8C A:80 X:00 Y:80 P:nvUbdIZC SP:FB PPU:169, 7 CYC:859 $CA87:90 03 BCC $CA8C A:80 X:00 Y:80 P:nvUbdIZC SP:FB PPU:175, 7 CYC:861 $CA89:4C 90 CA JMP $CA90 A:80 X:00 Y:80 P:nvUbdIZC SP:FB PPU:181, 7 CYC:863 $CA90:EA NOP A:80 X:00 Y:80 P:nvUbdIZC SP:FB PPU:190, 7 CYC:866 $CA91:C0 81 CPY #$81 A:80 X:00 Y:80 P:nvUbdIZC SP:FB PPU:196, 7 CYC:868 $CA93:B0 07 BCS $CA9C A:80 X:00 Y:80 P:NvUbdIzc SP:FB PPU:202, 7 CYC:870 $CA95:F0 05 BEQ $CA9C A:80 X:00 Y:80 P:NvUbdIzc SP:FB PPU:208, 7 CYC:872 $CA97:10 03 BPL $CA9C A:80 X:00 Y:80 P:NvUbdIzc SP:FB PPU:214, 7 CYC:874 $CA99:4C A0 CA JMP $CAA0 A:80 X:00 Y:80 P:NvUbdIzc SP:FB PPU:220, 7 CYC:876 $CAA0:EA NOP A:80 X:00 Y:80 P:NvUbdIzc SP:FB PPU:229, 7 CYC:879 $CAA1:C0 7F CPY #$7F A:80 X:00 Y:80 P:NvUbdIzc SP:FB PPU:235, 7 CYC:881 $CAA3:90 07 BCC $CAAC A:80 X:00 Y:80 P:25 SP:FB PPU:241, 7 CYC:883 $CAA5:F0 05 BEQ $CAAC A:80 X:00 Y:80 P:25 SP:FB PPU:247, 7 CYC:885 $CAA7:30 03 BMI $CAAC A:80 X:00 Y:80 P:25 SP:FB PPU:253, 7 CYC:887 $CAA9:4C B0 CA JMP $CAB0 A:80 X:00 Y:80 P:25 SP:FB PPU:259, 7 CYC:889 $CAB0:EA NOP A:80 X:00 Y:80 P:25 SP:FB PPU:268, 7 CYC:892 $CAB1:24 01 BIT $01 = #$FF A:80 X:00 Y:80 P:25 SP:FB PPU:274, 7 CYC:894 $CAB3:A2 40 LDX #$40 A:80 X:00 Y:80 P:E5 SP:FB PPU:283, 7 CYC:897 $CAB5:E0 40 CPX #$40 A:80 X:40 Y:80 P:65 SP:FB PPU:289, 7 CYC:899 $CAB7:D0 09 BNE $CAC2 A:80 X:40 Y:80 P:67 SP:FB PPU:295, 7 CYC:901 $CAB9:30 07 BMI $CAC2 A:80 X:40 Y:80 P:67 SP:FB PPU:301, 7 CYC:903 $CABB:90 05 BCC $CAC2 A:80 X:40 Y:80 P:67 SP:FB PPU:307, 7 CYC:905 $CABD:50 03 BVC $CAC2 A:80 X:40 Y:80 P:67 SP:FB PPU:313, 7 CYC:907 $CABF:4C C6 CA JMP $CAC6 A:80 X:40 Y:80 P:67 SP:FB PPU:319, 7 CYC:909 $CAC6:EA NOP A:80 X:40 Y:80 P:67 SP:FB PPU:328, 7 CYC:912 $CAC7:B8 CLV A:80 X:40 Y:80 P:67 SP:FB PPU:334, 7 CYC:914 $CAC8:E0 3F CPX #$3F A:80 X:40 Y:80 P:nvUbdIZC SP:FB PPU:340, 7 CYC:916 $CACA:F0 09 BEQ $CAD5 A:80 X:40 Y:80 P:25 SP:FB PPU: 5, 8 CYC:918 $CACC:30 07 BMI $CAD5 A:80 X:40 Y:80 P:25 SP:FB PPU: 11, 8 CYC:920 $CACE:90 05 BCC $CAD5 A:80 X:40 Y:80 P:25 SP:FB PPU: 17, 8 CYC:922 $CAD0:70 03 BVS $CAD5 A:80 X:40 Y:80 P:25 SP:FB PPU: 23, 8 CYC:924 $CAD2:4C D9 CA JMP $CAD9 A:80 X:40 Y:80 P:25 SP:FB PPU: 29, 8 CYC:926 $CAD9:EA NOP A:80 X:40 Y:80 P:25 SP:FB PPU: 38, 8 CYC:929 $CADA:E0 41 CPX #$41 A:80 X:40 Y:80 P:25 SP:FB PPU: 44, 8 CYC:931 $CADC:F0 07 BEQ $CAE5 A:80 X:40 Y:80 P:NvUbdIzc SP:FB PPU: 50, 8 CYC:933 $CADE:10 05 BPL $CAE5 A:80 X:40 Y:80 P:NvUbdIzc SP:FB PPU: 56, 8 CYC:935 $CAE0:10 03 BPL $CAE5 A:80 X:40 Y:80 P:NvUbdIzc SP:FB PPU: 62, 8 CYC:937 $CAE2:4C E9 CA JMP $CAE9 A:80 X:40 Y:80 P:NvUbdIzc SP:FB PPU: 68, 8 CYC:939 $CAE9:EA NOP A:80 X:40 Y:80 P:NvUbdIzc SP:FB PPU: 77, 8 CYC:942 $CAEA:A2 80 LDX #$80 A:80 X:40 Y:80 P:NvUbdIzc SP:FB PPU: 83, 8 CYC:944 $CAEC:E0 00 CPX #$00 A:80 X:80 Y:80 P:NvUbdIzc SP:FB PPU: 89, 8 CYC:946 $CAEE:F0 07 BEQ $CAF7 A:80 X:80 Y:80 P:A5 SP:FB PPU: 95, 8 CYC:948 $CAF0:10 05 BPL $CAF7 A:80 X:80 Y:80 P:A5 SP:FB PPU:101, 8 CYC:950 $CAF2:90 03 BCC $CAF7 A:80 X:80 Y:80 P:A5 SP:FB PPU:107, 8 CYC:952 $CAF4:4C FB CA JMP $CAFB A:80 X:80 Y:80 P:A5 SP:FB PPU:113, 8 CYC:954 $CAFB:EA NOP A:80 X:80 Y:80 P:A5 SP:FB PPU:122, 8 CYC:957 $CAFC:E0 80 CPX #$80 A:80 X:80 Y:80 P:A5 SP:FB PPU:128, 8 CYC:959 $CAFE:D0 07 BNE $CB07 A:80 X:80 Y:80 P:nvUbdIZC SP:FB PPU:134, 8 CYC:961 $CB00:30 05 BMI $CB07 A:80 X:80 Y:80 P:nvUbdIZC SP:FB PPU:140, 8 CYC:963 $CB02:90 03 BCC $CB07 A:80 X:80 Y:80 P:nvUbdIZC SP:FB PPU:146, 8 CYC:965 $CB04:4C 0B CB JMP $CB0B A:80 X:80 Y:80 P:nvUbdIZC SP:FB PPU:152, 8 CYC:967 $CB0B:EA NOP A:80 X:80 Y:80 P:nvUbdIZC SP:FB PPU:161, 8 CYC:970 $CB0C:E0 81 CPX #$81 A:80 X:80 Y:80 P:nvUbdIZC SP:FB PPU:167, 8 CYC:972 $CB0E:B0 07 BCS $CB17 A:80 X:80 Y:80 P:NvUbdIzc SP:FB PPU:173, 8 CYC:974 $CB10:F0 05 BEQ $CB17 A:80 X:80 Y:80 P:NvUbdIzc SP:FB PPU:179, 8 CYC:976 $CB12:10 03 BPL $CB17 A:80 X:80 Y:80 P:NvUbdIzc SP:FB PPU:185, 8 CYC:978 $CB14:4C 1B CB JMP $CB1B A:80 X:80 Y:80 P:NvUbdIzc SP:FB PPU:191, 8 CYC:980 $CB1B:EA NOP A:80 X:80 Y:80 P:NvUbdIzc SP:FB PPU:200, 8 CYC:983 $CB1C:E0 7F CPX #$7F A:80 X:80 Y:80 P:NvUbdIzc SP:FB PPU:206, 8 CYC:985 $CB1E:90 07 BCC $CB27 A:80 X:80 Y:80 P:25 SP:FB PPU:212, 8 CYC:987 $CB20:F0 05 BEQ $CB27 A:80 X:80 Y:80 P:25 SP:FB PPU:218, 8 CYC:989 $CB22:30 03 BMI $CB27 A:80 X:80 Y:80 P:25 SP:FB PPU:224, 8 CYC:991 $CB24:4C 2B CB JMP $CB2B A:80 X:80 Y:80 P:25 SP:FB PPU:230, 8 CYC:993 $CB2B:EA NOP A:80 X:80 Y:80 P:25 SP:FB PPU:239, 8 CYC:996 $CB2C:38 SEC A:80 X:80 Y:80 P:25 SP:FB PPU:245, 8 CYC:998 $CB2D:B8 CLV A:80 X:80 Y:80 P:25 SP:FB PPU:251, 8 CYC:1000 $CB2E:A2 9F LDX #$9F A:80 X:80 Y:80 P:25 SP:FB PPU:257, 8 CYC:1002 $CB30:F0 09 BEQ $CB3B A:80 X:9F Y:80 P:A5 SP:FB PPU:263, 8 CYC:1004 $CB32:10 07 BPL $CB3B A:80 X:9F Y:80 P:A5 SP:FB PPU:269, 8 CYC:1006 $CB34:70 05 BVS $CB3B A:80 X:9F Y:80 P:A5 SP:FB PPU:275, 8 CYC:1008 $CB36:90 03 BCC $CB3B A:80 X:9F Y:80 P:A5 SP:FB PPU:281, 8 CYC:1010 $CB38:4C 3F CB JMP $CB3F A:80 X:9F Y:80 P:A5 SP:FB PPU:287, 8 CYC:1012 $CB3F:EA NOP A:80 X:9F Y:80 P:A5 SP:FB PPU:296, 8 CYC:1015 $CB40:18 CLC A:80 X:9F Y:80 P:A5 SP:FB PPU:302, 8 CYC:1017 $CB41:24 01 BIT $01 = #$FF A:80 X:9F Y:80 P:NvUbdIzc SP:FB PPU:308, 8 CYC:1019 $CB43:A2 00 LDX #$00 A:80 X:9F Y:80 P:NVUbdIzc SP:FB PPU:317, 8 CYC:1022 $CB45:D0 09 BNE $CB50 A:80 X:00 Y:80 P:nVUbdIZc SP:FB PPU:323, 8 CYC:1024 $CB47:30 07 BMI $CB50 A:80 X:00 Y:80 P:nVUbdIZc SP:FB PPU:329, 8 CYC:1026 $CB49:50 05 BVC $CB50 A:80 X:00 Y:80 P:nVUbdIZc SP:FB PPU:335, 8 CYC:1028 $CB4B:B0 03 BCS $CB50 A:80 X:00 Y:80 P:nVUbdIZc SP:FB PPU: 0, 9 CYC:1030 $CB4D:4C 54 CB JMP $CB54 A:80 X:00 Y:80 P:nVUbdIZc SP:FB PPU: 6, 9 CYC:1032 $CB54:EA NOP A:80 X:00 Y:80 P:nVUbdIZc SP:FB PPU: 15, 9 CYC:1035 $CB55:38 SEC A:80 X:00 Y:80 P:nVUbdIZc SP:FB PPU: 21, 9 CYC:1037 $CB56:B8 CLV A:80 X:00 Y:80 P:67 SP:FB PPU: 27, 9 CYC:1039 $CB57:A0 9F LDY #$9F A:80 X:00 Y:80 P:nvUbdIZC SP:FB PPU: 33, 9 CYC:1041 $CB59:F0 09 BEQ $CB64 A:80 X:00 Y:9F P:A5 SP:FB PPU: 39, 9 CYC:1043 $CB5B:10 07 BPL $CB64 A:80 X:00 Y:9F P:A5 SP:FB PPU: 45, 9 CYC:1045 $CB5D:70 05 BVS $CB64 A:80 X:00 Y:9F P:A5 SP:FB PPU: 51, 9 CYC:1047 $CB5F:90 03 BCC $CB64 A:80 X:00 Y:9F P:A5 SP:FB PPU: 57, 9 CYC:1049 $CB61:4C 68 CB JMP $CB68 A:80 X:00 Y:9F P:A5 SP:FB PPU: 63, 9 CYC:1051 $CB68:EA NOP A:80 X:00 Y:9F P:A5 SP:FB PPU: 72, 9 CYC:1054 $CB69:18 CLC A:80 X:00 Y:9F P:A5 SP:FB PPU: 78, 9 CYC:1056 $CB6A:24 01 BIT $01 = #$FF A:80 X:00 Y:9F P:NvUbdIzc SP:FB PPU: 84, 9 CYC:1058 $CB6C:A0 00 LDY #$00 A:80 X:00 Y:9F P:NVUbdIzc SP:FB PPU: 93, 9 CYC:1061 $CB6E:D0 09 BNE $CB79 A:80 X:00 Y:00 P:nVUbdIZc SP:FB PPU: 99, 9 CYC:1063 $CB70:30 07 BMI $CB79 A:80 X:00 Y:00 P:nVUbdIZc SP:FB PPU:105, 9 CYC:1065 $CB72:50 05 BVC $CB79 A:80 X:00 Y:00 P:nVUbdIZc SP:FB PPU:111, 9 CYC:1067 $CB74:B0 03 BCS $CB79 A:80 X:00 Y:00 P:nVUbdIZc SP:FB PPU:117, 9 CYC:1069 $CB76:4C 7D CB JMP $CB7D A:80 X:00 Y:00 P:nVUbdIZc SP:FB PPU:123, 9 CYC:1071 $CB7D:EA NOP A:80 X:00 Y:00 P:nVUbdIZc SP:FB PPU:132, 9 CYC:1074 $CB7E:A9 55 LDA #$55 A:80 X:00 Y:00 P:nVUbdIZc SP:FB PPU:138, 9 CYC:1076 $CB80:A2 AA LDX #$AA A:55 X:00 Y:00 P:64 SP:FB PPU:144, 9 CYC:1078 $CB82:A0 33 LDY #$33 A:55 X:AA Y:00 P:NVUbdIzc SP:FB PPU:150, 9 CYC:1080 $CB84:C9 55 CMP #$55 A:55 X:AA Y:33 P:64 SP:FB PPU:156, 9 CYC:1082 $CB86:D0 23 BNE $CBAB A:55 X:AA Y:33 P:67 SP:FB PPU:162, 9 CYC:1084 $CB88:E0 AA CPX #$AA A:55 X:AA Y:33 P:67 SP:FB PPU:168, 9 CYC:1086 $CB8A:D0 1F BNE $CBAB A:55 X:AA Y:33 P:67 SP:FB PPU:174, 9 CYC:1088 $CB8C:C0 33 CPY #$33 A:55 X:AA Y:33 P:67 SP:FB PPU:180, 9 CYC:1090 $CB8E:D0 1B BNE $CBAB A:55 X:AA Y:33 P:67 SP:FB PPU:186, 9 CYC:1092 $CB90:C9 55 CMP #$55 A:55 X:AA Y:33 P:67 SP:FB PPU:192, 9 CYC:1094 $CB92:D0 17 BNE $CBAB A:55 X:AA Y:33 P:67 SP:FB PPU:198, 9 CYC:1096 $CB94:E0 AA CPX #$AA A:55 X:AA Y:33 P:67 SP:FB PPU:204, 9 CYC:1098 $CB96:D0 13 BNE $CBAB A:55 X:AA Y:33 P:67 SP:FB PPU:210, 9 CYC:1100 $CB98:C0 33 CPY #$33 A:55 X:AA Y:33 P:67 SP:FB PPU:216, 9 CYC:1102 $CB9A:D0 0F BNE $CBAB A:55 X:AA Y:33 P:67 SP:FB PPU:222, 9 CYC:1104 $CB9C:C9 56 CMP #$56 A:55 X:AA Y:33 P:67 SP:FB PPU:228, 9 CYC:1106 $CB9E:F0 0B BEQ $CBAB A:55 X:AA Y:33 P:NVUbdIzc SP:FB PPU:234, 9 CYC:1108 $CBA0:E0 AB CPX #$AB A:55 X:AA Y:33 P:NVUbdIzc SP:FB PPU:240, 9 CYC:1110 $CBA2:F0 07 BEQ $CBAB A:55 X:AA Y:33 P:NVUbdIzc SP:FB PPU:246, 9 CYC:1112 $CBA4:C0 34 CPY #$34 A:55 X:AA Y:33 P:NVUbdIzc SP:FB PPU:252, 9 CYC:1114 $CBA6:F0 03 BEQ $CBAB A:55 X:AA Y:33 P:NVUbdIzc SP:FB PPU:258, 9 CYC:1116 $CBA8:4C AF CB JMP $CBAF A:55 X:AA Y:33 P:NVUbdIzc SP:FB PPU:264, 9 CYC:1118 $CBAF:A0 71 LDY #$71 A:55 X:AA Y:33 P:NVUbdIzc SP:FB PPU:273, 9 CYC:1121 $CBB1:20 31 F9 JSR $F931 A:55 X:AA Y:71 P:64 SP:FB PPU:279, 9 CYC:1123 $F931:24 01 BIT $01 = #$FF A:55 X:AA Y:71 P:64 SP:F9 PPU:297, 9 CYC:1129 $F933:A9 40 LDA #$40 A:55 X:AA Y:71 P:NVUbdIzc SP:F9 PPU:306, 9 CYC:1132 $F935:38 SEC A:40 X:AA Y:71 P:64 SP:F9 PPU:312, 9 CYC:1134 $F936:60 RTS A:40 X:AA Y:71 P:65 SP:F9 PPU:318, 9 CYC:1136 $CBB4:E9 40 SBC #$40 A:40 X:AA Y:71 P:65 SP:FB PPU:336, 9 CYC:1142 $CBB6:20 37 F9 JSR $F937 A:00 X:AA Y:71 P:nvUbdIZC SP:FB PPU: 1, 10 CYC:1144 $F937:30 0B BMI $F944 A:00 X:AA Y:71 P:nvUbdIZC SP:F9 PPU: 19, 10 CYC:1150 $F939:90 09 BCC $F944 A:00 X:AA Y:71 P:nvUbdIZC SP:F9 PPU: 25, 10 CYC:1152 $F93B:D0 07 BNE $F944 A:00 X:AA Y:71 P:nvUbdIZC SP:F9 PPU: 31, 10 CYC:1154 $F93D:70 05 BVS $F944 A:00 X:AA Y:71 P:nvUbdIZC SP:F9 PPU: 37, 10 CYC:1156 $F93F:C9 00 CMP #$00 A:00 X:AA Y:71 P:nvUbdIZC SP:F9 PPU: 43, 10 CYC:1158 $F941:D0 01 BNE $F944 A:00 X:AA Y:71 P:nvUbdIZC SP:F9 PPU: 49, 10 CYC:1160 $F943:60 RTS A:00 X:AA Y:71 P:nvUbdIZC SP:F9 PPU: 55, 10 CYC:1162 $CBB9:C8 INY A:00 X:AA Y:71 P:nvUbdIZC SP:FB PPU: 73, 10 CYC:1168 $CBBA:20 47 F9 JSR $F947 A:00 X:AA Y:72 P:25 SP:FB PPU: 79, 10 CYC:1170 $F947:B8 CLV A:00 X:AA Y:72 P:25 SP:F9 PPU: 97, 10 CYC:1176 $F948:38 SEC A:00 X:AA Y:72 P:25 SP:F9 PPU:103, 10 CYC:1178 $F949:A9 40 LDA #$40 A:00 X:AA Y:72 P:25 SP:F9 PPU:109, 10 CYC:1180 $F94B:60 RTS A:40 X:AA Y:72 P:25 SP:F9 PPU:115, 10 CYC:1182 $CBBD:E9 3F SBC #$3F A:40 X:AA Y:72 P:25 SP:FB PPU:133, 10 CYC:1188 $CBBF:20 4C F9 JSR $F94C A:01 X:AA Y:72 P:25 SP:FB PPU:139, 10 CYC:1190 $F94C:F0 0B BEQ $F959 A:01 X:AA Y:72 P:25 SP:F9 PPU:157, 10 CYC:1196 $F94E:30 09 BMI $F959 A:01 X:AA Y:72 P:25 SP:F9 PPU:163, 10 CYC:1198 $F950:90 07 BCC $F959 A:01 X:AA Y:72 P:25 SP:F9 PPU:169, 10 CYC:1200 $F952:70 05 BVS $F959 A:01 X:AA Y:72 P:25 SP:F9 PPU:175, 10 CYC:1202 $F954:C9 01 CMP #$01 A:01 X:AA Y:72 P:25 SP:F9 PPU:181, 10 CYC:1204 $F956:D0 01 BNE $F959 A:01 X:AA Y:72 P:nvUbdIZC SP:F9 PPU:187, 10 CYC:1206 $F958:60 RTS A:01 X:AA Y:72 P:nvUbdIZC SP:F9 PPU:193, 10 CYC:1208 $CBC2:C8 INY A:01 X:AA Y:72 P:nvUbdIZC SP:FB PPU:211, 10 CYC:1214 $CBC3:20 5C F9 JSR $F95C A:01 X:AA Y:73 P:25 SP:FB PPU:217, 10 CYC:1216 $F95C:A9 40 LDA #$40 A:01 X:AA Y:73 P:25 SP:F9 PPU:235, 10 CYC:1222 $F95E:38 SEC A:40 X:AA Y:73 P:25 SP:F9 PPU:241, 10 CYC:1224 $F95F:24 01 BIT $01 = #$FF A:40 X:AA Y:73 P:25 SP:F9 PPU:247, 10 CYC:1226 $F961:60 RTS A:40 X:AA Y:73 P:E5 SP:F9 PPU:256, 10 CYC:1229 $CBC6:E9 41 SBC #$41 A:40 X:AA Y:73 P:E5 SP:FB PPU:274, 10 CYC:1235 $CBC8:20 62 F9 JSR $F962 A:FF X:AA Y:73 P:NvUbdIzc SP:FB PPU:280, 10 CYC:1237 $F962:B0 0B BCS $F96F A:FF X:AA Y:73 P:NvUbdIzc SP:F9 PPU:298, 10 CYC:1243 $F964:F0 09 BEQ $F96F A:FF X:AA Y:73 P:NvUbdIzc SP:F9 PPU:304, 10 CYC:1245 $F966:10 07 BPL $F96F A:FF X:AA Y:73 P:NvUbdIzc SP:F9 PPU:310, 10 CYC:1247 $F968:70 05 BVS $F96F A:FF X:AA Y:73 P:NvUbdIzc SP:F9 PPU:316, 10 CYC:1249 $F96A:C9 FF CMP #$FF A:FF X:AA Y:73 P:NvUbdIzc SP:F9 PPU:322, 10 CYC:1251 $F96C:D0 01 BNE $F96F A:FF X:AA Y:73 P:nvUbdIZC SP:F9 PPU:328, 10 CYC:1253 $F96E:60 RTS A:FF X:AA Y:73 P:nvUbdIZC SP:F9 PPU:334, 10 CYC:1255 $CBCB:C8 INY A:FF X:AA Y:73 P:nvUbdIZC SP:FB PPU: 11, 11 CYC:1261 $CBCC:20 72 F9 JSR $F972 A:FF X:AA Y:74 P:25 SP:FB PPU: 17, 11 CYC:1263 $F972:18 CLC A:FF X:AA Y:74 P:25 SP:F9 PPU: 35, 11 CYC:1269 $F973:A9 80 LDA #$80 A:FF X:AA Y:74 P:nvUbdIzc SP:F9 PPU: 41, 11 CYC:1271 $F975:60 RTS A:80 X:AA Y:74 P:NvUbdIzc SP:F9 PPU: 47, 11 CYC:1273 $CBCF:E9 00 SBC #$00 A:80 X:AA Y:74 P:NvUbdIzc SP:FB PPU: 65, 11 CYC:1279 $CBD1:20 76 F9 JSR $F976 A:7F X:AA Y:74 P:65 SP:FB PPU: 71, 11 CYC:1281 $F976:90 05 BCC $F97D A:7F X:AA Y:74 P:65 SP:F9 PPU: 89, 11 CYC:1287 $F978:C9 7F CMP #$7F A:7F X:AA Y:74 P:65 SP:F9 PPU: 95, 11 CYC:1289 $F97A:D0 01 BNE $F97D A:7F X:AA Y:74 P:67 SP:F9 PPU:101, 11 CYC:1291 $F97C:60 RTS A:7F X:AA Y:74 P:67 SP:F9 PPU:107, 11 CYC:1293 $CBD4:C8 INY A:7F X:AA Y:74 P:67 SP:FB PPU:125, 11 CYC:1299 $CBD5:20 80 F9 JSR $F980 A:7F X:AA Y:75 P:65 SP:FB PPU:131, 11 CYC:1301 $F980:38 SEC A:7F X:AA Y:75 P:65 SP:F9 PPU:149, 11 CYC:1307 $F981:A9 81 LDA #$81 A:7F X:AA Y:75 P:65 SP:F9 PPU:155, 11 CYC:1309 $F983:60 RTS A:81 X:AA Y:75 P:E5 SP:F9 PPU:161, 11 CYC:1311 $CBD8:E9 7F SBC #$7F A:81 X:AA Y:75 P:E5 SP:FB PPU:179, 11 CYC:1317 $CBDA:20 84 F9 JSR $F984 A:02 X:AA Y:75 P:65 SP:FB PPU:185, 11 CYC:1319 $F984:50 07 BVC $F98D A:02 X:AA Y:75 P:65 SP:F9 PPU:203, 11 CYC:1325 $F986:90 05 BCC $F98D A:02 X:AA Y:75 P:65 SP:F9 PPU:209, 11 CYC:1327 $F988:C9 02 CMP #$02 A:02 X:AA Y:75 P:65 SP:F9 PPU:215, 11 CYC:1329 $F98A:D0 01 BNE $F98D A:02 X:AA Y:75 P:67 SP:F9 PPU:221, 11 CYC:1331 $F98C:60 RTS A:02 X:AA Y:75 P:67 SP:F9 PPU:227, 11 CYC:1333 $CBDD:60 RTS A:02 X:AA Y:75 P:67 SP:FB PPU:245, 11 CYC:1339 $C606:20 DE CB JSR $CBDE A:02 X:AA Y:75 P:67 SP:FD PPU:263, 11 CYC:1345 $CBDE:EA NOP A:02 X:AA Y:75 P:67 SP:FB PPU:281, 11 CYC:1351 $CBDF:A9 FF LDA #$FF A:02 X:AA Y:75 P:67 SP:FB PPU:287, 11 CYC:1353 $CBE1:85 01 STA $01 = #$FF A:FF X:AA Y:75 P:E5 SP:FB PPU:293, 11 CYC:1355 $CBE3:A9 44 LDA #$44 A:FF X:AA Y:75 P:E5 SP:FB PPU:302, 11 CYC:1358 $CBE5:A2 55 LDX #$55 A:44 X:AA Y:75 P:65 SP:FB PPU:308, 11 CYC:1360 $CBE7:A0 66 LDY #$66 A:44 X:55 Y:75 P:65 SP:FB PPU:314, 11 CYC:1362 $CBE9:E8 INX A:44 X:55 Y:66 P:65 SP:FB PPU:320, 11 CYC:1364 $CBEA:88 DEY A:44 X:56 Y:66 P:65 SP:FB PPU:326, 11 CYC:1366 $CBEB:E0 56 CPX #$56 A:44 X:56 Y:65 P:65 SP:FB PPU:332, 11 CYC:1368 $CBED:D0 21 BNE $CC10 A:44 X:56 Y:65 P:67 SP:FB PPU:338, 11 CYC:1370 $CBEF:C0 65 CPY #$65 A:44 X:56 Y:65 P:67 SP:FB PPU: 3, 12 CYC:1372 $CBF1:D0 1D BNE $CC10 A:44 X:56 Y:65 P:67 SP:FB PPU: 9, 12 CYC:1374 $CBF3:E8 INX A:44 X:56 Y:65 P:67 SP:FB PPU: 15, 12 CYC:1376 $CBF4:E8 INX A:44 X:57 Y:65 P:65 SP:FB PPU: 21, 12 CYC:1378 $CBF5:88 DEY A:44 X:58 Y:65 P:65 SP:FB PPU: 27, 12 CYC:1380 $CBF6:88 DEY A:44 X:58 Y:64 P:65 SP:FB PPU: 33, 12 CYC:1382 $CBF7:E0 58 CPX #$58 A:44 X:58 Y:63 P:65 SP:FB PPU: 39, 12 CYC:1384 $CBF9:D0 15 BNE $CC10 A:44 X:58 Y:63 P:67 SP:FB PPU: 45, 12 CYC:1386 $CBFB:C0 63 CPY #$63 A:44 X:58 Y:63 P:67 SP:FB PPU: 51, 12 CYC:1388 $CBFD:D0 11 BNE $CC10 A:44 X:58 Y:63 P:67 SP:FB PPU: 57, 12 CYC:1390 $CBFF:CA DEX A:44 X:58 Y:63 P:67 SP:FB PPU: 63, 12 CYC:1392 $CC00:C8 INY A:44 X:57 Y:63 P:65 SP:FB PPU: 69, 12 CYC:1394 $CC01:E0 57 CPX #$57 A:44 X:57 Y:64 P:65 SP:FB PPU: 75, 12 CYC:1396 $CC03:D0 0B BNE $CC10 A:44 X:57 Y:64 P:67 SP:FB PPU: 81, 12 CYC:1398 $CC05:C0 64 CPY #$64 A:44 X:57 Y:64 P:67 SP:FB PPU: 87, 12 CYC:1400 $CC07:D0 07 BNE $CC10 A:44 X:57 Y:64 P:67 SP:FB PPU: 93, 12 CYC:1402 $CC09:C9 44 CMP #$44 A:44 X:57 Y:64 P:67 SP:FB PPU: 99, 12 CYC:1404 $CC0B:D0 03 BNE $CC10 A:44 X:57 Y:64 P:67 SP:FB PPU:105, 12 CYC:1406 $CC0D:4C 14 CC JMP $CC14 A:44 X:57 Y:64 P:67 SP:FB PPU:111, 12 CYC:1408 $CC14:EA NOP A:44 X:57 Y:64 P:67 SP:FB PPU:120, 12 CYC:1411 $CC15:38 SEC A:44 X:57 Y:64 P:67 SP:FB PPU:126, 12 CYC:1413 $CC16:A2 69 LDX #$69 A:44 X:57 Y:64 P:67 SP:FB PPU:132, 12 CYC:1415 $CC18:A9 96 LDA #$96 A:44 X:69 Y:64 P:65 SP:FB PPU:138, 12 CYC:1417 $CC1A:24 01 BIT $01 = #$FF A:96 X:69 Y:64 P:E5 SP:FB PPU:144, 12 CYC:1419 $CC1C:A0 FF LDY #$FF A:96 X:69 Y:64 P:E5 SP:FB PPU:153, 12 CYC:1422 $CC1E:C8 INY A:96 X:69 Y:FF P:E5 SP:FB PPU:159, 12 CYC:1424 $CC1F:D0 3D BNE $CC5E A:96 X:69 Y:00 P:67 SP:FB PPU:165, 12 CYC:1426 $CC21:30 3B BMI $CC5E A:96 X:69 Y:00 P:67 SP:FB PPU:171, 12 CYC:1428 $CC23:90 39 BCC $CC5E A:96 X:69 Y:00 P:67 SP:FB PPU:177, 12 CYC:1430 $CC25:50 37 BVC $CC5E A:96 X:69 Y:00 P:67 SP:FB PPU:183, 12 CYC:1432 $CC27:C0 00 CPY #$00 A:96 X:69 Y:00 P:67 SP:FB PPU:189, 12 CYC:1434 $CC29:D0 33 BNE $CC5E A:96 X:69 Y:00 P:67 SP:FB PPU:195, 12 CYC:1436 $CC2B:C8 INY A:96 X:69 Y:00 P:67 SP:FB PPU:201, 12 CYC:1438 $CC2C:F0 30 BEQ $CC5E A:96 X:69 Y:01 P:65 SP:FB PPU:207, 12 CYC:1440 $CC2E:30 2E BMI $CC5E A:96 X:69 Y:01 P:65 SP:FB PPU:213, 12 CYC:1442 $CC30:90 2C BCC $CC5E A:96 X:69 Y:01 P:65 SP:FB PPU:219, 12 CYC:1444 $CC32:50 2A BVC $CC5E A:96 X:69 Y:01 P:65 SP:FB PPU:225, 12 CYC:1446 $CC34:18 CLC A:96 X:69 Y:01 P:65 SP:FB PPU:231, 12 CYC:1448 $CC35:B8 CLV A:96 X:69 Y:01 P:64 SP:FB PPU:237, 12 CYC:1450 $CC36:A0 00 LDY #$00 A:96 X:69 Y:01 P:nvUbdIzc SP:FB PPU:243, 12 CYC:1452 $CC38:88 DEY A:96 X:69 Y:00 P:nvUbdIZc SP:FB PPU:249, 12 CYC:1454 $CC39:F0 23 BEQ $CC5E A:96 X:69 Y:FF P:NvUbdIzc SP:FB PPU:255, 12 CYC:1456 $CC3B:10 21 BPL $CC5E A:96 X:69 Y:FF P:NvUbdIzc SP:FB PPU:261, 12 CYC:1458 $CC3D:B0 1F BCS $CC5E A:96 X:69 Y:FF P:NvUbdIzc SP:FB PPU:267, 12 CYC:1460 $CC3F:70 1D BVS $CC5E A:96 X:69 Y:FF P:NvUbdIzc SP:FB PPU:273, 12 CYC:1462 $CC41:C0 FF CPY #$FF A:96 X:69 Y:FF P:NvUbdIzc SP:FB PPU:279, 12 CYC:1464 $CC43:D0 19 BNE $CC5E A:96 X:69 Y:FF P:nvUbdIZC SP:FB PPU:285, 12 CYC:1466 $CC45:18 CLC A:96 X:69 Y:FF P:nvUbdIZC SP:FB PPU:291, 12 CYC:1468 $CC46:88 DEY A:96 X:69 Y:FF P:nvUbdIZc SP:FB PPU:297, 12 CYC:1470 $CC47:F0 15 BEQ $CC5E A:96 X:69 Y:FE P:NvUbdIzc SP:FB PPU:303, 12 CYC:1472 $CC49:10 13 BPL $CC5E A:96 X:69 Y:FE P:NvUbdIzc SP:FB PPU:309, 12 CYC:1474 $CC4B:B0 11 BCS $CC5E A:96 X:69 Y:FE P:NvUbdIzc SP:FB PPU:315, 12 CYC:1476 $CC4D:70 0F BVS $CC5E A:96 X:69 Y:FE P:NvUbdIzc SP:FB PPU:321, 12 CYC:1478 $CC4F:C0 FE CPY #$FE A:96 X:69 Y:FE P:NvUbdIzc SP:FB PPU:327, 12 CYC:1480 $CC51:D0 0B BNE $CC5E A:96 X:69 Y:FE P:nvUbdIZC SP:FB PPU:333, 12 CYC:1482 $CC53:C9 96 CMP #$96 A:96 X:69 Y:FE P:nvUbdIZC SP:FB PPU:339, 12 CYC:1484 $CC55:D0 07 BNE $CC5E A:96 X:69 Y:FE P:nvUbdIZC SP:FB PPU: 4, 13 CYC:1486 $CC57:E0 69 CPX #$69 A:96 X:69 Y:FE P:nvUbdIZC SP:FB PPU: 10, 13 CYC:1488 $CC59:D0 03 BNE $CC5E A:96 X:69 Y:FE P:nvUbdIZC SP:FB PPU: 16, 13 CYC:1490 $CC5B:4C 62 CC JMP $CC62 A:96 X:69 Y:FE P:nvUbdIZC SP:FB PPU: 22, 13 CYC:1492 $CC62:EA NOP A:96 X:69 Y:FE P:nvUbdIZC SP:FB PPU: 31, 13 CYC:1495 $CC63:38 SEC A:96 X:69 Y:FE P:nvUbdIZC SP:FB PPU: 37, 13 CYC:1497 $CC64:A0 69 LDY #$69 A:96 X:69 Y:FE P:nvUbdIZC SP:FB PPU: 43, 13 CYC:1499 $CC66:A9 96 LDA #$96 A:96 X:69 Y:69 P:25 SP:FB PPU: 49, 13 CYC:1501 $CC68:24 01 BIT $01 = #$FF A:96 X:69 Y:69 P:A5 SP:FB PPU: 55, 13 CYC:1503 $CC6A:A2 FF LDX #$FF A:96 X:69 Y:69 P:E5 SP:FB PPU: 64, 13 CYC:1506 $CC6C:E8 INX A:96 X:FF Y:69 P:E5 SP:FB PPU: 70, 13 CYC:1508 $CC6D:D0 3D BNE $CCAC A:96 X:00 Y:69 P:67 SP:FB PPU: 76, 13 CYC:1510 $CC6F:30 3B BMI $CCAC A:96 X:00 Y:69 P:67 SP:FB PPU: 82, 13 CYC:1512 $CC71:90 39 BCC $CCAC A:96 X:00 Y:69 P:67 SP:FB PPU: 88, 13 CYC:1514 $CC73:50 37 BVC $CCAC A:96 X:00 Y:69 P:67 SP:FB PPU: 94, 13 CYC:1516 $CC75:E0 00 CPX #$00 A:96 X:00 Y:69 P:67 SP:FB PPU:100, 13 CYC:1518 $CC77:D0 33 BNE $CCAC A:96 X:00 Y:69 P:67 SP:FB PPU:106, 13 CYC:1520 $CC79:E8 INX A:96 X:00 Y:69 P:67 SP:FB PPU:112, 13 CYC:1522 $CC7A:F0 30 BEQ $CCAC A:96 X:01 Y:69 P:65 SP:FB PPU:118, 13 CYC:1524 $CC7C:30 2E BMI $CCAC A:96 X:01 Y:69 P:65 SP:FB PPU:124, 13 CYC:1526 $CC7E:90 2C BCC $CCAC A:96 X:01 Y:69 P:65 SP:FB PPU:130, 13 CYC:1528 $CC80:50 2A BVC $CCAC A:96 X:01 Y:69 P:65 SP:FB PPU:136, 13 CYC:1530 $CC82:18 CLC A:96 X:01 Y:69 P:65 SP:FB PPU:142, 13 CYC:1532 $CC83:B8 CLV A:96 X:01 Y:69 P:64 SP:FB PPU:148, 13 CYC:1534 $CC84:A2 00 LDX #$00 A:96 X:01 Y:69 P:nvUbdIzc SP:FB PPU:154, 13 CYC:1536 $CC86:CA DEX A:96 X:00 Y:69 P:nvUbdIZc SP:FB PPU:160, 13 CYC:1538 $CC87:F0 23 BEQ $CCAC A:96 X:FF Y:69 P:NvUbdIzc SP:FB PPU:166, 13 CYC:1540 $CC89:10 21 BPL $CCAC A:96 X:FF Y:69 P:NvUbdIzc SP:FB PPU:172, 13 CYC:1542 $CC8B:B0 1F BCS $CCAC A:96 X:FF Y:69 P:NvUbdIzc SP:FB PPU:178, 13 CYC:1544 $CC8D:70 1D BVS $CCAC A:96 X:FF Y:69 P:NvUbdIzc SP:FB PPU:184, 13 CYC:1546 $CC8F:E0 FF CPX #$FF A:96 X:FF Y:69 P:NvUbdIzc SP:FB PPU:190, 13 CYC:1548 $CC91:D0 19 BNE $CCAC A:96 X:FF Y:69 P:nvUbdIZC SP:FB PPU:196, 13 CYC:1550 $CC93:18 CLC A:96 X:FF Y:69 P:nvUbdIZC SP:FB PPU:202, 13 CYC:1552 $CC94:CA DEX A:96 X:FF Y:69 P:nvUbdIZc SP:FB PPU:208, 13 CYC:1554 $CC95:F0 15 BEQ $CCAC A:96 X:FE Y:69 P:NvUbdIzc SP:FB PPU:214, 13 CYC:1556 $CC97:10 13 BPL $CCAC A:96 X:FE Y:69 P:NvUbdIzc SP:FB PPU:220, 13 CYC:1558 $CC99:B0 11 BCS $CCAC A:96 X:FE Y:69 P:NvUbdIzc SP:FB PPU:226, 13 CYC:1560 $CC9B:70 0F BVS $CCAC A:96 X:FE Y:69 P:NvUbdIzc SP:FB PPU:232, 13 CYC:1562 $CC9D:E0 FE CPX #$FE A:96 X:FE Y:69 P:NvUbdIzc SP:FB PPU:238, 13 CYC:1564 $CC9F:D0 0B BNE $CCAC A:96 X:FE Y:69 P:nvUbdIZC SP:FB PPU:244, 13 CYC:1566 $CCA1:C9 96 CMP #$96 A:96 X:FE Y:69 P:nvUbdIZC SP:FB PPU:250, 13 CYC:1568 $CCA3:D0 07 BNE $CCAC A:96 X:FE Y:69 P:nvUbdIZC SP:FB PPU:256, 13 CYC:1570 $CCA5:C0 69 CPY #$69 A:96 X:FE Y:69 P:nvUbdIZC SP:FB PPU:262, 13 CYC:1572 $CCA7:D0 03 BNE $CCAC A:96 X:FE Y:69 P:nvUbdIZC SP:FB PPU:268, 13 CYC:1574 $CCA9:4C B0 CC JMP $CCB0 A:96 X:FE Y:69 P:nvUbdIZC SP:FB PPU:274, 13 CYC:1576 $CCB0:EA NOP A:96 X:FE Y:69 P:nvUbdIZC SP:FB PPU:283, 13 CYC:1579 $CCB1:A9 85 LDA #$85 A:96 X:FE Y:69 P:nvUbdIZC SP:FB PPU:289, 13 CYC:1581 $CCB3:A2 34 LDX #$34 A:85 X:FE Y:69 P:A5 SP:FB PPU:295, 13 CYC:1583 $CCB5:A0 99 LDY #$99 A:85 X:34 Y:69 P:25 SP:FB PPU:301, 13 CYC:1585 $CCB7:18 CLC A:85 X:34 Y:99 P:A5 SP:FB PPU:307, 13 CYC:1587 $CCB8:24 01 BIT $01 = #$FF A:85 X:34 Y:99 P:NvUbdIzc SP:FB PPU:313, 13 CYC:1589 $CCBA:A8 TAY A:85 X:34 Y:99 P:NVUbdIzc SP:FB PPU:322, 13 CYC:1592 $CCBB:F0 2E BEQ $CCEB A:85 X:34 Y:85 P:NVUbdIzc SP:FB PPU:328, 13 CYC:1594 $CCBD:B0 2C BCS $CCEB A:85 X:34 Y:85 P:NVUbdIzc SP:FB PPU:334, 13 CYC:1596 $CCBF:50 2A BVC $CCEB A:85 X:34 Y:85 P:NVUbdIzc SP:FB PPU:340, 13 CYC:1598 $CCC1:10 28 BPL $CCEB A:85 X:34 Y:85 P:NVUbdIzc SP:FB PPU: 5, 14 CYC:1600 $CCC3:C9 85 CMP #$85 A:85 X:34 Y:85 P:NVUbdIzc SP:FB PPU: 11, 14 CYC:1602 $CCC5:D0 24 BNE $CCEB A:85 X:34 Y:85 P:67 SP:FB PPU: 17, 14 CYC:1604 $CCC7:E0 34 CPX #$34 A:85 X:34 Y:85 P:67 SP:FB PPU: 23, 14 CYC:1606 $CCC9:D0 20 BNE $CCEB A:85 X:34 Y:85 P:67 SP:FB PPU: 29, 14 CYC:1608 $CCCB:C0 85 CPY #$85 A:85 X:34 Y:85 P:67 SP:FB PPU: 35, 14 CYC:1610 $CCCD:D0 1C BNE $CCEB A:85 X:34 Y:85 P:67 SP:FB PPU: 41, 14 CYC:1612 $CCCF:A9 00 LDA #$00 A:85 X:34 Y:85 P:67 SP:FB PPU: 47, 14 CYC:1614 $CCD1:38 SEC A:00 X:34 Y:85 P:67 SP:FB PPU: 53, 14 CYC:1616 $CCD2:B8 CLV A:00 X:34 Y:85 P:67 SP:FB PPU: 59, 14 CYC:1618 $CCD3:A8 TAY A:00 X:34 Y:85 P:nvUbdIZC SP:FB PPU: 65, 14 CYC:1620 $CCD4:D0 15 BNE $CCEB A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU: 71, 14 CYC:1622 $CCD6:90 13 BCC $CCEB A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU: 77, 14 CYC:1624 $CCD8:70 11 BVS $CCEB A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU: 83, 14 CYC:1626 $CCDA:30 0F BMI $CCEB A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU: 89, 14 CYC:1628 $CCDC:C9 00 CMP #$00 A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU: 95, 14 CYC:1630 $CCDE:D0 0B BNE $CCEB A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:101, 14 CYC:1632 $CCE0:E0 34 CPX #$34 A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:107, 14 CYC:1634 $CCE2:D0 07 BNE $CCEB A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:113, 14 CYC:1636 $CCE4:C0 00 CPY #$00 A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:119, 14 CYC:1638 $CCE6:D0 03 BNE $CCEB A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:125, 14 CYC:1640 $CCE8:4C EF CC JMP $CCEF A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:131, 14 CYC:1642 $CCEF:EA NOP A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:140, 14 CYC:1645 $CCF0:A9 85 LDA #$85 A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:146, 14 CYC:1647 $CCF2:A2 34 LDX #$34 A:85 X:34 Y:00 P:A5 SP:FB PPU:152, 14 CYC:1649 $CCF4:A0 99 LDY #$99 A:85 X:34 Y:00 P:25 SP:FB PPU:158, 14 CYC:1651 $CCF6:18 CLC A:85 X:34 Y:99 P:A5 SP:FB PPU:164, 14 CYC:1653 $CCF7:24 01 BIT $01 = #$FF A:85 X:34 Y:99 P:NvUbdIzc SP:FB PPU:170, 14 CYC:1655 $CCF9:AA TAX A:85 X:34 Y:99 P:NVUbdIzc SP:FB PPU:179, 14 CYC:1658 $CCFA:F0 2E BEQ $CD2A A:85 X:85 Y:99 P:NVUbdIzc SP:FB PPU:185, 14 CYC:1660 $CCFC:B0 2C BCS $CD2A A:85 X:85 Y:99 P:NVUbdIzc SP:FB PPU:191, 14 CYC:1662 $CCFE:50 2A BVC $CD2A A:85 X:85 Y:99 P:NVUbdIzc SP:FB PPU:197, 14 CYC:1664 $CD00:10 28 BPL $CD2A A:85 X:85 Y:99 P:NVUbdIzc SP:FB PPU:203, 14 CYC:1666 $CD02:C9 85 CMP #$85 A:85 X:85 Y:99 P:NVUbdIzc SP:FB PPU:209, 14 CYC:1668 $CD04:D0 24 BNE $CD2A A:85 X:85 Y:99 P:67 SP:FB PPU:215, 14 CYC:1670 $CD06:E0 85 CPX #$85 A:85 X:85 Y:99 P:67 SP:FB PPU:221, 14 CYC:1672 $CD08:D0 20 BNE $CD2A A:85 X:85 Y:99 P:67 SP:FB PPU:227, 14 CYC:1674 $CD0A:C0 99 CPY #$99 A:85 X:85 Y:99 P:67 SP:FB PPU:233, 14 CYC:1676 $CD0C:D0 1C BNE $CD2A A:85 X:85 Y:99 P:67 SP:FB PPU:239, 14 CYC:1678 $CD0E:A9 00 LDA #$00 A:85 X:85 Y:99 P:67 SP:FB PPU:245, 14 CYC:1680 $CD10:38 SEC A:00 X:85 Y:99 P:67 SP:FB PPU:251, 14 CYC:1682 $CD11:B8 CLV A:00 X:85 Y:99 P:67 SP:FB PPU:257, 14 CYC:1684 $CD12:AA TAX A:00 X:85 Y:99 P:nvUbdIZC SP:FB PPU:263, 14 CYC:1686 $CD13:D0 15 BNE $CD2A A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU:269, 14 CYC:1688 $CD15:90 13 BCC $CD2A A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU:275, 14 CYC:1690 $CD17:70 11 BVS $CD2A A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU:281, 14 CYC:1692 $CD19:30 0F BMI $CD2A A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU:287, 14 CYC:1694 $CD1B:C9 00 CMP #$00 A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU:293, 14 CYC:1696 $CD1D:D0 0B BNE $CD2A A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU:299, 14 CYC:1698 $CD1F:E0 00 CPX #$00 A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU:305, 14 CYC:1700 $CD21:D0 07 BNE $CD2A A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU:311, 14 CYC:1702 $CD23:C0 99 CPY #$99 A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU:317, 14 CYC:1704 $CD25:D0 03 BNE $CD2A A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU:323, 14 CYC:1706 $CD27:4C 2E CD JMP $CD2E A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU:329, 14 CYC:1708 $CD2E:EA NOP A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU:338, 14 CYC:1711 $CD2F:A9 85 LDA #$85 A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU: 3, 15 CYC:1713 $CD31:A2 34 LDX #$34 A:85 X:00 Y:99 P:A5 SP:FB PPU: 9, 15 CYC:1715 $CD33:A0 99 LDY #$99 A:85 X:34 Y:99 P:25 SP:FB PPU: 15, 15 CYC:1717 $CD35:18 CLC A:85 X:34 Y:99 P:A5 SP:FB PPU: 21, 15 CYC:1719 $CD36:24 01 BIT $01 = #$FF A:85 X:34 Y:99 P:NvUbdIzc SP:FB PPU: 27, 15 CYC:1721 $CD38:98 TYA A:85 X:34 Y:99 P:NVUbdIzc SP:FB PPU: 36, 15 CYC:1724 $CD39:F0 2E BEQ $CD69 A:99 X:34 Y:99 P:NVUbdIzc SP:FB PPU: 42, 15 CYC:1726 $CD3B:B0 2C BCS $CD69 A:99 X:34 Y:99 P:NVUbdIzc SP:FB PPU: 48, 15 CYC:1728 $CD3D:50 2A BVC $CD69 A:99 X:34 Y:99 P:NVUbdIzc SP:FB PPU: 54, 15 CYC:1730 $CD3F:10 28 BPL $CD69 A:99 X:34 Y:99 P:NVUbdIzc SP:FB PPU: 60, 15 CYC:1732 $CD41:C9 99 CMP #$99 A:99 X:34 Y:99 P:NVUbdIzc SP:FB PPU: 66, 15 CYC:1734 $CD43:D0 24 BNE $CD69 A:99 X:34 Y:99 P:67 SP:FB PPU: 72, 15 CYC:1736 $CD45:E0 34 CPX #$34 A:99 X:34 Y:99 P:67 SP:FB PPU: 78, 15 CYC:1738 $CD47:D0 20 BNE $CD69 A:99 X:34 Y:99 P:67 SP:FB PPU: 84, 15 CYC:1740 $CD49:C0 99 CPY #$99 A:99 X:34 Y:99 P:67 SP:FB PPU: 90, 15 CYC:1742 $CD4B:D0 1C BNE $CD69 A:99 X:34 Y:99 P:67 SP:FB PPU: 96, 15 CYC:1744 $CD4D:A0 00 LDY #$00 A:99 X:34 Y:99 P:67 SP:FB PPU:102, 15 CYC:1746 $CD4F:38 SEC A:99 X:34 Y:00 P:67 SP:FB PPU:108, 15 CYC:1748 $CD50:B8 CLV A:99 X:34 Y:00 P:67 SP:FB PPU:114, 15 CYC:1750 $CD51:98 TYA A:99 X:34 Y:00 P:nvUbdIZC SP:FB PPU:120, 15 CYC:1752 $CD52:D0 15 BNE $CD69 A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:126, 15 CYC:1754 $CD54:90 13 BCC $CD69 A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:132, 15 CYC:1756 $CD56:70 11 BVS $CD69 A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:138, 15 CYC:1758 $CD58:30 0F BMI $CD69 A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:144, 15 CYC:1760 $CD5A:C9 00 CMP #$00 A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:150, 15 CYC:1762 $CD5C:D0 0B BNE $CD69 A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:156, 15 CYC:1764 $CD5E:E0 34 CPX #$34 A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:162, 15 CYC:1766 $CD60:D0 07 BNE $CD69 A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:168, 15 CYC:1768 $CD62:C0 00 CPY #$00 A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:174, 15 CYC:1770 $CD64:D0 03 BNE $CD69 A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:180, 15 CYC:1772 $CD66:4C 6D CD JMP $CD6D A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:186, 15 CYC:1774 $CD6D:EA NOP A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:195, 15 CYC:1777 $CD6E:A9 85 LDA #$85 A:00 X:34 Y:00 P:nvUbdIZC SP:FB PPU:201, 15 CYC:1779 $CD70:A2 34 LDX #$34 A:85 X:34 Y:00 P:A5 SP:FB PPU:207, 15 CYC:1781 $CD72:A0 99 LDY #$99 A:85 X:34 Y:00 P:25 SP:FB PPU:213, 15 CYC:1783 $CD74:18 CLC A:85 X:34 Y:99 P:A5 SP:FB PPU:219, 15 CYC:1785 $CD75:24 01 BIT $01 = #$FF A:85 X:34 Y:99 P:NvUbdIzc SP:FB PPU:225, 15 CYC:1787 $CD77:8A TXA A:85 X:34 Y:99 P:NVUbdIzc SP:FB PPU:234, 15 CYC:1790 $CD78:F0 2E BEQ $CDA8 A:34 X:34 Y:99 P:64 SP:FB PPU:240, 15 CYC:1792 $CD7A:B0 2C BCS $CDA8 A:34 X:34 Y:99 P:64 SP:FB PPU:246, 15 CYC:1794 $CD7C:50 2A BVC $CDA8 A:34 X:34 Y:99 P:64 SP:FB PPU:252, 15 CYC:1796 $CD7E:30 28 BMI $CDA8 A:34 X:34 Y:99 P:64 SP:FB PPU:258, 15 CYC:1798 $CD80:C9 34 CMP #$34 A:34 X:34 Y:99 P:64 SP:FB PPU:264, 15 CYC:1800 $CD82:D0 24 BNE $CDA8 A:34 X:34 Y:99 P:67 SP:FB PPU:270, 15 CYC:1802 $CD84:E0 34 CPX #$34 A:34 X:34 Y:99 P:67 SP:FB PPU:276, 15 CYC:1804 $CD86:D0 20 BNE $CDA8 A:34 X:34 Y:99 P:67 SP:FB PPU:282, 15 CYC:1806 $CD88:C0 99 CPY #$99 A:34 X:34 Y:99 P:67 SP:FB PPU:288, 15 CYC:1808 $CD8A:D0 1C BNE $CDA8 A:34 X:34 Y:99 P:67 SP:FB PPU:294, 15 CYC:1810 $CD8C:A2 00 LDX #$00 A:34 X:34 Y:99 P:67 SP:FB PPU:300, 15 CYC:1812 $CD8E:38 SEC A:34 X:00 Y:99 P:67 SP:FB PPU:306, 15 CYC:1814 $CD8F:B8 CLV A:34 X:00 Y:99 P:67 SP:FB PPU:312, 15 CYC:1816 $CD90:8A TXA A:34 X:00 Y:99 P:nvUbdIZC SP:FB PPU:318, 15 CYC:1818 $CD91:D0 15 BNE $CDA8 A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU:324, 15 CYC:1820 $CD93:90 13 BCC $CDA8 A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU:330, 15 CYC:1822 $CD95:70 11 BVS $CDA8 A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU:336, 15 CYC:1824 $CD97:30 0F BMI $CDA8 A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU: 1, 16 CYC:1826 $CD99:C9 00 CMP #$00 A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU: 7, 16 CYC:1828 $CD9B:D0 0B BNE $CDA8 A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU: 13, 16 CYC:1830 $CD9D:E0 00 CPX #$00 A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU: 19, 16 CYC:1832 $CD9F:D0 07 BNE $CDA8 A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU: 25, 16 CYC:1834 $CDA1:C0 99 CPY #$99 A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU: 31, 16 CYC:1836 $CDA3:D0 03 BNE $CDA8 A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU: 37, 16 CYC:1838 $CDA5:4C AC CD JMP $CDAC A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU: 43, 16 CYC:1840 $CDAC:EA NOP A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU: 52, 16 CYC:1843 $CDAD:BA TSX A:00 X:00 Y:99 P:nvUbdIZC SP:FB PPU: 58, 16 CYC:1845 $CDAE:8E FF 07 STX $07FF = #$00 A:00 X:FB Y:99 P:A5 SP:FB PPU: 64, 16 CYC:1847 $CDB1:A0 33 LDY #$33 A:00 X:FB Y:99 P:A5 SP:FB PPU: 76, 16 CYC:1851 $CDB3:A2 69 LDX #$69 A:00 X:FB Y:33 P:25 SP:FB PPU: 82, 16 CYC:1853 $CDB5:A9 84 LDA #$84 A:00 X:69 Y:33 P:25 SP:FB PPU: 88, 16 CYC:1855 $CDB7:18 CLC A:84 X:69 Y:33 P:A5 SP:FB PPU: 94, 16 CYC:1857 $CDB8:24 01 BIT $01 = #$FF A:84 X:69 Y:33 P:NvUbdIzc SP:FB PPU:100, 16 CYC:1859 $CDBA:9A TXS A:84 X:69 Y:33 P:NVUbdIzc SP:FB PPU:109, 16 CYC:1862 $CDBB:F0 32 BEQ $CDEF A:84 X:69 Y:33 P:NVUbdIzc SP:69 PPU:115, 16 CYC:1864 $CDBD:10 30 BPL $CDEF A:84 X:69 Y:33 P:NVUbdIzc SP:69 PPU:121, 16 CYC:1866 $CDBF:B0 2E BCS $CDEF A:84 X:69 Y:33 P:NVUbdIzc SP:69 PPU:127, 16 CYC:1868 $CDC1:50 2C BVC $CDEF A:84 X:69 Y:33 P:NVUbdIzc SP:69 PPU:133, 16 CYC:1870 $CDC3:C9 84 CMP #$84 A:84 X:69 Y:33 P:NVUbdIzc SP:69 PPU:139, 16 CYC:1872 $CDC5:D0 28 BNE $CDEF A:84 X:69 Y:33 P:67 SP:69 PPU:145, 16 CYC:1874 $CDC7:E0 69 CPX #$69 A:84 X:69 Y:33 P:67 SP:69 PPU:151, 16 CYC:1876 $CDC9:D0 24 BNE $CDEF A:84 X:69 Y:33 P:67 SP:69 PPU:157, 16 CYC:1878 $CDCB:C0 33 CPY #$33 A:84 X:69 Y:33 P:67 SP:69 PPU:163, 16 CYC:1880 $CDCD:D0 20 BNE $CDEF A:84 X:69 Y:33 P:67 SP:69 PPU:169, 16 CYC:1882 $CDCF:A0 01 LDY #$01 A:84 X:69 Y:33 P:67 SP:69 PPU:175, 16 CYC:1884 $CDD1:A9 04 LDA #$04 A:84 X:69 Y:01 P:65 SP:69 PPU:181, 16 CYC:1886 $CDD3:38 SEC A:04 X:69 Y:01 P:65 SP:69 PPU:187, 16 CYC:1888 $CDD4:B8 CLV A:04 X:69 Y:01 P:65 SP:69 PPU:193, 16 CYC:1890 $CDD5:A2 00 LDX #$00 A:04 X:69 Y:01 P:25 SP:69 PPU:199, 16 CYC:1892 $CDD7:BA TSX A:04 X:00 Y:01 P:nvUbdIZC SP:69 PPU:205, 16 CYC:1894 $CDD8:F0 15 BEQ $CDEF A:04 X:69 Y:01 P:25 SP:69 PPU:211, 16 CYC:1896 $CDDA:30 13 BMI $CDEF A:04 X:69 Y:01 P:25 SP:69 PPU:217, 16 CYC:1898 $CDDC:90 11 BCC $CDEF A:04 X:69 Y:01 P:25 SP:69 PPU:223, 16 CYC:1900 $CDDE:70 0F BVS $CDEF A:04 X:69 Y:01 P:25 SP:69 PPU:229, 16 CYC:1902 $CDE0:E0 69 CPX #$69 A:04 X:69 Y:01 P:25 SP:69 PPU:235, 16 CYC:1904 $CDE2:D0 0B BNE $CDEF A:04 X:69 Y:01 P:nvUbdIZC SP:69 PPU:241, 16 CYC:1906 $CDE4:C9 04 CMP #$04 A:04 X:69 Y:01 P:nvUbdIZC SP:69 PPU:247, 16 CYC:1908 $CDE6:D0 07 BNE $CDEF A:04 X:69 Y:01 P:nvUbdIZC SP:69 PPU:253, 16 CYC:1910 $CDE8:C0 01 CPY #$01 A:04 X:69 Y:01 P:nvUbdIZC SP:69 PPU:259, 16 CYC:1912 $CDEA:D0 03 BNE $CDEF A:04 X:69 Y:01 P:nvUbdIZC SP:69 PPU:265, 16 CYC:1914 $CDEC:4C F3 CD JMP $CDF3 A:04 X:69 Y:01 P:nvUbdIZC SP:69 PPU:271, 16 CYC:1916 $CDF3:AE FF 07 LDX $07FF = #$FB A:04 X:69 Y:01 P:nvUbdIZC SP:69 PPU:280, 16 CYC:1919 $CDF6:9A TXS A:04 X:FB Y:01 P:A5 SP:69 PPU:292, 16 CYC:1923 $CDF7:60 RTS A:04 X:FB Y:01 P:A5 SP:FB PPU:298, 16 CYC:1925 $C609:20 F8 CD JSR $CDF8 A:04 X:FB Y:01 P:A5 SP:FD PPU:316, 16 CYC:1931 $CDF8:A9 FF LDA #$FF A:04 X:FB Y:01 P:A5 SP:FB PPU:334, 16 CYC:1937 $CDFA:85 01 STA $01 = #$FF A:FF X:FB Y:01 P:A5 SP:FB PPU:340, 16 CYC:1939 $CDFC:BA TSX A:FF X:FB Y:01 P:A5 SP:FB PPU: 8, 17 CYC:1942 $CDFD:8E FF 07 STX $07FF = #$FB A:FF X:FB Y:01 P:A5 SP:FB PPU: 14, 17 CYC:1944 $CE00:EA NOP A:FF X:FB Y:01 P:A5 SP:FB PPU: 26, 17 CYC:1948 $CE01:A2 80 LDX #$80 A:FF X:FB Y:01 P:A5 SP:FB PPU: 32, 17 CYC:1950 $CE03:9A TXS A:FF X:80 Y:01 P:A5 SP:FB PPU: 38, 17 CYC:1952 $CE04:A9 33 LDA #$33 A:FF X:80 Y:01 P:A5 SP:80 PPU: 44, 17 CYC:1954 $CE06:48 PHA A:33 X:80 Y:01 P:25 SP:80 PPU: 50, 17 CYC:1956 $CE07:A9 69 LDA #$69 A:33 X:80 Y:01 P:25 SP:7F PPU: 59, 17 CYC:1959 $CE09:48 PHA A:69 X:80 Y:01 P:25 SP:7F PPU: 65, 17 CYC:1961 $CE0A:BA TSX A:69 X:80 Y:01 P:25 SP:7E PPU: 74, 17 CYC:1964 $CE0B:E0 7E CPX #$7E A:69 X:7E Y:01 P:25 SP:7E PPU: 80, 17 CYC:1966 $CE0D:D0 20 BNE $CE2F A:69 X:7E Y:01 P:nvUbdIZC SP:7E PPU: 86, 17 CYC:1968 $CE0F:68 PLA A:69 X:7E Y:01 P:nvUbdIZC SP:7E PPU: 92, 17 CYC:1970 $CE10:C9 69 CMP #$69 A:69 X:7E Y:01 P:25 SP:7F PPU:104, 17 CYC:1974 $CE12:D0 1B BNE $CE2F A:69 X:7E Y:01 P:nvUbdIZC SP:7F PPU:110, 17 CYC:1976 $CE14:68 PLA A:69 X:7E Y:01 P:nvUbdIZC SP:7F PPU:116, 17 CYC:1978 $CE15:C9 33 CMP #$33 A:33 X:7E Y:01 P:25 SP:80 PPU:128, 17 CYC:1982 $CE17:D0 16 BNE $CE2F A:33 X:7E Y:01 P:nvUbdIZC SP:80 PPU:134, 17 CYC:1984 $CE19:BA TSX A:33 X:7E Y:01 P:nvUbdIZC SP:80 PPU:140, 17 CYC:1986 $CE1A:E0 80 CPX #$80 A:33 X:80 Y:01 P:A5 SP:80 PPU:146, 17 CYC:1988 $CE1C:D0 11 BNE $CE2F A:33 X:80 Y:01 P:nvUbdIZC SP:80 PPU:152, 17 CYC:1990 $CE1E:AD 80 01 LDA $0180 = #$33 A:33 X:80 Y:01 P:nvUbdIZC SP:80 PPU:158, 17 CYC:1992 $CE21:C9 33 CMP #$33 A:33 X:80 Y:01 P:25 SP:80 PPU:170, 17 CYC:1996 $CE23:D0 0A BNE $CE2F A:33 X:80 Y:01 P:nvUbdIZC SP:80 PPU:176, 17 CYC:1998 $CE25:AD 7F 01 LDA $017F = #$69 A:33 X:80 Y:01 P:nvUbdIZC SP:80 PPU:182, 17 CYC:2000 $CE28:C9 69 CMP #$69 A:69 X:80 Y:01 P:25 SP:80 PPU:194, 17 CYC:2004 $CE2A:D0 03 BNE $CE2F A:69 X:80 Y:01 P:nvUbdIZC SP:80 PPU:200, 17 CYC:2006 $CE2C:4C 33 CE JMP $CE33 A:69 X:80 Y:01 P:nvUbdIZC SP:80 PPU:206, 17 CYC:2008 $CE33:EA NOP A:69 X:80 Y:01 P:nvUbdIZC SP:80 PPU:215, 17 CYC:2011 $CE34:A2 80 LDX #$80 A:69 X:80 Y:01 P:nvUbdIZC SP:80 PPU:221, 17 CYC:2013 $CE36:9A TXS A:69 X:80 Y:01 P:A5 SP:80 PPU:227, 17 CYC:2015 $CE37:20 3D CE JSR $CE3D A:69 X:80 Y:01 P:A5 SP:80 PPU:233, 17 CYC:2017 $CE3D:BA TSX A:69 X:80 Y:01 P:A5 SP:7E PPU:251, 17 CYC:2023 $CE3E:E0 7E CPX #$7E A:69 X:7E Y:01 P:25 SP:7E PPU:257, 17 CYC:2025 $CE40:D0 19 BNE $CE5B A:69 X:7E Y:01 P:nvUbdIZC SP:7E PPU:263, 17 CYC:2027 $CE42:68 PLA A:69 X:7E Y:01 P:nvUbdIZC SP:7E PPU:269, 17 CYC:2029 $CE43:68 PLA A:39 X:7E Y:01 P:25 SP:7F PPU:281, 17 CYC:2033 $CE44:BA TSX A:CE X:7E Y:01 P:A5 SP:80 PPU:293, 17 CYC:2037 $CE45:E0 80 CPX #$80 A:CE X:80 Y:01 P:A5 SP:80 PPU:299, 17 CYC:2039 $CE47:D0 12 BNE $CE5B A:CE X:80 Y:01 P:nvUbdIZC SP:80 PPU:305, 17 CYC:2041 $CE49:A9 00 LDA #$00 A:CE X:80 Y:01 P:nvUbdIZC SP:80 PPU:311, 17 CYC:2043 $CE4B:20 4E CE JSR $CE4E A:00 X:80 Y:01 P:nvUbdIZC SP:80 PPU:317, 17 CYC:2045 $CE4E:68 PLA A:00 X:80 Y:01 P:nvUbdIZC SP:7E PPU:335, 17 CYC:2051 $CE4F:C9 4D CMP #$4D A:4D X:80 Y:01 P:25 SP:7F PPU: 6, 18 CYC:2055 $CE51:D0 08 BNE $CE5B A:4D X:80 Y:01 P:nvUbdIZC SP:7F PPU: 12, 18 CYC:2057 $CE53:68 PLA A:4D X:80 Y:01 P:nvUbdIZC SP:7F PPU: 18, 18 CYC:2059 $CE54:C9 CE CMP #$CE A:CE X:80 Y:01 P:A5 SP:80 PPU: 30, 18 CYC:2063 $CE56:D0 03 BNE $CE5B A:CE X:80 Y:01 P:nvUbdIZC SP:80 PPU: 36, 18 CYC:2065 $CE58:4C 5F CE JMP $CE5F A:CE X:80 Y:01 P:nvUbdIZC SP:80 PPU: 42, 18 CYC:2067 $CE5F:EA NOP A:CE X:80 Y:01 P:nvUbdIZC SP:80 PPU: 51, 18 CYC:2070 $CE60:A9 CE LDA #$CE A:CE X:80 Y:01 P:nvUbdIZC SP:80 PPU: 57, 18 CYC:2072 $CE62:48 PHA A:CE X:80 Y:01 P:A5 SP:80 PPU: 63, 18 CYC:2074 $CE63:A9 66 LDA #$66 A:CE X:80 Y:01 P:A5 SP:7F PPU: 72, 18 CYC:2077 $CE65:48 PHA A:66 X:80 Y:01 P:25 SP:7F PPU: 78, 18 CYC:2079 $CE66:60 RTS A:66 X:80 Y:01 P:25 SP:7E PPU: 87, 18 CYC:2082 $CE67:A2 77 LDX #$77 A:66 X:80 Y:01 P:25 SP:80 PPU:105, 18 CYC:2088 $CE69:A0 69 LDY #$69 A:66 X:77 Y:01 P:25 SP:80 PPU:111, 18 CYC:2090 $CE6B:18 CLC A:66 X:77 Y:69 P:25 SP:80 PPU:117, 18 CYC:2092 $CE6C:24 01 BIT $01 = #$FF A:66 X:77 Y:69 P:nvUbdIzc SP:80 PPU:123, 18 CYC:2094 $CE6E:A9 83 LDA #$83 A:66 X:77 Y:69 P:NVUbdIzc SP:80 PPU:132, 18 CYC:2097 $CE70:20 66 CE JSR $CE66 A:83 X:77 Y:69 P:NVUbdIzc SP:80 PPU:138, 18 CYC:2099 $CE66:60 RTS A:83 X:77 Y:69 P:NVUbdIzc SP:7E PPU:156, 18 CYC:2105 $CE73:F0 24 BEQ $CE99 A:83 X:77 Y:69 P:NVUbdIzc SP:80 PPU:174, 18 CYC:2111 $CE75:10 22 BPL $CE99 A:83 X:77 Y:69 P:NVUbdIzc SP:80 PPU:180, 18 CYC:2113 $CE77:B0 20 BCS $CE99 A:83 X:77 Y:69 P:NVUbdIzc SP:80 PPU:186, 18 CYC:2115 $CE79:50 1E BVC $CE99 A:83 X:77 Y:69 P:NVUbdIzc SP:80 PPU:192, 18 CYC:2117 $CE7B:C9 83 CMP #$83 A:83 X:77 Y:69 P:NVUbdIzc SP:80 PPU:198, 18 CYC:2119 $CE7D:D0 1A BNE $CE99 A:83 X:77 Y:69 P:67 SP:80 PPU:204, 18 CYC:2121 $CE7F:C0 69 CPY #$69 A:83 X:77 Y:69 P:67 SP:80 PPU:210, 18 CYC:2123 $CE81:D0 16 BNE $CE99 A:83 X:77 Y:69 P:67 SP:80 PPU:216, 18 CYC:2125 $CE83:E0 77 CPX #$77 A:83 X:77 Y:69 P:67 SP:80 PPU:222, 18 CYC:2127 $CE85:D0 12 BNE $CE99 A:83 X:77 Y:69 P:67 SP:80 PPU:228, 18 CYC:2129 $CE87:38 SEC A:83 X:77 Y:69 P:67 SP:80 PPU:234, 18 CYC:2131 $CE88:B8 CLV A:83 X:77 Y:69 P:67 SP:80 PPU:240, 18 CYC:2133 $CE89:A9 00 LDA #$00 A:83 X:77 Y:69 P:nvUbdIZC SP:80 PPU:246, 18 CYC:2135 $CE8B:20 66 CE JSR $CE66 A:00 X:77 Y:69 P:nvUbdIZC SP:80 PPU:252, 18 CYC:2137 $CE66:60 RTS A:00 X:77 Y:69 P:nvUbdIZC SP:7E PPU:270, 18 CYC:2143 $CE8E:D0 09 BNE $CE99 A:00 X:77 Y:69 P:nvUbdIZC SP:80 PPU:288, 18 CYC:2149 $CE90:30 07 BMI $CE99 A:00 X:77 Y:69 P:nvUbdIZC SP:80 PPU:294, 18 CYC:2151 $CE92:90 05 BCC $CE99 A:00 X:77 Y:69 P:nvUbdIZC SP:80 PPU:300, 18 CYC:2153 $CE94:70 03 BVS $CE99 A:00 X:77 Y:69 P:nvUbdIZC SP:80 PPU:306, 18 CYC:2155 $CE96:4C 9D CE JMP $CE9D A:00 X:77 Y:69 P:nvUbdIZC SP:80 PPU:312, 18 CYC:2157 $CE9D:EA NOP A:00 X:77 Y:69 P:nvUbdIZC SP:80 PPU:321, 18 CYC:2160 $CE9E:A9 CE LDA #$CE A:00 X:77 Y:69 P:nvUbdIZC SP:80 PPU:327, 18 CYC:2162 $CEA0:48 PHA A:CE X:77 Y:69 P:A5 SP:80 PPU:333, 18 CYC:2164 $CEA1:A9 AE LDA #$AE A:CE X:77 Y:69 P:A5 SP:7F PPU: 1, 19 CYC:2167 $CEA3:48 PHA A:AE X:77 Y:69 P:A5 SP:7F PPU: 7, 19 CYC:2169 $CEA4:A9 65 LDA #$65 A:AE X:77 Y:69 P:A5 SP:7E PPU: 16, 19 CYC:2172 $CEA6:48 PHA A:65 X:77 Y:69 P:25 SP:7E PPU: 22, 19 CYC:2174 $CEA7:A9 55 LDA #$55 A:65 X:77 Y:69 P:25 SP:7D PPU: 31, 19 CYC:2177 $CEA9:A0 88 LDY #$88 A:55 X:77 Y:69 P:25 SP:7D PPU: 37, 19 CYC:2179 $CEAB:A2 99 LDX #$99 A:55 X:77 Y:88 P:A5 SP:7D PPU: 43, 19 CYC:2181 $CEAD:40 RTI A:55 X:99 Y:88 P:A5 SP:7D PPU: 49, 19 CYC:2183 $CEAE:30 35 BMI $CEE5 A:55 X:99 Y:88 P:65 SP:80 PPU: 67, 19 CYC:2189 $CEB0:50 33 BVC $CEE5 A:55 X:99 Y:88 P:65 SP:80 PPU: 73, 19 CYC:2191 $CEB2:F0 31 BEQ $CEE5 A:55 X:99 Y:88 P:65 SP:80 PPU: 79, 19 CYC:2193 $CEB4:90 2F BCC $CEE5 A:55 X:99 Y:88 P:65 SP:80 PPU: 85, 19 CYC:2195 $CEB6:C9 55 CMP #$55 A:55 X:99 Y:88 P:65 SP:80 PPU: 91, 19 CYC:2197 $CEB8:D0 2B BNE $CEE5 A:55 X:99 Y:88 P:67 SP:80 PPU: 97, 19 CYC:2199 $CEBA:C0 88 CPY #$88 A:55 X:99 Y:88 P:67 SP:80 PPU:103, 19 CYC:2201 $CEBC:D0 27 BNE $CEE5 A:55 X:99 Y:88 P:67 SP:80 PPU:109, 19 CYC:2203 $CEBE:E0 99 CPX #$99 A:55 X:99 Y:88 P:67 SP:80 PPU:115, 19 CYC:2205 $CEC0:D0 23 BNE $CEE5 A:55 X:99 Y:88 P:67 SP:80 PPU:121, 19 CYC:2207 $CEC2:A9 CE LDA #$CE A:55 X:99 Y:88 P:67 SP:80 PPU:127, 19 CYC:2209 $CEC4:48 PHA A:CE X:99 Y:88 P:E5 SP:80 PPU:133, 19 CYC:2211 $CEC5:A9 CE LDA #$CE A:CE X:99 Y:88 P:E5 SP:7F PPU:142, 19 CYC:2214 $CEC7:48 PHA A:CE X:99 Y:88 P:E5 SP:7F PPU:148, 19 CYC:2216 $CEC8:A9 87 LDA #$87 A:CE X:99 Y:88 P:E5 SP:7E PPU:157, 19 CYC:2219 $CECA:48 PHA A:87 X:99 Y:88 P:E5 SP:7E PPU:163, 19 CYC:2221 $CECB:A9 55 LDA #$55 A:87 X:99 Y:88 P:E5 SP:7D PPU:172, 19 CYC:2224 $CECD:40 RTI A:55 X:99 Y:88 P:65 SP:7D PPU:178, 19 CYC:2226 $CECE:10 15 BPL $CEE5 A:55 X:99 Y:88 P:A7 SP:80 PPU:196, 19 CYC:2232 $CED0:70 13 BVS $CEE5 A:55 X:99 Y:88 P:A7 SP:80 PPU:202, 19 CYC:2234 $CED2:D0 11 BNE $CEE5 A:55 X:99 Y:88 P:A7 SP:80 PPU:208, 19 CYC:2236 $CED4:90 0F BCC $CEE5 A:55 X:99 Y:88 P:A7 SP:80 PPU:214, 19 CYC:2238 $CED6:C9 55 CMP #$55 A:55 X:99 Y:88 P:A7 SP:80 PPU:220, 19 CYC:2240 $CED8:D0 0B BNE $CEE5 A:55 X:99 Y:88 P:nvUbdIZC SP:80 PPU:226, 19 CYC:2242 $CEDA:C0 88 CPY #$88 A:55 X:99 Y:88 P:nvUbdIZC SP:80 PPU:232, 19 CYC:2244 $CEDC:D0 07 BNE $CEE5 A:55 X:99 Y:88 P:nvUbdIZC SP:80 PPU:238, 19 CYC:2246 $CEDE:E0 99 CPX #$99 A:55 X:99 Y:88 P:nvUbdIZC SP:80 PPU:244, 19 CYC:2248 $CEE0:D0 03 BNE $CEE5 A:55 X:99 Y:88 P:nvUbdIZC SP:80 PPU:250, 19 CYC:2250 $CEE2:4C E9 CE JMP $CEE9 A:55 X:99 Y:88 P:nvUbdIZC SP:80 PPU:256, 19 CYC:2252 $CEE9:AE FF 07 LDX $07FF = #$FB A:55 X:99 Y:88 P:nvUbdIZC SP:80 PPU:265, 19 CYC:2255 $CEEC:9A TXS A:55 X:FB Y:88 P:A5 SP:80 PPU:277, 19 CYC:2259 $CEED:60 RTS A:55 X:FB Y:88 P:A5 SP:FB PPU:283, 19 CYC:2261 $C60C:20 EE CE JSR $CEEE A:55 X:FB Y:88 P:A5 SP:FD PPU:301, 19 CYC:2267 $CEEE:A2 55 LDX #$55 A:55 X:FB Y:88 P:A5 SP:FB PPU:319, 19 CYC:2273 $CEF0:A0 69 LDY #$69 A:55 X:55 Y:88 P:25 SP:FB PPU:325, 19 CYC:2275 $CEF2:A9 FF LDA #$FF A:55 X:55 Y:69 P:25 SP:FB PPU:331, 19 CYC:2277 $CEF4:85 01 STA $01 = #$FF A:FF X:55 Y:69 P:A5 SP:FB PPU:337, 19 CYC:2279 $CEF6:EA NOP A:FF X:55 Y:69 P:A5 SP:FB PPU: 5, 20 CYC:2282 $CEF7:24 01 BIT $01 = #$FF A:FF X:55 Y:69 P:A5 SP:FB PPU: 11, 20 CYC:2284 $CEF9:38 SEC A:FF X:55 Y:69 P:E5 SP:FB PPU: 20, 20 CYC:2287 $CEFA:A9 01 LDA #$01 A:FF X:55 Y:69 P:E5 SP:FB PPU: 26, 20 CYC:2289 $CEFC:4A LSR A A:01 X:55 Y:69 P:65 SP:FB PPU: 32, 20 CYC:2291 $CEFD:90 1D BCC $CF1C A:00 X:55 Y:69 P:67 SP:FB PPU: 38, 20 CYC:2293 $CEFF:D0 1B BNE $CF1C A:00 X:55 Y:69 P:67 SP:FB PPU: 44, 20 CYC:2295 $CF01:30 19 BMI $CF1C A:00 X:55 Y:69 P:67 SP:FB PPU: 50, 20 CYC:2297 $CF03:50 17 BVC $CF1C A:00 X:55 Y:69 P:67 SP:FB PPU: 56, 20 CYC:2299 $CF05:C9 00 CMP #$00 A:00 X:55 Y:69 P:67 SP:FB PPU: 62, 20 CYC:2301 $CF07:D0 13 BNE $CF1C A:00 X:55 Y:69 P:67 SP:FB PPU: 68, 20 CYC:2303 $CF09:B8 CLV A:00 X:55 Y:69 P:67 SP:FB PPU: 74, 20 CYC:2305 $CF0A:A9 AA LDA #$AA A:00 X:55 Y:69 P:nvUbdIZC SP:FB PPU: 80, 20 CYC:2307 $CF0C:4A LSR A A:AA X:55 Y:69 P:A5 SP:FB PPU: 86, 20 CYC:2309 $CF0D:B0 0D BCS $CF1C A:55 X:55 Y:69 P:nvUbdIzc SP:FB PPU: 92, 20 CYC:2311 $CF0F:F0 0B BEQ $CF1C A:55 X:55 Y:69 P:nvUbdIzc SP:FB PPU: 98, 20 CYC:2313 $CF11:30 09 BMI $CF1C A:55 X:55 Y:69 P:nvUbdIzc SP:FB PPU:104, 20 CYC:2315 $CF13:70 07 BVS $CF1C A:55 X:55 Y:69 P:nvUbdIzc SP:FB PPU:110, 20 CYC:2317 $CF15:C9 55 CMP #$55 A:55 X:55 Y:69 P:nvUbdIzc SP:FB PPU:116, 20 CYC:2319 $CF17:D0 03 BNE $CF1C A:55 X:55 Y:69 P:nvUbdIZC SP:FB PPU:122, 20 CYC:2321 $CF19:4C 20 CF JMP $CF20 A:55 X:55 Y:69 P:nvUbdIZC SP:FB PPU:128, 20 CYC:2323 $CF20:EA NOP A:55 X:55 Y:69 P:nvUbdIZC SP:FB PPU:137, 20 CYC:2326 $CF21:24 01 BIT $01 = #$FF A:55 X:55 Y:69 P:nvUbdIZC SP:FB PPU:143, 20 CYC:2328 $CF23:38 SEC A:55 X:55 Y:69 P:E5 SP:FB PPU:152, 20 CYC:2331 $CF24:A9 80 LDA #$80 A:55 X:55 Y:69 P:E5 SP:FB PPU:158, 20 CYC:2333 $CF26:0A ASL A A:80 X:55 Y:69 P:E5 SP:FB PPU:164, 20 CYC:2335 $CF27:90 1E BCC $CF47 A:00 X:55 Y:69 P:67 SP:FB PPU:170, 20 CYC:2337 $CF29:D0 1C BNE $CF47 A:00 X:55 Y:69 P:67 SP:FB PPU:176, 20 CYC:2339 $CF2B:30 1A BMI $CF47 A:00 X:55 Y:69 P:67 SP:FB PPU:182, 20 CYC:2341 $CF2D:50 18 BVC $CF47 A:00 X:55 Y:69 P:67 SP:FB PPU:188, 20 CYC:2343 $CF2F:C9 00 CMP #$00 A:00 X:55 Y:69 P:67 SP:FB PPU:194, 20 CYC:2345 $CF31:D0 14 BNE $CF47 A:00 X:55 Y:69 P:67 SP:FB PPU:200, 20 CYC:2347 $CF33:B8 CLV A:00 X:55 Y:69 P:67 SP:FB PPU:206, 20 CYC:2349 $CF34:38 SEC A:00 X:55 Y:69 P:nvUbdIZC SP:FB PPU:212, 20 CYC:2351 $CF35:A9 55 LDA #$55 A:00 X:55 Y:69 P:nvUbdIZC SP:FB PPU:218, 20 CYC:2353 $CF37:0A ASL A A:55 X:55 Y:69 P:25 SP:FB PPU:224, 20 CYC:2355 $CF38:B0 0D BCS $CF47 A:AA X:55 Y:69 P:NvUbdIzc SP:FB PPU:230, 20 CYC:2357 $CF3A:F0 0B BEQ $CF47 A:AA X:55 Y:69 P:NvUbdIzc SP:FB PPU:236, 20 CYC:2359 $CF3C:10 09 BPL $CF47 A:AA X:55 Y:69 P:NvUbdIzc SP:FB PPU:242, 20 CYC:2361 $CF3E:70 07 BVS $CF47 A:AA X:55 Y:69 P:NvUbdIzc SP:FB PPU:248, 20 CYC:2363 $CF40:C9 AA CMP #$AA A:AA X:55 Y:69 P:NvUbdIzc SP:FB PPU:254, 20 CYC:2365 $CF42:D0 03 BNE $CF47 A:AA X:55 Y:69 P:nvUbdIZC SP:FB PPU:260, 20 CYC:2367 $CF44:4C 4B CF JMP $CF4B A:AA X:55 Y:69 P:nvUbdIZC SP:FB PPU:266, 20 CYC:2369 $CF4B:EA NOP A:AA X:55 Y:69 P:nvUbdIZC SP:FB PPU:275, 20 CYC:2372 $CF4C:24 01 BIT $01 = #$FF A:AA X:55 Y:69 P:nvUbdIZC SP:FB PPU:281, 20 CYC:2374 $CF4E:38 SEC A:AA X:55 Y:69 P:E5 SP:FB PPU:290, 20 CYC:2377 $CF4F:A9 01 LDA #$01 A:AA X:55 Y:69 P:E5 SP:FB PPU:296, 20 CYC:2379 $CF51:6A ROR A A:01 X:55 Y:69 P:65 SP:FB PPU:302, 20 CYC:2381 $CF52:90 1E BCC $CF72 A:80 X:55 Y:69 P:E5 SP:FB PPU:308, 20 CYC:2383 $CF54:F0 1C BEQ $CF72 A:80 X:55 Y:69 P:E5 SP:FB PPU:314, 20 CYC:2385 $CF56:10 1A BPL $CF72 A:80 X:55 Y:69 P:E5 SP:FB PPU:320, 20 CYC:2387 $CF58:50 18 BVC $CF72 A:80 X:55 Y:69 P:E5 SP:FB PPU:326, 20 CYC:2389 $CF5A:C9 80 CMP #$80 A:80 X:55 Y:69 P:E5 SP:FB PPU:332, 20 CYC:2391 $CF5C:D0 14 BNE $CF72 A:80 X:55 Y:69 P:67 SP:FB PPU:338, 20 CYC:2393 $CF5E:B8 CLV A:80 X:55 Y:69 P:67 SP:FB PPU: 3, 21 CYC:2395 $CF5F:18 CLC A:80 X:55 Y:69 P:nvUbdIZC SP:FB PPU: 9, 21 CYC:2397 $CF60:A9 55 LDA #$55 A:80 X:55 Y:69 P:nvUbdIZc SP:FB PPU: 15, 21 CYC:2399 $CF62:6A ROR A A:55 X:55 Y:69 P:nvUbdIzc SP:FB PPU: 21, 21 CYC:2401 $CF63:90 0D BCC $CF72 A:2A X:55 Y:69 P:25 SP:FB PPU: 27, 21 CYC:2403 $CF65:F0 0B BEQ $CF72 A:2A X:55 Y:69 P:25 SP:FB PPU: 33, 21 CYC:2405 $CF67:30 09 BMI $CF72 A:2A X:55 Y:69 P:25 SP:FB PPU: 39, 21 CYC:2407 $CF69:70 07 BVS $CF72 A:2A X:55 Y:69 P:25 SP:FB PPU: 45, 21 CYC:2409 $CF6B:C9 2A CMP #$2A A:2A X:55 Y:69 P:25 SP:FB PPU: 51, 21 CYC:2411 $CF6D:D0 03 BNE $CF72 A:2A X:55 Y:69 P:nvUbdIZC SP:FB PPU: 57, 21 CYC:2413 $CF6F:4C 76 CF JMP $CF76 A:2A X:55 Y:69 P:nvUbdIZC SP:FB PPU: 63, 21 CYC:2415 $CF76:EA NOP A:2A X:55 Y:69 P:nvUbdIZC SP:FB PPU: 72, 21 CYC:2418 $CF77:24 01 BIT $01 = #$FF A:2A X:55 Y:69 P:nvUbdIZC SP:FB PPU: 78, 21 CYC:2420 $CF79:38 SEC A:2A X:55 Y:69 P:E5 SP:FB PPU: 87, 21 CYC:2423 $CF7A:A9 80 LDA #$80 A:2A X:55 Y:69 P:E5 SP:FB PPU: 93, 21 CYC:2425 $CF7C:2A ROL A A:80 X:55 Y:69 P:E5 SP:FB PPU: 99, 21 CYC:2427 $CF7D:90 1E BCC $CF9D A:01 X:55 Y:69 P:65 SP:FB PPU:105, 21 CYC:2429 $CF7F:F0 1C BEQ $CF9D A:01 X:55 Y:69 P:65 SP:FB PPU:111, 21 CYC:2431 $CF81:30 1A BMI $CF9D A:01 X:55 Y:69 P:65 SP:FB PPU:117, 21 CYC:2433 $CF83:50 18 BVC $CF9D A:01 X:55 Y:69 P:65 SP:FB PPU:123, 21 CYC:2435 $CF85:C9 01 CMP #$01 A:01 X:55 Y:69 P:65 SP:FB PPU:129, 21 CYC:2437 $CF87:D0 14 BNE $CF9D A:01 X:55 Y:69 P:67 SP:FB PPU:135, 21 CYC:2439 $CF89:B8 CLV A:01 X:55 Y:69 P:67 SP:FB PPU:141, 21 CYC:2441 $CF8A:18 CLC A:01 X:55 Y:69 P:nvUbdIZC SP:FB PPU:147, 21 CYC:2443 $CF8B:A9 55 LDA #$55 A:01 X:55 Y:69 P:nvUbdIZc SP:FB PPU:153, 21 CYC:2445 $CF8D:2A ROL A A:55 X:55 Y:69 P:nvUbdIzc SP:FB PPU:159, 21 CYC:2447 $CF8E:B0 0D BCS $CF9D A:AA X:55 Y:69 P:NvUbdIzc SP:FB PPU:165, 21 CYC:2449 $CF90:F0 0B BEQ $CF9D A:AA X:55 Y:69 P:NvUbdIzc SP:FB PPU:171, 21 CYC:2451 $CF92:10 09 BPL $CF9D A:AA X:55 Y:69 P:NvUbdIzc SP:FB PPU:177, 21 CYC:2453 $CF94:70 07 BVS $CF9D A:AA X:55 Y:69 P:NvUbdIzc SP:FB PPU:183, 21 CYC:2455 $CF96:C9 AA CMP #$AA A:AA X:55 Y:69 P:NvUbdIzc SP:FB PPU:189, 21 CYC:2457 $CF98:D0 03 BNE $CF9D A:AA X:55 Y:69 P:nvUbdIZC SP:FB PPU:195, 21 CYC:2459 $CF9A:4C A1 CF JMP $CFA1 A:AA X:55 Y:69 P:nvUbdIZC SP:FB PPU:201, 21 CYC:2461 $CFA1:60 RTS A:AA X:55 Y:69 P:nvUbdIZC SP:FB PPU:210, 21 CYC:2464 $C60F:20 A2 CF JSR $CFA2 A:AA X:55 Y:69 P:nvUbdIZC SP:FD PPU:228, 21 CYC:2470 $CFA2:A5 00 LDA $00 = #$00 A:AA X:55 Y:69 P:nvUbdIZC SP:FB PPU:246, 21 CYC:2476 $CFA4:8D FF 07 STA $07FF = #$FB A:00 X:55 Y:69 P:nvUbdIZC SP:FB PPU:255, 21 CYC:2479 $CFA7:A9 00 LDA #$00 A:00 X:55 Y:69 P:nvUbdIZC SP:FB PPU:267, 21 CYC:2483 $CFA9:85 80 STA $80 = #$00 A:00 X:55 Y:69 P:nvUbdIZC SP:FB PPU:273, 21 CYC:2485 $CFAB:A9 02 LDA #$02 A:00 X:55 Y:69 P:nvUbdIZC SP:FB PPU:282, 21 CYC:2488 $CFAD:85 81 STA $81 = #$00 A:02 X:55 Y:69 P:25 SP:FB PPU:288, 21 CYC:2490 $CFAF:A9 FF LDA #$FF A:02 X:55 Y:69 P:25 SP:FB PPU:297, 21 CYC:2493 $CFB1:85 01 STA $01 = #$FF A:FF X:55 Y:69 P:A5 SP:FB PPU:303, 21 CYC:2495 $CFB3:A9 00 LDA #$00 A:FF X:55 Y:69 P:A5 SP:FB PPU:312, 21 CYC:2498 $CFB5:85 82 STA $82 = #$00 A:00 X:55 Y:69 P:nvUbdIZC SP:FB PPU:318, 21 CYC:2500 $CFB7:A9 03 LDA #$03 A:00 X:55 Y:69 P:nvUbdIZC SP:FB PPU:327, 21 CYC:2503 $CFB9:85 83 STA $83 = #$00 A:03 X:55 Y:69 P:25 SP:FB PPU:333, 21 CYC:2505 $CFBB:85 84 STA $84 = #$00 A:03 X:55 Y:69 P:25 SP:FB PPU: 1, 22 CYC:2508 $CFBD:A9 00 LDA #$00 A:03 X:55 Y:69 P:25 SP:FB PPU: 10, 22 CYC:2511 $CFBF:85 FF STA $FF = #$00 A:00 X:55 Y:69 P:nvUbdIZC SP:FB PPU: 16, 22 CYC:2513 $CFC1:A9 04 LDA #$04 A:00 X:55 Y:69 P:nvUbdIZC SP:FB PPU: 25, 22 CYC:2516 $CFC3:85 00 STA $00 = #$00 A:04 X:55 Y:69 P:25 SP:FB PPU: 31, 22 CYC:2518 $CFC5:A9 5A LDA #$5A A:04 X:55 Y:69 P:25 SP:FB PPU: 40, 22 CYC:2521 $CFC7:8D 00 02 STA $0200 = #$00 A:5A X:55 Y:69 P:25 SP:FB PPU: 46, 22 CYC:2523 $CFCA:A9 5B LDA #$5B A:5A X:55 Y:69 P:25 SP:FB PPU: 58, 22 CYC:2527 $CFCC:8D 00 03 STA $0300 = #$00 A:5B X:55 Y:69 P:25 SP:FB PPU: 64, 22 CYC:2529 $CFCF:A9 5C LDA #$5C A:5B X:55 Y:69 P:25 SP:FB PPU: 76, 22 CYC:2533 $CFD1:8D 03 03 STA $0303 = #$00 A:5C X:55 Y:69 P:25 SP:FB PPU: 82, 22 CYC:2535 $CFD4:A9 5D LDA #$5D A:5C X:55 Y:69 P:25 SP:FB PPU: 94, 22 CYC:2539 $CFD6:8D 00 04 STA $0400 = #$00 A:5D X:55 Y:69 P:25 SP:FB PPU:100, 22 CYC:2541 $CFD9:A2 00 LDX #$00 A:5D X:55 Y:69 P:25 SP:FB PPU:112, 22 CYC:2545 $CFDB:A1 80 LDA ($80,X) @ 80 = #$0200 = #$5A A:5D X:00 Y:69 P:nvUbdIZC SP:FB PPU:118, 22 CYC:2547 $CFDD:C9 5A CMP #$5A A:5A X:00 Y:69 P:25 SP:FB PPU:136, 22 CYC:2553 $CFDF:D0 1F BNE $D000 A:5A X:00 Y:69 P:nvUbdIZC SP:FB PPU:142, 22 CYC:2555 $CFE1:E8 INX A:5A X:00 Y:69 P:nvUbdIZC SP:FB PPU:148, 22 CYC:2557 $CFE2:E8 INX A:5A X:01 Y:69 P:25 SP:FB PPU:154, 22 CYC:2559 $CFE3:A1 80 LDA ($80,X) @ 82 = #$0300 = #$5B A:5A X:02 Y:69 P:25 SP:FB PPU:160, 22 CYC:2561 $CFE5:C9 5B CMP #$5B A:5B X:02 Y:69 P:25 SP:FB PPU:178, 22 CYC:2567 $CFE7:D0 17 BNE $D000 A:5B X:02 Y:69 P:nvUbdIZC SP:FB PPU:184, 22 CYC:2569 $CFE9:E8 INX A:5B X:02 Y:69 P:nvUbdIZC SP:FB PPU:190, 22 CYC:2571 $CFEA:A1 80 LDA ($80,X) @ 83 = #$0303 = #$5C A:5B X:03 Y:69 P:25 SP:FB PPU:196, 22 CYC:2573 $CFEC:C9 5C CMP #$5C A:5C X:03 Y:69 P:25 SP:FB PPU:214, 22 CYC:2579 $CFEE:D0 10 BNE $D000 A:5C X:03 Y:69 P:nvUbdIZC SP:FB PPU:220, 22 CYC:2581 $CFF0:A2 00 LDX #$00 A:5C X:03 Y:69 P:nvUbdIZC SP:FB PPU:226, 22 CYC:2583 $CFF2:A1 FF LDA ($FF,X) @ FF = #$0400 = #$5D A:5C X:00 Y:69 P:nvUbdIZC SP:FB PPU:232, 22 CYC:2585 $CFF4:C9 5D CMP #$5D A:5D X:00 Y:69 P:25 SP:FB PPU:250, 22 CYC:2591 $CFF6:D0 08 BNE $D000 A:5D X:00 Y:69 P:nvUbdIZC SP:FB PPU:256, 22 CYC:2593 $CFF8:A2 81 LDX #$81 A:5D X:00 Y:69 P:nvUbdIZC SP:FB PPU:262, 22 CYC:2595 $CFFA:A1 FF LDA ($FF,X) @ 80 = #$0200 = #$5A A:5D X:81 Y:69 P:A5 SP:FB PPU:268, 22 CYC:2597 $CFFC:C9 5A CMP #$5A A:5A X:81 Y:69 P:25 SP:FB PPU:286, 22 CYC:2603 $CFFE:F0 05 BEQ $D005 A:5A X:81 Y:69 P:nvUbdIZC SP:FB PPU:292, 22 CYC:2605 $D005:A9 AA LDA #$AA A:5A X:81 Y:69 P:nvUbdIZC SP:FB PPU:301, 22 CYC:2608 $D007:A2 00 LDX #$00 A:AA X:81 Y:69 P:A5 SP:FB PPU:307, 22 CYC:2610 $D009:81 80 STA ($80,X) @ 80 = #$0200 = #$5A A:AA X:00 Y:69 P:nvUbdIZC SP:FB PPU:313, 22 CYC:2612 $D00B:E8 INX A:AA X:00 Y:69 P:nvUbdIZC SP:FB PPU:331, 22 CYC:2618 $D00C:E8 INX A:AA X:01 Y:69 P:25 SP:FB PPU:337, 22 CYC:2620 $D00D:A9 AB LDA #$AB A:AA X:02 Y:69 P:25 SP:FB PPU: 2, 23 CYC:2622 $D00F:81 80 STA ($80,X) @ 82 = #$0300 = #$5B A:AB X:02 Y:69 P:A5 SP:FB PPU: 8, 23 CYC:2624 $D011:E8 INX A:AB X:02 Y:69 P:A5 SP:FB PPU: 26, 23 CYC:2630 $D012:A9 AC LDA #$AC A:AB X:03 Y:69 P:25 SP:FB PPU: 32, 23 CYC:2632 $D014:81 80 STA ($80,X) @ 83 = #$0303 = #$5C A:AC X:03 Y:69 P:A5 SP:FB PPU: 38, 23 CYC:2634 $D016:A2 00 LDX #$00 A:AC X:03 Y:69 P:A5 SP:FB PPU: 56, 23 CYC:2640 $D018:A9 AD LDA #$AD A:AC X:00 Y:69 P:nvUbdIZC SP:FB PPU: 62, 23 CYC:2642 $D01A:81 FF STA ($FF,X) @ FF = #$0400 = #$5D A:AD X:00 Y:69 P:A5 SP:FB PPU: 68, 23 CYC:2644 $D01C:AD 00 02 LDA $0200 = #$AA A:AD X:00 Y:69 P:A5 SP:FB PPU: 86, 23 CYC:2650 $D01F:C9 AA CMP #$AA A:AA X:00 Y:69 P:A5 SP:FB PPU: 98, 23 CYC:2654 $D021:D0 15 BNE $D038 A:AA X:00 Y:69 P:nvUbdIZC SP:FB PPU:104, 23 CYC:2656 $D023:AD 00 03 LDA $0300 = #$AB A:AA X:00 Y:69 P:nvUbdIZC SP:FB PPU:110, 23 CYC:2658 $D026:C9 AB CMP #$AB A:AB X:00 Y:69 P:A5 SP:FB PPU:122, 23 CYC:2662 $D028:D0 0E BNE $D038 A:AB X:00 Y:69 P:nvUbdIZC SP:FB PPU:128, 23 CYC:2664 $D02A:AD 03 03 LDA $0303 = #$AC A:AB X:00 Y:69 P:nvUbdIZC SP:FB PPU:134, 23 CYC:2666 $D02D:C9 AC CMP #$AC A:AC X:00 Y:69 P:A5 SP:FB PPU:146, 23 CYC:2670 $D02F:D0 07 BNE $D038 A:AC X:00 Y:69 P:nvUbdIZC SP:FB PPU:152, 23 CYC:2672 $D031:AD 00 04 LDA $0400 = #$AD A:AC X:00 Y:69 P:nvUbdIZC SP:FB PPU:158, 23 CYC:2674 $D034:C9 AD CMP #$AD A:AD X:00 Y:69 P:A5 SP:FB PPU:170, 23 CYC:2678 $D036:F0 05 BEQ $D03D A:AD X:00 Y:69 P:nvUbdIZC SP:FB PPU:176, 23 CYC:2680 $D03D:AD FF 07 LDA $07FF = #$00 A:AD X:00 Y:69 P:nvUbdIZC SP:FB PPU:185, 23 CYC:2683 $D040:85 00 STA $00 = #$04 A:00 X:00 Y:69 P:nvUbdIZC SP:FB PPU:197, 23 CYC:2687 $D042:A9 00 LDA #$00 A:00 X:00 Y:69 P:nvUbdIZC SP:FB PPU:206, 23 CYC:2690 $D044:8D 00 03 STA $0300 = #$AB A:00 X:00 Y:69 P:nvUbdIZC SP:FB PPU:212, 23 CYC:2692 $D047:A9 AA LDA #$AA A:00 X:00 Y:69 P:nvUbdIZC SP:FB PPU:224, 23 CYC:2696 $D049:8D 00 02 STA $0200 = #$AA A:AA X:00 Y:69 P:A5 SP:FB PPU:230, 23 CYC:2698 $D04C:A2 00 LDX #$00 A:AA X:00 Y:69 P:A5 SP:FB PPU:242, 23 CYC:2702 $D04E:A0 5A LDY #$5A A:AA X:00 Y:69 P:nvUbdIZC SP:FB PPU:248, 23 CYC:2704 $D050:20 B6 F7 JSR $F7B6 A:AA X:00 Y:5A P:25 SP:FB PPU:254, 23 CYC:2706 $F7B6:18 CLC A:AA X:00 Y:5A P:25 SP:F9 PPU:272, 23 CYC:2712 $F7B7:A9 FF LDA #$FF A:AA X:00 Y:5A P:nvUbdIzc SP:F9 PPU:278, 23 CYC:2714 $F7B9:85 01 STA $01 = #$FF A:FF X:00 Y:5A P:NvUbdIzc SP:F9 PPU:284, 23 CYC:2716 $F7BB:24 01 BIT $01 = #$FF A:FF X:00 Y:5A P:NvUbdIzc SP:F9 PPU:293, 23 CYC:2719 $F7BD:A9 55 LDA #$55 A:FF X:00 Y:5A P:NVUbdIzc SP:F9 PPU:302, 23 CYC:2722 $F7BF:60 RTS A:55 X:00 Y:5A P:64 SP:F9 PPU:308, 23 CYC:2724 $D053:01 80 ORA ($80,X) @ 80 = #$0200 = #$AA A:55 X:00 Y:5A P:64 SP:FB PPU:326, 23 CYC:2730 $D055:20 C0 F7 JSR $F7C0 A:FF X:00 Y:5A P:NVUbdIzc SP:FB PPU: 3, 24 CYC:2736 $F7C0:B0 09 BCS $F7CB A:FF X:00 Y:5A P:NVUbdIzc SP:F9 PPU: 21, 24 CYC:2742 $F7C2:10 07 BPL $F7CB A:FF X:00 Y:5A P:NVUbdIzc SP:F9 PPU: 27, 24 CYC:2744 $F7C4:C9 FF CMP #$FF A:FF X:00 Y:5A P:NVUbdIzc SP:F9 PPU: 33, 24 CYC:2746 $F7C6:D0 03 BNE $F7CB A:FF X:00 Y:5A P:67 SP:F9 PPU: 39, 24 CYC:2748 $F7C8:50 01 BVC $F7CB A:FF X:00 Y:5A P:67 SP:F9 PPU: 45, 24 CYC:2750 $F7CA:60 RTS A:FF X:00 Y:5A P:67 SP:F9 PPU: 51, 24 CYC:2752 $D058:C8 INY A:FF X:00 Y:5A P:67 SP:FB PPU: 69, 24 CYC:2758 $D059:20 CE F7 JSR $F7CE A:FF X:00 Y:5B P:65 SP:FB PPU: 75, 24 CYC:2760 $F7CE:38 SEC A:FF X:00 Y:5B P:65 SP:F9 PPU: 93, 24 CYC:2766 $F7CF:B8 CLV A:FF X:00 Y:5B P:65 SP:F9 PPU: 99, 24 CYC:2768 $F7D0:A9 00 LDA #$00 A:FF X:00 Y:5B P:25 SP:F9 PPU:105, 24 CYC:2770 $F7D2:60 RTS A:00 X:00 Y:5B P:nvUbdIZC SP:F9 PPU:111, 24 CYC:2772 $D05C:01 82 ORA ($82,X) @ 82 = #$0300 = #$00 A:00 X:00 Y:5B P:nvUbdIZC SP:FB PPU:129, 24 CYC:2778 $D05E:20 D3 F7 JSR $F7D3 A:00 X:00 Y:5B P:nvUbdIZC SP:FB PPU:147, 24 CYC:2784 $F7D3:D0 07 BNE $F7DC A:00 X:00 Y:5B P:nvUbdIZC SP:F9 PPU:165, 24 CYC:2790 $F7D5:70 05 BVS $F7DC A:00 X:00 Y:5B P:nvUbdIZC SP:F9 PPU:171, 24 CYC:2792 $F7D7:90 03 BCC $F7DC A:00 X:00 Y:5B P:nvUbdIZC SP:F9 PPU:177, 24 CYC:2794 $F7D9:30 01 BMI $F7DC A:00 X:00 Y:5B P:nvUbdIZC SP:F9 PPU:183, 24 CYC:2796 $F7DB:60 RTS A:00 X:00 Y:5B P:nvUbdIZC SP:F9 PPU:189, 24 CYC:2798 $D061:C8 INY A:00 X:00 Y:5B P:nvUbdIZC SP:FB PPU:207, 24 CYC:2804 $D062:20 DF F7 JSR $F7DF A:00 X:00 Y:5C P:25 SP:FB PPU:213, 24 CYC:2806 $F7DF:18 CLC A:00 X:00 Y:5C P:25 SP:F9 PPU:231, 24 CYC:2812 $F7E0:24 01 BIT $01 = #$FF A:00 X:00 Y:5C P:nvUbdIzc SP:F9 PPU:237, 24 CYC:2814 $F7E2:A9 55 LDA #$55 A:00 X:00 Y:5C P:E6 SP:F9 PPU:246, 24 CYC:2817 $F7E4:60 RTS A:55 X:00 Y:5C P:64 SP:F9 PPU:252, 24 CYC:2819 $D065:21 80 AND ($80,X) @ 80 = #$0200 = #$AA A:55 X:00 Y:5C P:64 SP:FB PPU:270, 24 CYC:2825 $D067:20 E5 F7 JSR $F7E5 A:00 X:00 Y:5C P:nVUbdIZc SP:FB PPU:288, 24 CYC:2831 $F7E5:D0 07 BNE $F7EE A:00 X:00 Y:5C P:nVUbdIZc SP:F9 PPU:306, 24 CYC:2837 $F7E7:50 05 BVC $F7EE A:00 X:00 Y:5C P:nVUbdIZc SP:F9 PPU:312, 24 CYC:2839 $F7E9:B0 03 BCS $F7EE A:00 X:00 Y:5C P:nVUbdIZc SP:F9 PPU:318, 24 CYC:2841 $F7EB:30 01 BMI $F7EE A:00 X:00 Y:5C P:nVUbdIZc SP:F9 PPU:324, 24 CYC:2843 $F7ED:60 RTS A:00 X:00 Y:5C P:nVUbdIZc SP:F9 PPU:330, 24 CYC:2845 $D06A:C8 INY A:00 X:00 Y:5C P:nVUbdIZc SP:FB PPU: 7, 25 CYC:2851 $D06B:A9 EF LDA #$EF A:00 X:00 Y:5D P:64 SP:FB PPU: 13, 25 CYC:2853 $D06D:8D 00 03 STA $0300 = #$00 A:EF X:00 Y:5D P:NVUbdIzc SP:FB PPU: 19, 25 CYC:2855 $D070:20 F1 F7 JSR $F7F1 A:EF X:00 Y:5D P:NVUbdIzc SP:FB PPU: 31, 25 CYC:2859 $F7F1:38 SEC A:EF X:00 Y:5D P:NVUbdIzc SP:F9 PPU: 49, 25 CYC:2865 $F7F2:B8 CLV A:EF X:00 Y:5D P:E5 SP:F9 PPU: 55, 25 CYC:2867 $F7F3:A9 F8 LDA #$F8 A:EF X:00 Y:5D P:A5 SP:F9 PPU: 61, 25 CYC:2869 $F7F5:60 RTS A:F8 X:00 Y:5D P:A5 SP:F9 PPU: 67, 25 CYC:2871 $D073:21 82 AND ($82,X) @ 82 = #$0300 = #$EF A:F8 X:00 Y:5D P:A5 SP:FB PPU: 85, 25 CYC:2877 $D075:20 F6 F7 JSR $F7F6 A:E8 X:00 Y:5D P:A5 SP:FB PPU:103, 25 CYC:2883 $F7F6:90 09 BCC $F801 A:E8 X:00 Y:5D P:A5 SP:F9 PPU:121, 25 CYC:2889 $F7F8:10 07 BPL $F801 A:E8 X:00 Y:5D P:A5 SP:F9 PPU:127, 25 CYC:2891 $F7FA:C9 E8 CMP #$E8 A:E8 X:00 Y:5D P:A5 SP:F9 PPU:133, 25 CYC:2893 $F7FC:D0 03 BNE $F801 A:E8 X:00 Y:5D P:nvUbdIZC SP:F9 PPU:139, 25 CYC:2895 $F7FE:70 01 BVS $F801 A:E8 X:00 Y:5D P:nvUbdIZC SP:F9 PPU:145, 25 CYC:2897 $F800:60 RTS A:E8 X:00 Y:5D P:nvUbdIZC SP:F9 PPU:151, 25 CYC:2899 $D078:C8 INY A:E8 X:00 Y:5D P:nvUbdIZC SP:FB PPU:169, 25 CYC:2905 $D079:20 04 F8 JSR $F804 A:E8 X:00 Y:5E P:25 SP:FB PPU:175, 25 CYC:2907 $F804:18 CLC A:E8 X:00 Y:5E P:25 SP:F9 PPU:193, 25 CYC:2913 $F805:24 01 BIT $01 = #$FF A:E8 X:00 Y:5E P:nvUbdIzc SP:F9 PPU:199, 25 CYC:2915 $F807:A9 5F LDA #$5F A:E8 X:00 Y:5E P:NVUbdIzc SP:F9 PPU:208, 25 CYC:2918 $F809:60 RTS A:5F X:00 Y:5E P:64 SP:F9 PPU:214, 25 CYC:2920 $D07C:41 80 EOR ($80,X) @ 80 = #$0200 = #$AA A:5F X:00 Y:5E P:64 SP:FB PPU:232, 25 CYC:2926 $D07E:20 0A F8 JSR $F80A A:F5 X:00 Y:5E P:NVUbdIzc SP:FB PPU:250, 25 CYC:2932 $F80A:B0 09 BCS $F815 A:F5 X:00 Y:5E P:NVUbdIzc SP:F9 PPU:268, 25 CYC:2938 $F80C:10 07 BPL $F815 A:F5 X:00 Y:5E P:NVUbdIzc SP:F9 PPU:274, 25 CYC:2940 $F80E:C9 F5 CMP #$F5 A:F5 X:00 Y:5E P:NVUbdIzc SP:F9 PPU:280, 25 CYC:2942 $F810:D0 03 BNE $F815 A:F5 X:00 Y:5E P:67 SP:F9 PPU:286, 25 CYC:2944 $F812:50 01 BVC $F815 A:F5 X:00 Y:5E P:67 SP:F9 PPU:292, 25 CYC:2946 $F814:60 RTS A:F5 X:00 Y:5E P:67 SP:F9 PPU:298, 25 CYC:2948 $D081:C8 INY A:F5 X:00 Y:5E P:67 SP:FB PPU:316, 25 CYC:2954 $D082:A9 70 LDA #$70 A:F5 X:00 Y:5F P:65 SP:FB PPU:322, 25 CYC:2956 $D084:8D 00 03 STA $0300 = #$EF A:70 X:00 Y:5F P:65 SP:FB PPU:328, 25 CYC:2958 $D087:20 18 F8 JSR $F818 A:70 X:00 Y:5F P:65 SP:FB PPU:340, 25 CYC:2962 $F818:38 SEC A:70 X:00 Y:5F P:65 SP:F9 PPU: 17, 26 CYC:2968 $F819:B8 CLV A:70 X:00 Y:5F P:65 SP:F9 PPU: 23, 26 CYC:2970 $F81A:A9 70 LDA #$70 A:70 X:00 Y:5F P:25 SP:F9 PPU: 29, 26 CYC:2972 $F81C:60 RTS A:70 X:00 Y:5F P:25 SP:F9 PPU: 35, 26 CYC:2974 $D08A:41 82 EOR ($82,X) @ 82 = #$0300 = #$70 A:70 X:00 Y:5F P:25 SP:FB PPU: 53, 26 CYC:2980 $D08C:20 1D F8 JSR $F81D A:00 X:00 Y:5F P:nvUbdIZC SP:FB PPU: 71, 26 CYC:2986 $F81D:D0 07 BNE $F826 A:00 X:00 Y:5F P:nvUbdIZC SP:F9 PPU: 89, 26 CYC:2992 $F81F:70 05 BVS $F826 A:00 X:00 Y:5F P:nvUbdIZC SP:F9 PPU: 95, 26 CYC:2994 $F821:90 03 BCC $F826 A:00 X:00 Y:5F P:nvUbdIZC SP:F9 PPU:101, 26 CYC:2996 $F823:30 01 BMI $F826 A:00 X:00 Y:5F P:nvUbdIZC SP:F9 PPU:107, 26 CYC:2998 $F825:60 RTS A:00 X:00 Y:5F P:nvUbdIZC SP:F9 PPU:113, 26 CYC:3000 $D08F:C8 INY A:00 X:00 Y:5F P:nvUbdIZC SP:FB PPU:131, 26 CYC:3006 $D090:A9 69 LDA #$69 A:00 X:00 Y:60 P:25 SP:FB PPU:137, 26 CYC:3008 $D092:8D 00 02 STA $0200 = #$AA A:69 X:00 Y:60 P:25 SP:FB PPU:143, 26 CYC:3010 $D095:20 29 F8 JSR $F829 A:69 X:00 Y:60 P:25 SP:FB PPU:155, 26 CYC:3014 $F829:18 CLC A:69 X:00 Y:60 P:25 SP:F9 PPU:173, 26 CYC:3020 $F82A:24 01 BIT $01 = #$FF A:69 X:00 Y:60 P:nvUbdIzc SP:F9 PPU:179, 26 CYC:3022 $F82C:A9 00 LDA #$00 A:69 X:00 Y:60 P:NVUbdIzc SP:F9 PPU:188, 26 CYC:3025 $F82E:60 RTS A:00 X:00 Y:60 P:nVUbdIZc SP:F9 PPU:194, 26 CYC:3027 $D098:61 80 ADC ($80,X) @ 80 = #$0200 = #$69 A:00 X:00 Y:60 P:nVUbdIZc SP:FB PPU:212, 26 CYC:3033 $D09A:20 2F F8 JSR $F82F A:69 X:00 Y:60 P:nvUbdIzc SP:FB PPU:230, 26 CYC:3039 $F82F:30 09 BMI $F83A A:69 X:00 Y:60 P:nvUbdIzc SP:F9 PPU:248, 26 CYC:3045 $F831:B0 07 BCS $F83A A:69 X:00 Y:60 P:nvUbdIzc SP:F9 PPU:254, 26 CYC:3047 $F833:C9 69 CMP #$69 A:69 X:00 Y:60 P:nvUbdIzc SP:F9 PPU:260, 26 CYC:3049 $F835:D0 03 BNE $F83A A:69 X:00 Y:60 P:nvUbdIZC SP:F9 PPU:266, 26 CYC:3051 $F837:70 01 BVS $F83A A:69 X:00 Y:60 P:nvUbdIZC SP:F9 PPU:272, 26 CYC:3053 $F839:60 RTS A:69 X:00 Y:60 P:nvUbdIZC SP:F9 PPU:278, 26 CYC:3055 $D09D:C8 INY A:69 X:00 Y:60 P:nvUbdIZC SP:FB PPU:296, 26 CYC:3061 $D09E:20 3D F8 JSR $F83D A:69 X:00 Y:61 P:25 SP:FB PPU:302, 26 CYC:3063 $F83D:38 SEC A:69 X:00 Y:61 P:25 SP:F9 PPU:320, 26 CYC:3069 $F83E:24 01 BIT $01 = #$FF A:69 X:00 Y:61 P:25 SP:F9 PPU:326, 26 CYC:3071 $F840:A9 00 LDA #$00 A:69 X:00 Y:61 P:E5 SP:F9 PPU:335, 26 CYC:3074 $F842:60 RTS A:00 X:00 Y:61 P:67 SP:F9 PPU: 0, 27 CYC:3076 $D0A1:61 80 ADC ($80,X) @ 80 = #$0200 = #$69 A:00 X:00 Y:61 P:67 SP:FB PPU: 18, 27 CYC:3082 $D0A3:20 43 F8 JSR $F843 A:6A X:00 Y:61 P:nvUbdIzc SP:FB PPU: 36, 27 CYC:3088 $F843:30 09 BMI $F84E A:6A X:00 Y:61 P:nvUbdIzc SP:F9 PPU: 54, 27 CYC:3094 $F845:B0 07 BCS $F84E A:6A X:00 Y:61 P:nvUbdIzc SP:F9 PPU: 60, 27 CYC:3096 $F847:C9 6A CMP #$6A A:6A X:00 Y:61 P:nvUbdIzc SP:F9 PPU: 66, 27 CYC:3098 $F849:D0 03 BNE $F84E A:6A X:00 Y:61 P:nvUbdIZC SP:F9 PPU: 72, 27 CYC:3100 $F84B:70 01 BVS $F84E A:6A X:00 Y:61 P:nvUbdIZC SP:F9 PPU: 78, 27 CYC:3102 $F84D:60 RTS A:6A X:00 Y:61 P:nvUbdIZC SP:F9 PPU: 84, 27 CYC:3104 $D0A6:C8 INY A:6A X:00 Y:61 P:nvUbdIZC SP:FB PPU:102, 27 CYC:3110 $D0A7:A9 7F LDA #$7F A:6A X:00 Y:62 P:25 SP:FB PPU:108, 27 CYC:3112 $D0A9:8D 00 02 STA $0200 = #$69 A:7F X:00 Y:62 P:25 SP:FB PPU:114, 27 CYC:3114 $D0AC:20 51 F8 JSR $F851 A:7F X:00 Y:62 P:25 SP:FB PPU:126, 27 CYC:3118 $F851:38 SEC A:7F X:00 Y:62 P:25 SP:F9 PPU:144, 27 CYC:3124 $F852:B8 CLV A:7F X:00 Y:62 P:25 SP:F9 PPU:150, 27 CYC:3126 $F853:A9 7F LDA #$7F A:7F X:00 Y:62 P:25 SP:F9 PPU:156, 27 CYC:3128 $F855:60 RTS A:7F X:00 Y:62 P:25 SP:F9 PPU:162, 27 CYC:3130 $D0AF:61 80 ADC ($80,X) @ 80 = #$0200 = #$7F A:7F X:00 Y:62 P:25 SP:FB PPU:180, 27 CYC:3136 $D0B1:20 56 F8 JSR $F856 A:FF X:00 Y:62 P:NVUbdIzc SP:FB PPU:198, 27 CYC:3142 $F856:10 09 BPL $F861 A:FF X:00 Y:62 P:NVUbdIzc SP:F9 PPU:216, 27 CYC:3148 $F858:B0 07 BCS $F861 A:FF X:00 Y:62 P:NVUbdIzc SP:F9 PPU:222, 27 CYC:3150 $F85A:C9 FF CMP #$FF A:FF X:00 Y:62 P:NVUbdIzc SP:F9 PPU:228, 27 CYC:3152 $F85C:D0 03 BNE $F861 A:FF X:00 Y:62 P:67 SP:F9 PPU:234, 27 CYC:3154 $F85E:50 01 BVC $F861 A:FF X:00 Y:62 P:67 SP:F9 PPU:240, 27 CYC:3156 $F860:60 RTS A:FF X:00 Y:62 P:67 SP:F9 PPU:246, 27 CYC:3158 $D0B4:C8 INY A:FF X:00 Y:62 P:67 SP:FB PPU:264, 27 CYC:3164 $D0B5:A9 80 LDA #$80 A:FF X:00 Y:63 P:65 SP:FB PPU:270, 27 CYC:3166 $D0B7:8D 00 02 STA $0200 = #$7F A:80 X:00 Y:63 P:E5 SP:FB PPU:276, 27 CYC:3168 $D0BA:20 64 F8 JSR $F864 A:80 X:00 Y:63 P:E5 SP:FB PPU:288, 27 CYC:3172 $F864:18 CLC A:80 X:00 Y:63 P:E5 SP:F9 PPU:306, 27 CYC:3178 $F865:24 01 BIT $01 = #$FF A:80 X:00 Y:63 P:NVUbdIzc SP:F9 PPU:312, 27 CYC:3180 $F867:A9 7F LDA #$7F A:80 X:00 Y:63 P:NVUbdIzc SP:F9 PPU:321, 27 CYC:3183 $F869:60 RTS A:7F X:00 Y:63 P:64 SP:F9 PPU:327, 27 CYC:3185 $D0BD:61 80 ADC ($80,X) @ 80 = #$0200 = #$80 A:7F X:00 Y:63 P:64 SP:FB PPU: 4, 28 CYC:3191 $D0BF:20 6A F8 JSR $F86A A:FF X:00 Y:63 P:NvUbdIzc SP:FB PPU: 22, 28 CYC:3197 $F86A:10 09 BPL $F875 A:FF X:00 Y:63 P:NvUbdIzc SP:F9 PPU: 40, 28 CYC:3203 $F86C:B0 07 BCS $F875 A:FF X:00 Y:63 P:NvUbdIzc SP:F9 PPU: 46, 28 CYC:3205 $F86E:C9 FF CMP #$FF A:FF X:00 Y:63 P:NvUbdIzc SP:F9 PPU: 52, 28 CYC:3207 $F870:D0 03 BNE $F875 A:FF X:00 Y:63 P:nvUbdIZC SP:F9 PPU: 58, 28 CYC:3209 $F872:70 01 BVS $F875 A:FF X:00 Y:63 P:nvUbdIZC SP:F9 PPU: 64, 28 CYC:3211 $F874:60 RTS A:FF X:00 Y:63 P:nvUbdIZC SP:F9 PPU: 70, 28 CYC:3213 $D0C2:C8 INY A:FF X:00 Y:63 P:nvUbdIZC SP:FB PPU: 88, 28 CYC:3219 $D0C3:20 78 F8 JSR $F878 A:FF X:00 Y:64 P:25 SP:FB PPU: 94, 28 CYC:3221 $F878:38 SEC A:FF X:00 Y:64 P:25 SP:F9 PPU:112, 28 CYC:3227 $F879:B8 CLV A:FF X:00 Y:64 P:25 SP:F9 PPU:118, 28 CYC:3229 $F87A:A9 7F LDA #$7F A:FF X:00 Y:64 P:25 SP:F9 PPU:124, 28 CYC:3231 $F87C:60 RTS A:7F X:00 Y:64 P:25 SP:F9 PPU:130, 28 CYC:3233 $D0C6:61 80 ADC ($80,X) @ 80 = #$0200 = #$80 A:7F X:00 Y:64 P:25 SP:FB PPU:148, 28 CYC:3239 $D0C8:20 7D F8 JSR $F87D A:00 X:00 Y:64 P:nvUbdIZC SP:FB PPU:166, 28 CYC:3245 $F87D:D0 07 BNE $F886 A:00 X:00 Y:64 P:nvUbdIZC SP:F9 PPU:184, 28 CYC:3251 $F87F:30 05 BMI $F886 A:00 X:00 Y:64 P:nvUbdIZC SP:F9 PPU:190, 28 CYC:3253 $F881:70 03 BVS $F886 A:00 X:00 Y:64 P:nvUbdIZC SP:F9 PPU:196, 28 CYC:3255 $F883:90 01 BCC $F886 A:00 X:00 Y:64 P:nvUbdIZC SP:F9 PPU:202, 28 CYC:3257 $F885:60 RTS A:00 X:00 Y:64 P:nvUbdIZC SP:F9 PPU:208, 28 CYC:3259 $D0CB:C8 INY A:00 X:00 Y:64 P:nvUbdIZC SP:FB PPU:226, 28 CYC:3265 $D0CC:A9 40 LDA #$40 A:00 X:00 Y:65 P:25 SP:FB PPU:232, 28 CYC:3267 $D0CE:8D 00 02 STA $0200 = #$80 A:40 X:00 Y:65 P:25 SP:FB PPU:238, 28 CYC:3269 $D0D1:20 89 F8 JSR $F889 A:40 X:00 Y:65 P:25 SP:FB PPU:250, 28 CYC:3273 $F889:24 01 BIT $01 = #$FF A:40 X:00 Y:65 P:25 SP:F9 PPU:268, 28 CYC:3279 $F88B:A9 40 LDA #$40 A:40 X:00 Y:65 P:E5 SP:F9 PPU:277, 28 CYC:3282 $F88D:60 RTS A:40 X:00 Y:65 P:65 SP:F9 PPU:283, 28 CYC:3284 $D0D4:C1 80 CMP ($80,X) @ 80 = #$0200 = #$40 A:40 X:00 Y:65 P:65 SP:FB PPU:301, 28 CYC:3290 $D0D6:20 8E F8 JSR $F88E A:40 X:00 Y:65 P:67 SP:FB PPU:319, 28 CYC:3296 $F88E:30 07 BMI $F897 A:40 X:00 Y:65 P:67 SP:F9 PPU:337, 28 CYC:3302 $F890:90 05 BCC $F897 A:40 X:00 Y:65 P:67 SP:F9 PPU: 2, 29 CYC:3304 $F892:D0 03 BNE $F897 A:40 X:00 Y:65 P:67 SP:F9 PPU: 8, 29 CYC:3306 $F894:50 01 BVC $F897 A:40 X:00 Y:65 P:67 SP:F9 PPU: 14, 29 CYC:3308 $F896:60 RTS A:40 X:00 Y:65 P:67 SP:F9 PPU: 20, 29 CYC:3310 $D0D9:C8 INY A:40 X:00 Y:65 P:67 SP:FB PPU: 38, 29 CYC:3316 $D0DA:48 PHA A:40 X:00 Y:66 P:65 SP:FB PPU: 44, 29 CYC:3318 $D0DB:A9 3F LDA #$3F A:40 X:00 Y:66 P:65 SP:FA PPU: 53, 29 CYC:3321 $D0DD:8D 00 02 STA $0200 = #$40 A:3F X:00 Y:66 P:65 SP:FA PPU: 59, 29 CYC:3323 $D0E0:68 PLA A:3F X:00 Y:66 P:65 SP:FA PPU: 71, 29 CYC:3327 $D0E1:20 9A F8 JSR $F89A A:40 X:00 Y:66 P:65 SP:FB PPU: 83, 29 CYC:3331 $F89A:B8 CLV A:40 X:00 Y:66 P:65 SP:F9 PPU:101, 29 CYC:3337 $F89B:60 RTS A:40 X:00 Y:66 P:25 SP:F9 PPU:107, 29 CYC:3339 $D0E4:C1 80 CMP ($80,X) @ 80 = #$0200 = #$3F A:40 X:00 Y:66 P:25 SP:FB PPU:125, 29 CYC:3345 $D0E6:20 9C F8 JSR $F89C A:40 X:00 Y:66 P:25 SP:FB PPU:143, 29 CYC:3351 $F89C:F0 07 BEQ $F8A5 A:40 X:00 Y:66 P:25 SP:F9 PPU:161, 29 CYC:3357 $F89E:30 05 BMI $F8A5 A:40 X:00 Y:66 P:25 SP:F9 PPU:167, 29 CYC:3359 $F8A0:90 03 BCC $F8A5 A:40 X:00 Y:66 P:25 SP:F9 PPU:173, 29 CYC:3361 $F8A2:70 01 BVS $F8A5 A:40 X:00 Y:66 P:25 SP:F9 PPU:179, 29 CYC:3363 $F8A4:60 RTS A:40 X:00 Y:66 P:25 SP:F9 PPU:185, 29 CYC:3365 $D0E9:C8 INY A:40 X:00 Y:66 P:25 SP:FB PPU:203, 29 CYC:3371 $D0EA:48 PHA A:40 X:00 Y:67 P:25 SP:FB PPU:209, 29 CYC:3373 $D0EB:A9 41 LDA #$41 A:40 X:00 Y:67 P:25 SP:FA PPU:218, 29 CYC:3376 $D0ED:8D 00 02 STA $0200 = #$3F A:41 X:00 Y:67 P:25 SP:FA PPU:224, 29 CYC:3378 $D0F0:68 PLA A:41 X:00 Y:67 P:25 SP:FA PPU:236, 29 CYC:3382 $D0F1:C1 80 CMP ($80,X) @ 80 = #$0200 = #$41 A:40 X:00 Y:67 P:25 SP:FB PPU:248, 29 CYC:3386 $D0F3:20 A8 F8 JSR $F8A8 A:40 X:00 Y:67 P:NvUbdIzc SP:FB PPU:266, 29 CYC:3392 $F8A8:F0 05 BEQ $F8AF A:40 X:00 Y:67 P:NvUbdIzc SP:F9 PPU:284, 29 CYC:3398 $F8AA:10 03 BPL $F8AF A:40 X:00 Y:67 P:NvUbdIzc SP:F9 PPU:290, 29 CYC:3400 $F8AC:10 01 BPL $F8AF A:40 X:00 Y:67 P:NvUbdIzc SP:F9 PPU:296, 29 CYC:3402 $F8AE:60 RTS A:40 X:00 Y:67 P:NvUbdIzc SP:F9 PPU:302, 29 CYC:3404 $D0F6:C8 INY A:40 X:00 Y:67 P:NvUbdIzc SP:FB PPU:320, 29 CYC:3410 $D0F7:48 PHA A:40 X:00 Y:68 P:nvUbdIzc SP:FB PPU:326, 29 CYC:3412 $D0F8:A9 00 LDA #$00 A:40 X:00 Y:68 P:nvUbdIzc SP:FA PPU:335, 29 CYC:3415 $D0FA:8D 00 02 STA $0200 = #$41 A:00 X:00 Y:68 P:nvUbdIZc SP:FA PPU: 0, 30 CYC:3417 $D0FD:68 PLA A:00 X:00 Y:68 P:nvUbdIZc SP:FA PPU: 12, 30 CYC:3421 $D0FE:20 B2 F8 JSR $F8B2 A:40 X:00 Y:68 P:nvUbdIzc SP:FB PPU: 24, 30 CYC:3425 $F8B2:A9 80 LDA #$80 A:40 X:00 Y:68 P:nvUbdIzc SP:F9 PPU: 42, 30 CYC:3431 $F8B4:60 RTS A:80 X:00 Y:68 P:NvUbdIzc SP:F9 PPU: 48, 30 CYC:3433 $D101:C1 80 CMP ($80,X) @ 80 = #$0200 = #$00 A:80 X:00 Y:68 P:NvUbdIzc SP:FB PPU: 66, 30 CYC:3439 $D103:20 B5 F8 JSR $F8B5 A:80 X:00 Y:68 P:A5 SP:FB PPU: 84, 30 CYC:3445 $F8B5:F0 05 BEQ $F8BC A:80 X:00 Y:68 P:A5 SP:F9 PPU:102, 30 CYC:3451 $F8B7:10 03 BPL $F8BC A:80 X:00 Y:68 P:A5 SP:F9 PPU:108, 30 CYC:3453 $F8B9:90 01 BCC $F8BC A:80 X:00 Y:68 P:A5 SP:F9 PPU:114, 30 CYC:3455 $F8BB:60 RTS A:80 X:00 Y:68 P:A5 SP:F9 PPU:120, 30 CYC:3457 $D106:C8 INY A:80 X:00 Y:68 P:A5 SP:FB PPU:138, 30 CYC:3463 $D107:48 PHA A:80 X:00 Y:69 P:25 SP:FB PPU:144, 30 CYC:3465 $D108:A9 80 LDA #$80 A:80 X:00 Y:69 P:25 SP:FA PPU:153, 30 CYC:3468 $D10A:8D 00 02 STA $0200 = #$00 A:80 X:00 Y:69 P:A5 SP:FA PPU:159, 30 CYC:3470 $D10D:68 PLA A:80 X:00 Y:69 P:A5 SP:FA PPU:171, 30 CYC:3474 $D10E:C1 80 CMP ($80,X) @ 80 = #$0200 = #$80 A:80 X:00 Y:69 P:A5 SP:FB PPU:183, 30 CYC:3478 $D110:20 BF F8 JSR $F8BF A:80 X:00 Y:69 P:nvUbdIZC SP:FB PPU:201, 30 CYC:3484 $F8BF:D0 05 BNE $F8C6 A:80 X:00 Y:69 P:nvUbdIZC SP:F9 PPU:219, 30 CYC:3490 $F8C1:30 03 BMI $F8C6 A:80 X:00 Y:69 P:nvUbdIZC SP:F9 PPU:225, 30 CYC:3492 $F8C3:90 01 BCC $F8C6 A:80 X:00 Y:69 P:nvUbdIZC SP:F9 PPU:231, 30 CYC:3494 $F8C5:60 RTS A:80 X:00 Y:69 P:nvUbdIZC SP:F9 PPU:237, 30 CYC:3496 $D113:C8 INY A:80 X:00 Y:69 P:nvUbdIZC SP:FB PPU:255, 30 CYC:3502 $D114:48 PHA A:80 X:00 Y:6A P:25 SP:FB PPU:261, 30 CYC:3504 $D115:A9 81 LDA #$81 A:80 X:00 Y:6A P:25 SP:FA PPU:270, 30 CYC:3507 $D117:8D 00 02 STA $0200 = #$80 A:81 X:00 Y:6A P:A5 SP:FA PPU:276, 30 CYC:3509 $D11A:68 PLA A:81 X:00 Y:6A P:A5 SP:FA PPU:288, 30 CYC:3513 $D11B:C1 80 CMP ($80,X) @ 80 = #$0200 = #$81 A:80 X:00 Y:6A P:A5 SP:FB PPU:300, 30 CYC:3517 $D11D:20 C9 F8 JSR $F8C9 A:80 X:00 Y:6A P:NvUbdIzc SP:FB PPU:318, 30 CYC:3523 $F8C9:B0 05 BCS $F8D0 A:80 X:00 Y:6A P:NvUbdIzc SP:F9 PPU:336, 30 CYC:3529 $F8CB:F0 03 BEQ $F8D0 A:80 X:00 Y:6A P:NvUbdIzc SP:F9 PPU: 1, 31 CYC:3531 $F8CD:10 01 BPL $F8D0 A:80 X:00 Y:6A P:NvUbdIzc SP:F9 PPU: 7, 31 CYC:3533 $F8CF:60 RTS A:80 X:00 Y:6A P:NvUbdIzc SP:F9 PPU: 13, 31 CYC:3535 $D120:C8 INY A:80 X:00 Y:6A P:NvUbdIzc SP:FB PPU: 31, 31 CYC:3541 $D121:48 PHA A:80 X:00 Y:6B P:nvUbdIzc SP:FB PPU: 37, 31 CYC:3543 $D122:A9 7F LDA #$7F A:80 X:00 Y:6B P:nvUbdIzc SP:FA PPU: 46, 31 CYC:3546 $D124:8D 00 02 STA $0200 = #$81 A:7F X:00 Y:6B P:nvUbdIzc SP:FA PPU: 52, 31 CYC:3548 $D127:68 PLA A:7F X:00 Y:6B P:nvUbdIzc SP:FA PPU: 64, 31 CYC:3552 $D128:C1 80 CMP ($80,X) @ 80 = #$0200 = #$7F A:80 X:00 Y:6B P:NvUbdIzc SP:FB PPU: 76, 31 CYC:3556 $D12A:20 D3 F8 JSR $F8D3 A:80 X:00 Y:6B P:25 SP:FB PPU: 94, 31 CYC:3562 $F8D3:90 05 BCC $F8DA A:80 X:00 Y:6B P:25 SP:F9 PPU:112, 31 CYC:3568 $F8D5:F0 03 BEQ $F8DA A:80 X:00 Y:6B P:25 SP:F9 PPU:118, 31 CYC:3570 $F8D7:30 01 BMI $F8DA A:80 X:00 Y:6B P:25 SP:F9 PPU:124, 31 CYC:3572 $F8D9:60 RTS A:80 X:00 Y:6B P:25 SP:F9 PPU:130, 31 CYC:3574 $D12D:C8 INY A:80 X:00 Y:6B P:25 SP:FB PPU:148, 31 CYC:3580 $D12E:A9 40 LDA #$40 A:80 X:00 Y:6C P:25 SP:FB PPU:154, 31 CYC:3582 $D130:8D 00 02 STA $0200 = #$7F A:40 X:00 Y:6C P:25 SP:FB PPU:160, 31 CYC:3584 $D133:20 31 F9 JSR $F931 A:40 X:00 Y:6C P:25 SP:FB PPU:172, 31 CYC:3588 $F931:24 01 BIT $01 = #$FF A:40 X:00 Y:6C P:25 SP:F9 PPU:190, 31 CYC:3594 $F933:A9 40 LDA #$40 A:40 X:00 Y:6C P:E5 SP:F9 PPU:199, 31 CYC:3597 $F935:38 SEC A:40 X:00 Y:6C P:65 SP:F9 PPU:205, 31 CYC:3599 $F936:60 RTS A:40 X:00 Y:6C P:65 SP:F9 PPU:211, 31 CYC:3601 $D136:E1 80 SBC ($80,X) @ 80 = #$0200 = #$40 A:40 X:00 Y:6C P:65 SP:FB PPU:229, 31 CYC:3607 $D138:20 37 F9 JSR $F937 A:00 X:00 Y:6C P:nvUbdIZC SP:FB PPU:247, 31 CYC:3613 $F937:30 0B BMI $F944 A:00 X:00 Y:6C P:nvUbdIZC SP:F9 PPU:265, 31 CYC:3619 $F939:90 09 BCC $F944 A:00 X:00 Y:6C P:nvUbdIZC SP:F9 PPU:271, 31 CYC:3621 $F93B:D0 07 BNE $F944 A:00 X:00 Y:6C P:nvUbdIZC SP:F9 PPU:277, 31 CYC:3623 $F93D:70 05 BVS $F944 A:00 X:00 Y:6C P:nvUbdIZC SP:F9 PPU:283, 31 CYC:3625 $F93F:C9 00 CMP #$00 A:00 X:00 Y:6C P:nvUbdIZC SP:F9 PPU:289, 31 CYC:3627 $F941:D0 01 BNE $F944 A:00 X:00 Y:6C P:nvUbdIZC SP:F9 PPU:295, 31 CYC:3629 $F943:60 RTS A:00 X:00 Y:6C P:nvUbdIZC SP:F9 PPU:301, 31 CYC:3631 $D13B:C8 INY A:00 X:00 Y:6C P:nvUbdIZC SP:FB PPU:319, 31 CYC:3637 $D13C:A9 3F LDA #$3F A:00 X:00 Y:6D P:25 SP:FB PPU:325, 31 CYC:3639 $D13E:8D 00 02 STA $0200 = #$40 A:3F X:00 Y:6D P:25 SP:FB PPU:331, 31 CYC:3641 $D141:20 47 F9 JSR $F947 A:3F X:00 Y:6D P:25 SP:FB PPU: 2, 32 CYC:3645 $F947:B8 CLV A:3F X:00 Y:6D P:25 SP:F9 PPU: 20, 32 CYC:3651 $F948:38 SEC A:3F X:00 Y:6D P:25 SP:F9 PPU: 26, 32 CYC:3653 $F949:A9 40 LDA #$40 A:3F X:00 Y:6D P:25 SP:F9 PPU: 32, 32 CYC:3655 $F94B:60 RTS A:40 X:00 Y:6D P:25 SP:F9 PPU: 38, 32 CYC:3657 $D144:E1 80 SBC ($80,X) @ 80 = #$0200 = #$3F A:40 X:00 Y:6D P:25 SP:FB PPU: 56, 32 CYC:3663 $D146:20 4C F9 JSR $F94C A:01 X:00 Y:6D P:25 SP:FB PPU: 74, 32 CYC:3669 $F94C:F0 0B BEQ $F959 A:01 X:00 Y:6D P:25 SP:F9 PPU: 92, 32 CYC:3675 $F94E:30 09 BMI $F959 A:01 X:00 Y:6D P:25 SP:F9 PPU: 98, 32 CYC:3677 $F950:90 07 BCC $F959 A:01 X:00 Y:6D P:25 SP:F9 PPU:104, 32 CYC:3679 $F952:70 05 BVS $F959 A:01 X:00 Y:6D P:25 SP:F9 PPU:110, 32 CYC:3681 $F954:C9 01 CMP #$01 A:01 X:00 Y:6D P:25 SP:F9 PPU:116, 32 CYC:3683 $F956:D0 01 BNE $F959 A:01 X:00 Y:6D P:nvUbdIZC SP:F9 PPU:122, 32 CYC:3685 $F958:60 RTS A:01 X:00 Y:6D P:nvUbdIZC SP:F9 PPU:128, 32 CYC:3687 $D149:C8 INY A:01 X:00 Y:6D P:nvUbdIZC SP:FB PPU:146, 32 CYC:3693 $D14A:A9 41 LDA #$41 A:01 X:00 Y:6E P:25 SP:FB PPU:152, 32 CYC:3695 $D14C:8D 00 02 STA $0200 = #$3F A:41 X:00 Y:6E P:25 SP:FB PPU:158, 32 CYC:3697 $D14F:20 5C F9 JSR $F95C A:41 X:00 Y:6E P:25 SP:FB PPU:170, 32 CYC:3701 $F95C:A9 40 LDA #$40 A:41 X:00 Y:6E P:25 SP:F9 PPU:188, 32 CYC:3707 $F95E:38 SEC A:40 X:00 Y:6E P:25 SP:F9 PPU:194, 32 CYC:3709 $F95F:24 01 BIT $01 = #$FF A:40 X:00 Y:6E P:25 SP:F9 PPU:200, 32 CYC:3711 $F961:60 RTS A:40 X:00 Y:6E P:E5 SP:F9 PPU:209, 32 CYC:3714 $D152:E1 80 SBC ($80,X) @ 80 = #$0200 = #$41 A:40 X:00 Y:6E P:E5 SP:FB PPU:227, 32 CYC:3720 $D154:20 62 F9 JSR $F962 A:FF X:00 Y:6E P:NvUbdIzc SP:FB PPU:245, 32 CYC:3726 $F962:B0 0B BCS $F96F A:FF X:00 Y:6E P:NvUbdIzc SP:F9 PPU:263, 32 CYC:3732 $F964:F0 09 BEQ $F96F A:FF X:00 Y:6E P:NvUbdIzc SP:F9 PPU:269, 32 CYC:3734 $F966:10 07 BPL $F96F A:FF X:00 Y:6E P:NvUbdIzc SP:F9 PPU:275, 32 CYC:3736 $F968:70 05 BVS $F96F A:FF X:00 Y:6E P:NvUbdIzc SP:F9 PPU:281, 32 CYC:3738 $F96A:C9 FF CMP #$FF A:FF X:00 Y:6E P:NvUbdIzc SP:F9 PPU:287, 32 CYC:3740 $F96C:D0 01 BNE $F96F A:FF X:00 Y:6E P:nvUbdIZC SP:F9 PPU:293, 32 CYC:3742 $F96E:60 RTS A:FF X:00 Y:6E P:nvUbdIZC SP:F9 PPU:299, 32 CYC:3744 $D157:C8 INY A:FF X:00 Y:6E P:nvUbdIZC SP:FB PPU:317, 32 CYC:3750 $D158:A9 00 LDA #$00 A:FF X:00 Y:6F P:25 SP:FB PPU:323, 32 CYC:3752 $D15A:8D 00 02 STA $0200 = #$41 A:00 X:00 Y:6F P:nvUbdIZC SP:FB PPU:329, 32 CYC:3754 $D15D:20 72 F9 JSR $F972 A:00 X:00 Y:6F P:nvUbdIZC SP:FB PPU: 0, 33 CYC:3758 $F972:18 CLC A:00 X:00 Y:6F P:nvUbdIZC SP:F9 PPU: 18, 33 CYC:3764 $F973:A9 80 LDA #$80 A:00 X:00 Y:6F P:nvUbdIZc SP:F9 PPU: 24, 33 CYC:3766 $F975:60 RTS A:80 X:00 Y:6F P:NvUbdIzc SP:F9 PPU: 30, 33 CYC:3768 $D160:E1 80 SBC ($80,X) @ 80 = #$0200 = #$00 A:80 X:00 Y:6F P:NvUbdIzc SP:FB PPU: 48, 33 CYC:3774 $D162:20 76 F9 JSR $F976 A:7F X:00 Y:6F P:65 SP:FB PPU: 66, 33 CYC:3780 $F976:90 05 BCC $F97D A:7F X:00 Y:6F P:65 SP:F9 PPU: 84, 33 CYC:3786 $F978:C9 7F CMP #$7F A:7F X:00 Y:6F P:65 SP:F9 PPU: 90, 33 CYC:3788 $F97A:D0 01 BNE $F97D A:7F X:00 Y:6F P:67 SP:F9 PPU: 96, 33 CYC:3790 $F97C:60 RTS A:7F X:00 Y:6F P:67 SP:F9 PPU:102, 33 CYC:3792 $D165:C8 INY A:7F X:00 Y:6F P:67 SP:FB PPU:120, 33 CYC:3798 $D166:A9 7F LDA #$7F A:7F X:00 Y:70 P:65 SP:FB PPU:126, 33 CYC:3800 $D168:8D 00 02 STA $0200 = #$00 A:7F X:00 Y:70 P:65 SP:FB PPU:132, 33 CYC:3802 $D16B:20 80 F9 JSR $F980 A:7F X:00 Y:70 P:65 SP:FB PPU:144, 33 CYC:3806 $F980:38 SEC A:7F X:00 Y:70 P:65 SP:F9 PPU:162, 33 CYC:3812 $F981:A9 81 LDA #$81 A:7F X:00 Y:70 P:65 SP:F9 PPU:168, 33 CYC:3814 $F983:60 RTS A:81 X:00 Y:70 P:E5 SP:F9 PPU:174, 33 CYC:3816 $D16E:E1 80 SBC ($80,X) @ 80 = #$0200 = #$7F A:81 X:00 Y:70 P:E5 SP:FB PPU:192, 33 CYC:3822 $D170:20 84 F9 JSR $F984 A:02 X:00 Y:70 P:65 SP:FB PPU:210, 33 CYC:3828 $F984:50 07 BVC $F98D A:02 X:00 Y:70 P:65 SP:F9 PPU:228, 33 CYC:3834 $F986:90 05 BCC $F98D A:02 X:00 Y:70 P:65 SP:F9 PPU:234, 33 CYC:3836 $F988:C9 02 CMP #$02 A:02 X:00 Y:70 P:65 SP:F9 PPU:240, 33 CYC:3838 $F98A:D0 01 BNE $F98D A:02 X:00 Y:70 P:67 SP:F9 PPU:246, 33 CYC:3840 $F98C:60 RTS A:02 X:00 Y:70 P:67 SP:F9 PPU:252, 33 CYC:3842 $D173:60 RTS A:02 X:00 Y:70 P:67 SP:FB PPU:270, 33 CYC:3848 $C612:20 74 D1 JSR $D174 A:02 X:00 Y:70 P:67 SP:FD PPU:288, 33 CYC:3854 $D174:A9 55 LDA #$55 A:02 X:00 Y:70 P:67 SP:FB PPU:306, 33 CYC:3860 $D176:85 78 STA $78 = #$00 A:55 X:00 Y:70 P:65 SP:FB PPU:312, 33 CYC:3862 $D178:A9 FF LDA #$FF A:55 X:00 Y:70 P:65 SP:FB PPU:321, 33 CYC:3865 $D17A:85 01 STA $01 = #$FF A:FF X:00 Y:70 P:E5 SP:FB PPU:327, 33 CYC:3867 $D17C:24 01 BIT $01 = #$FF A:FF X:00 Y:70 P:E5 SP:FB PPU:336, 33 CYC:3870 $D17E:A0 11 LDY #$11 A:FF X:00 Y:70 P:E5 SP:FB PPU: 4, 34 CYC:3873 $D180:A2 23 LDX #$23 A:FF X:00 Y:11 P:65 SP:FB PPU: 10, 34 CYC:3875 $D182:A9 00 LDA #$00 A:FF X:23 Y:11 P:65 SP:FB PPU: 16, 34 CYC:3877 $D184:A5 78 LDA $78 = #$55 A:00 X:23 Y:11 P:67 SP:FB PPU: 22, 34 CYC:3879 $D186:F0 10 BEQ $D198 A:55 X:23 Y:11 P:65 SP:FB PPU: 31, 34 CYC:3882 $D188:30 0E BMI $D198 A:55 X:23 Y:11 P:65 SP:FB PPU: 37, 34 CYC:3884 $D18A:C9 55 CMP #$55 A:55 X:23 Y:11 P:65 SP:FB PPU: 43, 34 CYC:3886 $D18C:D0 0A BNE $D198 A:55 X:23 Y:11 P:67 SP:FB PPU: 49, 34 CYC:3888 $D18E:C0 11 CPY #$11 A:55 X:23 Y:11 P:67 SP:FB PPU: 55, 34 CYC:3890 $D190:D0 06 BNE $D198 A:55 X:23 Y:11 P:67 SP:FB PPU: 61, 34 CYC:3892 $D192:E0 23 CPX #$23 A:55 X:23 Y:11 P:67 SP:FB PPU: 67, 34 CYC:3894 $D194:50 02 BVC $D198 A:55 X:23 Y:11 P:67 SP:FB PPU: 73, 34 CYC:3896 $D196:F0 04 BEQ $D19C A:55 X:23 Y:11 P:67 SP:FB PPU: 79, 34 CYC:3898 $D19C:A9 46 LDA #$46 A:55 X:23 Y:11 P:67 SP:FB PPU: 88, 34 CYC:3901 $D19E:24 01 BIT $01 = #$FF A:46 X:23 Y:11 P:65 SP:FB PPU: 94, 34 CYC:3903 $D1A0:85 78 STA $78 = #$55 A:46 X:23 Y:11 P:E5 SP:FB PPU:103, 34 CYC:3906 $D1A2:F0 0A BEQ $D1AE A:46 X:23 Y:11 P:E5 SP:FB PPU:112, 34 CYC:3909 $D1A4:10 08 BPL $D1AE A:46 X:23 Y:11 P:E5 SP:FB PPU:118, 34 CYC:3911 $D1A6:50 06 BVC $D1AE A:46 X:23 Y:11 P:E5 SP:FB PPU:124, 34 CYC:3913 $D1A8:A5 78 LDA $78 = #$46 A:46 X:23 Y:11 P:E5 SP:FB PPU:130, 34 CYC:3915 $D1AA:C9 46 CMP #$46 A:46 X:23 Y:11 P:65 SP:FB PPU:139, 34 CYC:3918 $D1AC:F0 04 BEQ $D1B2 A:46 X:23 Y:11 P:67 SP:FB PPU:145, 34 CYC:3920 $D1B2:A9 55 LDA #$55 A:46 X:23 Y:11 P:67 SP:FB PPU:154, 34 CYC:3923 $D1B4:85 78 STA $78 = #$46 A:55 X:23 Y:11 P:65 SP:FB PPU:160, 34 CYC:3925 $D1B6:24 01 BIT $01 = #$FF A:55 X:23 Y:11 P:65 SP:FB PPU:169, 34 CYC:3928 $D1B8:A9 11 LDA #$11 A:55 X:23 Y:11 P:E5 SP:FB PPU:178, 34 CYC:3931 $D1BA:A2 23 LDX #$23 A:11 X:23 Y:11 P:65 SP:FB PPU:184, 34 CYC:3933 $D1BC:A0 00 LDY #$00 A:11 X:23 Y:11 P:65 SP:FB PPU:190, 34 CYC:3935 $D1BE:A4 78 LDY $78 = #$55 A:11 X:23 Y:00 P:67 SP:FB PPU:196, 34 CYC:3937 $D1C0:F0 10 BEQ $D1D2 A:11 X:23 Y:55 P:65 SP:FB PPU:205, 34 CYC:3940 $D1C2:30 0E BMI $D1D2 A:11 X:23 Y:55 P:65 SP:FB PPU:211, 34 CYC:3942 $D1C4:C0 55 CPY #$55 A:11 X:23 Y:55 P:65 SP:FB PPU:217, 34 CYC:3944 $D1C6:D0 0A BNE $D1D2 A:11 X:23 Y:55 P:67 SP:FB PPU:223, 34 CYC:3946 $D1C8:C9 11 CMP #$11 A:11 X:23 Y:55 P:67 SP:FB PPU:229, 34 CYC:3948 $D1CA:D0 06 BNE $D1D2 A:11 X:23 Y:55 P:67 SP:FB PPU:235, 34 CYC:3950 $D1CC:E0 23 CPX #$23 A:11 X:23 Y:55 P:67 SP:FB PPU:241, 34 CYC:3952 $D1CE:50 02 BVC $D1D2 A:11 X:23 Y:55 P:67 SP:FB PPU:247, 34 CYC:3954 $D1D0:F0 04 BEQ $D1D6 A:11 X:23 Y:55 P:67 SP:FB PPU:253, 34 CYC:3956 $D1D6:A0 46 LDY #$46 A:11 X:23 Y:55 P:67 SP:FB PPU:262, 34 CYC:3959 $D1D8:24 01 BIT $01 = #$FF A:11 X:23 Y:46 P:65 SP:FB PPU:268, 34 CYC:3961 $D1DA:84 78 STY $78 = #$55 A:11 X:23 Y:46 P:E5 SP:FB PPU:277, 34 CYC:3964 $D1DC:F0 0A BEQ $D1E8 A:11 X:23 Y:46 P:E5 SP:FB PPU:286, 34 CYC:3967 $D1DE:10 08 BPL $D1E8 A:11 X:23 Y:46 P:E5 SP:FB PPU:292, 34 CYC:3969 $D1E0:50 06 BVC $D1E8 A:11 X:23 Y:46 P:E5 SP:FB PPU:298, 34 CYC:3971 $D1E2:A4 78 LDY $78 = #$46 A:11 X:23 Y:46 P:E5 SP:FB PPU:304, 34 CYC:3973 $D1E4:C0 46 CPY #$46 A:11 X:23 Y:46 P:65 SP:FB PPU:313, 34 CYC:3976 $D1E6:F0 04 BEQ $D1EC A:11 X:23 Y:46 P:67 SP:FB PPU:319, 34 CYC:3978 $D1EC:24 01 BIT $01 = #$FF A:11 X:23 Y:46 P:67 SP:FB PPU:328, 34 CYC:3981 $D1EE:A9 55 LDA #$55 A:11 X:23 Y:46 P:E5 SP:FB PPU:337, 34 CYC:3984 $D1F0:85 78 STA $78 = #$46 A:55 X:23 Y:46 P:65 SP:FB PPU: 2, 35 CYC:3986 $D1F2:A0 11 LDY #$11 A:55 X:23 Y:46 P:65 SP:FB PPU: 11, 35 CYC:3989 $D1F4:A9 23 LDA #$23 A:55 X:23 Y:11 P:65 SP:FB PPU: 17, 35 CYC:3991 $D1F6:A2 00 LDX #$00 A:23 X:23 Y:11 P:65 SP:FB PPU: 23, 35 CYC:3993 $D1F8:A6 78 LDX $78 = #$55 A:23 X:00 Y:11 P:67 SP:FB PPU: 29, 35 CYC:3995 $D1FA:F0 10 BEQ $D20C A:23 X:55 Y:11 P:65 SP:FB PPU: 38, 35 CYC:3998 $D1FC:30 0E BMI $D20C A:23 X:55 Y:11 P:65 SP:FB PPU: 44, 35 CYC:4000 $D1FE:E0 55 CPX #$55 A:23 X:55 Y:11 P:65 SP:FB PPU: 50, 35 CYC:4002 $D200:D0 0A BNE $D20C A:23 X:55 Y:11 P:67 SP:FB PPU: 56, 35 CYC:4004 $D202:C0 11 CPY #$11 A:23 X:55 Y:11 P:67 SP:FB PPU: 62, 35 CYC:4006 $D204:D0 06 BNE $D20C A:23 X:55 Y:11 P:67 SP:FB PPU: 68, 35 CYC:4008 $D206:C9 23 CMP #$23 A:23 X:55 Y:11 P:67 SP:FB PPU: 74, 35 CYC:4010 $D208:50 02 BVC $D20C A:23 X:55 Y:11 P:67 SP:FB PPU: 80, 35 CYC:4012 $D20A:F0 04 BEQ $D210 A:23 X:55 Y:11 P:67 SP:FB PPU: 86, 35 CYC:4014 $D210:A2 46 LDX #$46 A:23 X:55 Y:11 P:67 SP:FB PPU: 95, 35 CYC:4017 $D212:24 01 BIT $01 = #$FF A:23 X:46 Y:11 P:65 SP:FB PPU:101, 35 CYC:4019 $D214:86 78 STX $78 = #$55 A:23 X:46 Y:11 P:E5 SP:FB PPU:110, 35 CYC:4022 $D216:F0 0A BEQ $D222 A:23 X:46 Y:11 P:E5 SP:FB PPU:119, 35 CYC:4025 $D218:10 08 BPL $D222 A:23 X:46 Y:11 P:E5 SP:FB PPU:125, 35 CYC:4027 $D21A:50 06 BVC $D222 A:23 X:46 Y:11 P:E5 SP:FB PPU:131, 35 CYC:4029 $D21C:A6 78 LDX $78 = #$46 A:23 X:46 Y:11 P:E5 SP:FB PPU:137, 35 CYC:4031 $D21E:E0 46 CPX #$46 A:23 X:46 Y:11 P:65 SP:FB PPU:146, 35 CYC:4034 $D220:F0 04 BEQ $D226 A:23 X:46 Y:11 P:67 SP:FB PPU:152, 35 CYC:4036 $D226:A9 C0 LDA #$C0 A:23 X:46 Y:11 P:67 SP:FB PPU:161, 35 CYC:4039 $D228:85 78 STA $78 = #$46 A:C0 X:46 Y:11 P:E5 SP:FB PPU:167, 35 CYC:4041 $D22A:A2 33 LDX #$33 A:C0 X:46 Y:11 P:E5 SP:FB PPU:176, 35 CYC:4044 $D22C:A0 88 LDY #$88 A:C0 X:33 Y:11 P:65 SP:FB PPU:182, 35 CYC:4046 $D22E:A9 05 LDA #$05 A:C0 X:33 Y:88 P:E5 SP:FB PPU:188, 35 CYC:4048 $D230:24 78 BIT $78 = #$C0 A:05 X:33 Y:88 P:65 SP:FB PPU:194, 35 CYC:4050 $D232:10 10 BPL $D244 A:05 X:33 Y:88 P:E7 SP:FB PPU:203, 35 CYC:4053 $D234:50 0E BVC $D244 A:05 X:33 Y:88 P:E7 SP:FB PPU:209, 35 CYC:4055 $D236:D0 0C BNE $D244 A:05 X:33 Y:88 P:E7 SP:FB PPU:215, 35 CYC:4057 $D238:C9 05 CMP #$05 A:05 X:33 Y:88 P:E7 SP:FB PPU:221, 35 CYC:4059 $D23A:D0 08 BNE $D244 A:05 X:33 Y:88 P:67 SP:FB PPU:227, 35 CYC:4061 $D23C:E0 33 CPX #$33 A:05 X:33 Y:88 P:67 SP:FB PPU:233, 35 CYC:4063 $D23E:D0 04 BNE $D244 A:05 X:33 Y:88 P:67 SP:FB PPU:239, 35 CYC:4065 $D240:C0 88 CPY #$88 A:05 X:33 Y:88 P:67 SP:FB PPU:245, 35 CYC:4067 $D242:F0 04 BEQ $D248 A:05 X:33 Y:88 P:67 SP:FB PPU:251, 35 CYC:4069 $D248:A9 03 LDA #$03 A:05 X:33 Y:88 P:67 SP:FB PPU:260, 35 CYC:4072 $D24A:85 78 STA $78 = #$C0 A:03 X:33 Y:88 P:65 SP:FB PPU:266, 35 CYC:4074 $D24C:A9 01 LDA #$01 A:03 X:33 Y:88 P:65 SP:FB PPU:275, 35 CYC:4077 $D24E:24 78 BIT $78 = #$03 A:01 X:33 Y:88 P:65 SP:FB PPU:281, 35 CYC:4079 $D250:30 08 BMI $D25A A:01 X:33 Y:88 P:25 SP:FB PPU:290, 35 CYC:4082 $D252:70 06 BVS $D25A A:01 X:33 Y:88 P:25 SP:FB PPU:296, 35 CYC:4084 $D254:F0 04 BEQ $D25A A:01 X:33 Y:88 P:25 SP:FB PPU:302, 35 CYC:4086 $D256:C9 01 CMP #$01 A:01 X:33 Y:88 P:25 SP:FB PPU:308, 35 CYC:4088 $D258:F0 04 BEQ $D25E A:01 X:33 Y:88 P:nvUbdIZC SP:FB PPU:314, 35 CYC:4090 $D25E:A0 7E LDY #$7E A:01 X:33 Y:88 P:nvUbdIZC SP:FB PPU:323, 35 CYC:4093 $D260:A9 AA LDA #$AA A:01 X:33 Y:7E P:25 SP:FB PPU:329, 35 CYC:4095 $D262:85 78 STA $78 = #$03 A:AA X:33 Y:7E P:A5 SP:FB PPU:335, 35 CYC:4097 $D264:20 B6 F7 JSR $F7B6 A:AA X:33 Y:7E P:A5 SP:FB PPU: 3, 36 CYC:4100 $F7B6:18 CLC A:AA X:33 Y:7E P:A5 SP:F9 PPU: 21, 36 CYC:4106 $F7B7:A9 FF LDA #$FF A:AA X:33 Y:7E P:NvUbdIzc SP:F9 PPU: 27, 36 CYC:4108 $F7B9:85 01 STA $01 = #$FF A:FF X:33 Y:7E P:NvUbdIzc SP:F9 PPU: 33, 36 CYC:4110 $F7BB:24 01 BIT $01 = #$FF A:FF X:33 Y:7E P:NvUbdIzc SP:F9 PPU: 42, 36 CYC:4113 $F7BD:A9 55 LDA #$55 A:FF X:33 Y:7E P:NVUbdIzc SP:F9 PPU: 51, 36 CYC:4116 $F7BF:60 RTS A:55 X:33 Y:7E P:64 SP:F9 PPU: 57, 36 CYC:4118 $D267:05 78 ORA $78 = #$AA A:55 X:33 Y:7E P:64 SP:FB PPU: 75, 36 CYC:4124 $D269:20 C0 F7 JSR $F7C0 A:FF X:33 Y:7E P:NVUbdIzc SP:FB PPU: 84, 36 CYC:4127 $F7C0:B0 09 BCS $F7CB A:FF X:33 Y:7E P:NVUbdIzc SP:F9 PPU:102, 36 CYC:4133 $F7C2:10 07 BPL $F7CB A:FF X:33 Y:7E P:NVUbdIzc SP:F9 PPU:108, 36 CYC:4135 $F7C4:C9 FF CMP #$FF A:FF X:33 Y:7E P:NVUbdIzc SP:F9 PPU:114, 36 CYC:4137 $F7C6:D0 03 BNE $F7CB A:FF X:33 Y:7E P:67 SP:F9 PPU:120, 36 CYC:4139 $F7C8:50 01 BVC $F7CB A:FF X:33 Y:7E P:67 SP:F9 PPU:126, 36 CYC:4141 $F7CA:60 RTS A:FF X:33 Y:7E P:67 SP:F9 PPU:132, 36 CYC:4143 $D26C:C8 INY A:FF X:33 Y:7E P:67 SP:FB PPU:150, 36 CYC:4149 $D26D:A9 00 LDA #$00 A:FF X:33 Y:7F P:65 SP:FB PPU:156, 36 CYC:4151 $D26F:85 78 STA $78 = #$AA A:00 X:33 Y:7F P:67 SP:FB PPU:162, 36 CYC:4153 $D271:20 CE F7 JSR $F7CE A:00 X:33 Y:7F P:67 SP:FB PPU:171, 36 CYC:4156 $F7CE:38 SEC A:00 X:33 Y:7F P:67 SP:F9 PPU:189, 36 CYC:4162 $F7CF:B8 CLV A:00 X:33 Y:7F P:67 SP:F9 PPU:195, 36 CYC:4164 $F7D0:A9 00 LDA #$00 A:00 X:33 Y:7F P:nvUbdIZC SP:F9 PPU:201, 36 CYC:4166 $F7D2:60 RTS A:00 X:33 Y:7F P:nvUbdIZC SP:F9 PPU:207, 36 CYC:4168 $D274:05 78 ORA $78 = #$00 A:00 X:33 Y:7F P:nvUbdIZC SP:FB PPU:225, 36 CYC:4174 $D276:20 D3 F7 JSR $F7D3 A:00 X:33 Y:7F P:nvUbdIZC SP:FB PPU:234, 36 CYC:4177 $F7D3:D0 07 BNE $F7DC A:00 X:33 Y:7F P:nvUbdIZC SP:F9 PPU:252, 36 CYC:4183 $F7D5:70 05 BVS $F7DC A:00 X:33 Y:7F P:nvUbdIZC SP:F9 PPU:258, 36 CYC:4185 $F7D7:90 03 BCC $F7DC A:00 X:33 Y:7F P:nvUbdIZC SP:F9 PPU:264, 36 CYC:4187 $F7D9:30 01 BMI $F7DC A:00 X:33 Y:7F P:nvUbdIZC SP:F9 PPU:270, 36 CYC:4189 $F7DB:60 RTS A:00 X:33 Y:7F P:nvUbdIZC SP:F9 PPU:276, 36 CYC:4191 $D279:C8 INY A:00 X:33 Y:7F P:nvUbdIZC SP:FB PPU:294, 36 CYC:4197 $D27A:A9 AA LDA #$AA A:00 X:33 Y:80 P:A5 SP:FB PPU:300, 36 CYC:4199 $D27C:85 78 STA $78 = #$00 A:AA X:33 Y:80 P:A5 SP:FB PPU:306, 36 CYC:4201 $D27E:20 DF F7 JSR $F7DF A:AA X:33 Y:80 P:A5 SP:FB PPU:315, 36 CYC:4204 $F7DF:18 CLC A:AA X:33 Y:80 P:A5 SP:F9 PPU:333, 36 CYC:4210 $F7E0:24 01 BIT $01 = #$FF A:AA X:33 Y:80 P:NvUbdIzc SP:F9 PPU:339, 36 CYC:4212 $F7E2:A9 55 LDA #$55 A:AA X:33 Y:80 P:NVUbdIzc SP:F9 PPU: 7, 37 CYC:4215 $F7E4:60 RTS A:55 X:33 Y:80 P:64 SP:F9 PPU: 13, 37 CYC:4217 $D281:25 78 AND $78 = #$AA A:55 X:33 Y:80 P:64 SP:FB PPU: 31, 37 CYC:4223 $D283:20 E5 F7 JSR $F7E5 A:00 X:33 Y:80 P:nVUbdIZc SP:FB PPU: 40, 37 CYC:4226 $F7E5:D0 07 BNE $F7EE A:00 X:33 Y:80 P:nVUbdIZc SP:F9 PPU: 58, 37 CYC:4232 $F7E7:50 05 BVC $F7EE A:00 X:33 Y:80 P:nVUbdIZc SP:F9 PPU: 64, 37 CYC:4234 $F7E9:B0 03 BCS $F7EE A:00 X:33 Y:80 P:nVUbdIZc SP:F9 PPU: 70, 37 CYC:4236 $F7EB:30 01 BMI $F7EE A:00 X:33 Y:80 P:nVUbdIZc SP:F9 PPU: 76, 37 CYC:4238 $F7ED:60 RTS A:00 X:33 Y:80 P:nVUbdIZc SP:F9 PPU: 82, 37 CYC:4240 $D286:C8 INY A:00 X:33 Y:80 P:nVUbdIZc SP:FB PPU:100, 37 CYC:4246 $D287:A9 EF LDA #$EF A:00 X:33 Y:81 P:NVUbdIzc SP:FB PPU:106, 37 CYC:4248 $D289:85 78 STA $78 = #$AA A:EF X:33 Y:81 P:NVUbdIzc SP:FB PPU:112, 37 CYC:4250 $D28B:20 F1 F7 JSR $F7F1 A:EF X:33 Y:81 P:NVUbdIzc SP:FB PPU:121, 37 CYC:4253 $F7F1:38 SEC A:EF X:33 Y:81 P:NVUbdIzc SP:F9 PPU:139, 37 CYC:4259 $F7F2:B8 CLV A:EF X:33 Y:81 P:E5 SP:F9 PPU:145, 37 CYC:4261 $F7F3:A9 F8 LDA #$F8 A:EF X:33 Y:81 P:A5 SP:F9 PPU:151, 37 CYC:4263 $F7F5:60 RTS A:F8 X:33 Y:81 P:A5 SP:F9 PPU:157, 37 CYC:4265 $D28E:25 78 AND $78 = #$EF A:F8 X:33 Y:81 P:A5 SP:FB PPU:175, 37 CYC:4271 $D290:20 F6 F7 JSR $F7F6 A:E8 X:33 Y:81 P:A5 SP:FB PPU:184, 37 CYC:4274 $F7F6:90 09 BCC $F801 A:E8 X:33 Y:81 P:A5 SP:F9 PPU:202, 37 CYC:4280 $F7F8:10 07 BPL $F801 A:E8 X:33 Y:81 P:A5 SP:F9 PPU:208, 37 CYC:4282 $F7FA:C9 E8 CMP #$E8 A:E8 X:33 Y:81 P:A5 SP:F9 PPU:214, 37 CYC:4284 $F7FC:D0 03 BNE $F801 A:E8 X:33 Y:81 P:nvUbdIZC SP:F9 PPU:220, 37 CYC:4286 $F7FE:70 01 BVS $F801 A:E8 X:33 Y:81 P:nvUbdIZC SP:F9 PPU:226, 37 CYC:4288 $F800:60 RTS A:E8 X:33 Y:81 P:nvUbdIZC SP:F9 PPU:232, 37 CYC:4290 $D293:C8 INY A:E8 X:33 Y:81 P:nvUbdIZC SP:FB PPU:250, 37 CYC:4296 $D294:A9 AA LDA #$AA A:E8 X:33 Y:82 P:A5 SP:FB PPU:256, 37 CYC:4298 $D296:85 78 STA $78 = #$EF A:AA X:33 Y:82 P:A5 SP:FB PPU:262, 37 CYC:4300 $D298:20 04 F8 JSR $F804 A:AA X:33 Y:82 P:A5 SP:FB PPU:271, 37 CYC:4303 $F804:18 CLC A:AA X:33 Y:82 P:A5 SP:F9 PPU:289, 37 CYC:4309 $F805:24 01 BIT $01 = #$FF A:AA X:33 Y:82 P:NvUbdIzc SP:F9 PPU:295, 37 CYC:4311 $F807:A9 5F LDA #$5F A:AA X:33 Y:82 P:NVUbdIzc SP:F9 PPU:304, 37 CYC:4314 $F809:60 RTS A:5F X:33 Y:82 P:64 SP:F9 PPU:310, 37 CYC:4316 $D29B:45 78 EOR $78 = #$AA A:5F X:33 Y:82 P:64 SP:FB PPU:328, 37 CYC:4322 $D29D:20 0A F8 JSR $F80A A:F5 X:33 Y:82 P:NVUbdIzc SP:FB PPU:337, 37 CYC:4325 $F80A:B0 09 BCS $F815 A:F5 X:33 Y:82 P:NVUbdIzc SP:F9 PPU: 14, 38 CYC:4331 $F80C:10 07 BPL $F815 A:F5 X:33 Y:82 P:NVUbdIzc SP:F9 PPU: 20, 38 CYC:4333 $F80E:C9 F5 CMP #$F5 A:F5 X:33 Y:82 P:NVUbdIzc SP:F9 PPU: 26, 38 CYC:4335 $F810:D0 03 BNE $F815 A:F5 X:33 Y:82 P:67 SP:F9 PPU: 32, 38 CYC:4337 $F812:50 01 BVC $F815 A:F5 X:33 Y:82 P:67 SP:F9 PPU: 38, 38 CYC:4339 $F814:60 RTS A:F5 X:33 Y:82 P:67 SP:F9 PPU: 44, 38 CYC:4341 $D2A0:C8 INY A:F5 X:33 Y:82 P:67 SP:FB PPU: 62, 38 CYC:4347 $D2A1:A9 70 LDA #$70 A:F5 X:33 Y:83 P:E5 SP:FB PPU: 68, 38 CYC:4349 $D2A3:85 78 STA $78 = #$AA A:70 X:33 Y:83 P:65 SP:FB PPU: 74, 38 CYC:4351 $D2A5:20 18 F8 JSR $F818 A:70 X:33 Y:83 P:65 SP:FB PPU: 83, 38 CYC:4354 $F818:38 SEC A:70 X:33 Y:83 P:65 SP:F9 PPU:101, 38 CYC:4360 $F819:B8 CLV A:70 X:33 Y:83 P:65 SP:F9 PPU:107, 38 CYC:4362 $F81A:A9 70 LDA #$70 A:70 X:33 Y:83 P:25 SP:F9 PPU:113, 38 CYC:4364 $F81C:60 RTS A:70 X:33 Y:83 P:25 SP:F9 PPU:119, 38 CYC:4366 $D2A8:45 78 EOR $78 = #$70 A:70 X:33 Y:83 P:25 SP:FB PPU:137, 38 CYC:4372 $D2AA:20 1D F8 JSR $F81D A:00 X:33 Y:83 P:nvUbdIZC SP:FB PPU:146, 38 CYC:4375 $F81D:D0 07 BNE $F826 A:00 X:33 Y:83 P:nvUbdIZC SP:F9 PPU:164, 38 CYC:4381 $F81F:70 05 BVS $F826 A:00 X:33 Y:83 P:nvUbdIZC SP:F9 PPU:170, 38 CYC:4383 $F821:90 03 BCC $F826 A:00 X:33 Y:83 P:nvUbdIZC SP:F9 PPU:176, 38 CYC:4385 $F823:30 01 BMI $F826 A:00 X:33 Y:83 P:nvUbdIZC SP:F9 PPU:182, 38 CYC:4387 $F825:60 RTS A:00 X:33 Y:83 P:nvUbdIZC SP:F9 PPU:188, 38 CYC:4389 $D2AD:C8 INY A:00 X:33 Y:83 P:nvUbdIZC SP:FB PPU:206, 38 CYC:4395 $D2AE:A9 69 LDA #$69 A:00 X:33 Y:84 P:A5 SP:FB PPU:212, 38 CYC:4397 $D2B0:85 78 STA $78 = #$70 A:69 X:33 Y:84 P:25 SP:FB PPU:218, 38 CYC:4399 $D2B2:20 29 F8 JSR $F829 A:69 X:33 Y:84 P:25 SP:FB PPU:227, 38 CYC:4402 $F829:18 CLC A:69 X:33 Y:84 P:25 SP:F9 PPU:245, 38 CYC:4408 $F82A:24 01 BIT $01 = #$FF A:69 X:33 Y:84 P:nvUbdIzc SP:F9 PPU:251, 38 CYC:4410 $F82C:A9 00 LDA #$00 A:69 X:33 Y:84 P:NVUbdIzc SP:F9 PPU:260, 38 CYC:4413 $F82E:60 RTS A:00 X:33 Y:84 P:nVUbdIZc SP:F9 PPU:266, 38 CYC:4415 $D2B5:65 78 ADC $78 = #$69 A:00 X:33 Y:84 P:nVUbdIZc SP:FB PPU:284, 38 CYC:4421 $D2B7:20 2F F8 JSR $F82F A:69 X:33 Y:84 P:nvUbdIzc SP:FB PPU:293, 38 CYC:4424 $F82F:30 09 BMI $F83A A:69 X:33 Y:84 P:nvUbdIzc SP:F9 PPU:311, 38 CYC:4430 $F831:B0 07 BCS $F83A A:69 X:33 Y:84 P:nvUbdIzc SP:F9 PPU:317, 38 CYC:4432 $F833:C9 69 CMP #$69 A:69 X:33 Y:84 P:nvUbdIzc SP:F9 PPU:323, 38 CYC:4434 $F835:D0 03 BNE $F83A A:69 X:33 Y:84 P:nvUbdIZC SP:F9 PPU:329, 38 CYC:4436 $F837:70 01 BVS $F83A A:69 X:33 Y:84 P:nvUbdIZC SP:F9 PPU:335, 38 CYC:4438 $F839:60 RTS A:69 X:33 Y:84 P:nvUbdIZC SP:F9 PPU: 0, 39 CYC:4440 $D2BA:C8 INY A:69 X:33 Y:84 P:nvUbdIZC SP:FB PPU: 18, 39 CYC:4446 $D2BB:20 3D F8 JSR $F83D A:69 X:33 Y:85 P:A5 SP:FB PPU: 24, 39 CYC:4448 $F83D:38 SEC A:69 X:33 Y:85 P:A5 SP:F9 PPU: 42, 39 CYC:4454 $F83E:24 01 BIT $01 = #$FF A:69 X:33 Y:85 P:A5 SP:F9 PPU: 48, 39 CYC:4456 $F840:A9 00 LDA #$00 A:69 X:33 Y:85 P:E5 SP:F9 PPU: 57, 39 CYC:4459 $F842:60 RTS A:00 X:33 Y:85 P:67 SP:F9 PPU: 63, 39 CYC:4461 $D2BE:65 78 ADC $78 = #$69 A:00 X:33 Y:85 P:67 SP:FB PPU: 81, 39 CYC:4467 $D2C0:20 43 F8 JSR $F843 A:6A X:33 Y:85 P:nvUbdIzc SP:FB PPU: 90, 39 CYC:4470 $F843:30 09 BMI $F84E A:6A X:33 Y:85 P:nvUbdIzc SP:F9 PPU:108, 39 CYC:4476 $F845:B0 07 BCS $F84E A:6A X:33 Y:85 P:nvUbdIzc SP:F9 PPU:114, 39 CYC:4478 $F847:C9 6A CMP #$6A A:6A X:33 Y:85 P:nvUbdIzc SP:F9 PPU:120, 39 CYC:4480 $F849:D0 03 BNE $F84E A:6A X:33 Y:85 P:nvUbdIZC SP:F9 PPU:126, 39 CYC:4482 $F84B:70 01 BVS $F84E A:6A X:33 Y:85 P:nvUbdIZC SP:F9 PPU:132, 39 CYC:4484 $F84D:60 RTS A:6A X:33 Y:85 P:nvUbdIZC SP:F9 PPU:138, 39 CYC:4486 $D2C3:C8 INY A:6A X:33 Y:85 P:nvUbdIZC SP:FB PPU:156, 39 CYC:4492 $D2C4:A9 7F LDA #$7F A:6A X:33 Y:86 P:A5 SP:FB PPU:162, 39 CYC:4494 $D2C6:85 78 STA $78 = #$69 A:7F X:33 Y:86 P:25 SP:FB PPU:168, 39 CYC:4496 $D2C8:20 51 F8 JSR $F851 A:7F X:33 Y:86 P:25 SP:FB PPU:177, 39 CYC:4499 $F851:38 SEC A:7F X:33 Y:86 P:25 SP:F9 PPU:195, 39 CYC:4505 $F852:B8 CLV A:7F X:33 Y:86 P:25 SP:F9 PPU:201, 39 CYC:4507 $F853:A9 7F LDA #$7F A:7F X:33 Y:86 P:25 SP:F9 PPU:207, 39 CYC:4509 $F855:60 RTS A:7F X:33 Y:86 P:25 SP:F9 PPU:213, 39 CYC:4511 $D2CB:65 78 ADC $78 = #$7F A:7F X:33 Y:86 P:25 SP:FB PPU:231, 39 CYC:4517 $D2CD:20 56 F8 JSR $F856 A:FF X:33 Y:86 P:NVUbdIzc SP:FB PPU:240, 39 CYC:4520 $F856:10 09 BPL $F861 A:FF X:33 Y:86 P:NVUbdIzc SP:F9 PPU:258, 39 CYC:4526 $F858:B0 07 BCS $F861 A:FF X:33 Y:86 P:NVUbdIzc SP:F9 PPU:264, 39 CYC:4528 $F85A:C9 FF CMP #$FF A:FF X:33 Y:86 P:NVUbdIzc SP:F9 PPU:270, 39 CYC:4530 $F85C:D0 03 BNE $F861 A:FF X:33 Y:86 P:67 SP:F9 PPU:276, 39 CYC:4532 $F85E:50 01 BVC $F861 A:FF X:33 Y:86 P:67 SP:F9 PPU:282, 39 CYC:4534 $F860:60 RTS A:FF X:33 Y:86 P:67 SP:F9 PPU:288, 39 CYC:4536 $D2D0:C8 INY A:FF X:33 Y:86 P:67 SP:FB PPU:306, 39 CYC:4542 $D2D1:A9 80 LDA #$80 A:FF X:33 Y:87 P:E5 SP:FB PPU:312, 39 CYC:4544 $D2D3:85 78 STA $78 = #$7F A:80 X:33 Y:87 P:E5 SP:FB PPU:318, 39 CYC:4546 $D2D5:20 64 F8 JSR $F864 A:80 X:33 Y:87 P:E5 SP:FB PPU:327, 39 CYC:4549 $F864:18 CLC A:80 X:33 Y:87 P:E5 SP:F9 PPU: 4, 40 CYC:4555 $F865:24 01 BIT $01 = #$FF A:80 X:33 Y:87 P:NVUbdIzc SP:F9 PPU: 10, 40 CYC:4557 $F867:A9 7F LDA #$7F A:80 X:33 Y:87 P:NVUbdIzc SP:F9 PPU: 19, 40 CYC:4560 $F869:60 RTS A:7F X:33 Y:87 P:64 SP:F9 PPU: 25, 40 CYC:4562 $D2D8:65 78 ADC $78 = #$80 A:7F X:33 Y:87 P:64 SP:FB PPU: 43, 40 CYC:4568 $D2DA:20 6A F8 JSR $F86A A:FF X:33 Y:87 P:NvUbdIzc SP:FB PPU: 52, 40 CYC:4571 $F86A:10 09 BPL $F875 A:FF X:33 Y:87 P:NvUbdIzc SP:F9 PPU: 70, 40 CYC:4577 $F86C:B0 07 BCS $F875 A:FF X:33 Y:87 P:NvUbdIzc SP:F9 PPU: 76, 40 CYC:4579 $F86E:C9 FF CMP #$FF A:FF X:33 Y:87 P:NvUbdIzc SP:F9 PPU: 82, 40 CYC:4581 $F870:D0 03 BNE $F875 A:FF X:33 Y:87 P:nvUbdIZC SP:F9 PPU: 88, 40 CYC:4583 $F872:70 01 BVS $F875 A:FF X:33 Y:87 P:nvUbdIZC SP:F9 PPU: 94, 40 CYC:4585 $F874:60 RTS A:FF X:33 Y:87 P:nvUbdIZC SP:F9 PPU:100, 40 CYC:4587 $D2DD:C8 INY A:FF X:33 Y:87 P:nvUbdIZC SP:FB PPU:118, 40 CYC:4593 $D2DE:20 78 F8 JSR $F878 A:FF X:33 Y:88 P:A5 SP:FB PPU:124, 40 CYC:4595 $F878:38 SEC A:FF X:33 Y:88 P:A5 SP:F9 PPU:142, 40 CYC:4601 $F879:B8 CLV A:FF X:33 Y:88 P:A5 SP:F9 PPU:148, 40 CYC:4603 $F87A:A9 7F LDA #$7F A:FF X:33 Y:88 P:A5 SP:F9 PPU:154, 40 CYC:4605 $F87C:60 RTS A:7F X:33 Y:88 P:25 SP:F9 PPU:160, 40 CYC:4607 $D2E1:65 78 ADC $78 = #$80 A:7F X:33 Y:88 P:25 SP:FB PPU:178, 40 CYC:4613 $D2E3:20 7D F8 JSR $F87D A:00 X:33 Y:88 P:nvUbdIZC SP:FB PPU:187, 40 CYC:4616 $F87D:D0 07 BNE $F886 A:00 X:33 Y:88 P:nvUbdIZC SP:F9 PPU:205, 40 CYC:4622 $F87F:30 05 BMI $F886 A:00 X:33 Y:88 P:nvUbdIZC SP:F9 PPU:211, 40 CYC:4624 $F881:70 03 BVS $F886 A:00 X:33 Y:88 P:nvUbdIZC SP:F9 PPU:217, 40 CYC:4626 $F883:90 01 BCC $F886 A:00 X:33 Y:88 P:nvUbdIZC SP:F9 PPU:223, 40 CYC:4628 $F885:60 RTS A:00 X:33 Y:88 P:nvUbdIZC SP:F9 PPU:229, 40 CYC:4630 $D2E6:C8 INY A:00 X:33 Y:88 P:nvUbdIZC SP:FB PPU:247, 40 CYC:4636 $D2E7:A9 40 LDA #$40 A:00 X:33 Y:89 P:A5 SP:FB PPU:253, 40 CYC:4638 $D2E9:85 78 STA $78 = #$80 A:40 X:33 Y:89 P:25 SP:FB PPU:259, 40 CYC:4640 $D2EB:20 89 F8 JSR $F889 A:40 X:33 Y:89 P:25 SP:FB PPU:268, 40 CYC:4643 $F889:24 01 BIT $01 = #$FF A:40 X:33 Y:89 P:25 SP:F9 PPU:286, 40 CYC:4649 $F88B:A9 40 LDA #$40 A:40 X:33 Y:89 P:E5 SP:F9 PPU:295, 40 CYC:4652 $F88D:60 RTS A:40 X:33 Y:89 P:65 SP:F9 PPU:301, 40 CYC:4654 $D2EE:C5 78 CMP $78 = #$40 A:40 X:33 Y:89 P:65 SP:FB PPU:319, 40 CYC:4660 $D2F0:20 8E F8 JSR $F88E A:40 X:33 Y:89 P:67 SP:FB PPU:328, 40 CYC:4663 $F88E:30 07 BMI $F897 A:40 X:33 Y:89 P:67 SP:F9 PPU: 5, 41 CYC:4669 $F890:90 05 BCC $F897 A:40 X:33 Y:89 P:67 SP:F9 PPU: 11, 41 CYC:4671 $F892:D0 03 BNE $F897 A:40 X:33 Y:89 P:67 SP:F9 PPU: 17, 41 CYC:4673 $F894:50 01 BVC $F897 A:40 X:33 Y:89 P:67 SP:F9 PPU: 23, 41 CYC:4675 $F896:60 RTS A:40 X:33 Y:89 P:67 SP:F9 PPU: 29, 41 CYC:4677 $D2F3:C8 INY A:40 X:33 Y:89 P:67 SP:FB PPU: 47, 41 CYC:4683 $D2F4:48 PHA A:40 X:33 Y:8A P:E5 SP:FB PPU: 53, 41 CYC:4685 $D2F5:A9 3F LDA #$3F A:40 X:33 Y:8A P:E5 SP:FA PPU: 62, 41 CYC:4688 $D2F7:85 78 STA $78 = #$40 A:3F X:33 Y:8A P:65 SP:FA PPU: 68, 41 CYC:4690 $D2F9:68 PLA A:3F X:33 Y:8A P:65 SP:FA PPU: 77, 41 CYC:4693 $D2FA:20 9A F8 JSR $F89A A:40 X:33 Y:8A P:65 SP:FB PPU: 89, 41 CYC:4697 $F89A:B8 CLV A:40 X:33 Y:8A P:65 SP:F9 PPU:107, 41 CYC:4703 $F89B:60 RTS A:40 X:33 Y:8A P:25 SP:F9 PPU:113, 41 CYC:4705 $D2FD:C5 78 CMP $78 = #$3F A:40 X:33 Y:8A P:25 SP:FB PPU:131, 41 CYC:4711 $D2FF:20 9C F8 JSR $F89C A:40 X:33 Y:8A P:25 SP:FB PPU:140, 41 CYC:4714 $F89C:F0 07 BEQ $F8A5 A:40 X:33 Y:8A P:25 SP:F9 PPU:158, 41 CYC:4720 $F89E:30 05 BMI $F8A5 A:40 X:33 Y:8A P:25 SP:F9 PPU:164, 41 CYC:4722 $F8A0:90 03 BCC $F8A5 A:40 X:33 Y:8A P:25 SP:F9 PPU:170, 41 CYC:4724 $F8A2:70 01 BVS $F8A5 A:40 X:33 Y:8A P:25 SP:F9 PPU:176, 41 CYC:4726 $F8A4:60 RTS A:40 X:33 Y:8A P:25 SP:F9 PPU:182, 41 CYC:4728 $D302:C8 INY A:40 X:33 Y:8A P:25 SP:FB PPU:200, 41 CYC:4734 $D303:48 PHA A:40 X:33 Y:8B P:A5 SP:FB PPU:206, 41 CYC:4736 $D304:A9 41 LDA #$41 A:40 X:33 Y:8B P:A5 SP:FA PPU:215, 41 CYC:4739 $D306:85 78 STA $78 = #$3F A:41 X:33 Y:8B P:25 SP:FA PPU:221, 41 CYC:4741 $D308:68 PLA A:41 X:33 Y:8B P:25 SP:FA PPU:230, 41 CYC:4744 $D309:C5 78 CMP $78 = #$41 A:40 X:33 Y:8B P:25 SP:FB PPU:242, 41 CYC:4748 $D30B:20 A8 F8 JSR $F8A8 A:40 X:33 Y:8B P:NvUbdIzc SP:FB PPU:251, 41 CYC:4751 $F8A8:F0 05 BEQ $F8AF A:40 X:33 Y:8B P:NvUbdIzc SP:F9 PPU:269, 41 CYC:4757 $F8AA:10 03 BPL $F8AF A:40 X:33 Y:8B P:NvUbdIzc SP:F9 PPU:275, 41 CYC:4759 $F8AC:10 01 BPL $F8AF A:40 X:33 Y:8B P:NvUbdIzc SP:F9 PPU:281, 41 CYC:4761 $F8AE:60 RTS A:40 X:33 Y:8B P:NvUbdIzc SP:F9 PPU:287, 41 CYC:4763 $D30E:C8 INY A:40 X:33 Y:8B P:NvUbdIzc SP:FB PPU:305, 41 CYC:4769 $D30F:48 PHA A:40 X:33 Y:8C P:NvUbdIzc SP:FB PPU:311, 41 CYC:4771 $D310:A9 00 LDA #$00 A:40 X:33 Y:8C P:NvUbdIzc SP:FA PPU:320, 41 CYC:4774 $D312:85 78 STA $78 = #$41 A:00 X:33 Y:8C P:nvUbdIZc SP:FA PPU:326, 41 CYC:4776 $D314:68 PLA A:00 X:33 Y:8C P:nvUbdIZc SP:FA PPU:335, 41 CYC:4779 $D315:20 B2 F8 JSR $F8B2 A:40 X:33 Y:8C P:nvUbdIzc SP:FB PPU: 6, 42 CYC:4783 $F8B2:A9 80 LDA #$80 A:40 X:33 Y:8C P:nvUbdIzc SP:F9 PPU: 24, 42 CYC:4789 $F8B4:60 RTS A:80 X:33 Y:8C P:NvUbdIzc SP:F9 PPU: 30, 42 CYC:4791 $D318:C5 78 CMP $78 = #$00 A:80 X:33 Y:8C P:NvUbdIzc SP:FB PPU: 48, 42 CYC:4797 $D31A:20 B5 F8 JSR $F8B5 A:80 X:33 Y:8C P:A5 SP:FB PPU: 57, 42 CYC:4800 $F8B5:F0 05 BEQ $F8BC A:80 X:33 Y:8C P:A5 SP:F9 PPU: 75, 42 CYC:4806 $F8B7:10 03 BPL $F8BC A:80 X:33 Y:8C P:A5 SP:F9 PPU: 81, 42 CYC:4808 $F8B9:90 01 BCC $F8BC A:80 X:33 Y:8C P:A5 SP:F9 PPU: 87, 42 CYC:4810 $F8BB:60 RTS A:80 X:33 Y:8C P:A5 SP:F9 PPU: 93, 42 CYC:4812 $D31D:C8 INY A:80 X:33 Y:8C P:A5 SP:FB PPU:111, 42 CYC:4818 $D31E:48 PHA A:80 X:33 Y:8D P:A5 SP:FB PPU:117, 42 CYC:4820 $D31F:A9 80 LDA #$80 A:80 X:33 Y:8D P:A5 SP:FA PPU:126, 42 CYC:4823 $D321:85 78 STA $78 = #$00 A:80 X:33 Y:8D P:A5 SP:FA PPU:132, 42 CYC:4825 $D323:68 PLA A:80 X:33 Y:8D P:A5 SP:FA PPU:141, 42 CYC:4828 $D324:C5 78 CMP $78 = #$80 A:80 X:33 Y:8D P:A5 SP:FB PPU:153, 42 CYC:4832 $D326:20 BF F8 JSR $F8BF A:80 X:33 Y:8D P:nvUbdIZC SP:FB PPU:162, 42 CYC:4835 $F8BF:D0 05 BNE $F8C6 A:80 X:33 Y:8D P:nvUbdIZC SP:F9 PPU:180, 42 CYC:4841 $F8C1:30 03 BMI $F8C6 A:80 X:33 Y:8D P:nvUbdIZC SP:F9 PPU:186, 42 CYC:4843 $F8C3:90 01 BCC $F8C6 A:80 X:33 Y:8D P:nvUbdIZC SP:F9 PPU:192, 42 CYC:4845 $F8C5:60 RTS A:80 X:33 Y:8D P:nvUbdIZC SP:F9 PPU:198, 42 CYC:4847 $D329:C8 INY A:80 X:33 Y:8D P:nvUbdIZC SP:FB PPU:216, 42 CYC:4853 $D32A:48 PHA A:80 X:33 Y:8E P:A5 SP:FB PPU:222, 42 CYC:4855 $D32B:A9 81 LDA #$81 A:80 X:33 Y:8E P:A5 SP:FA PPU:231, 42 CYC:4858 $D32D:85 78 STA $78 = #$80 A:81 X:33 Y:8E P:A5 SP:FA PPU:237, 42 CYC:4860 $D32F:68 PLA A:81 X:33 Y:8E P:A5 SP:FA PPU:246, 42 CYC:4863 $D330:C5 78 CMP $78 = #$81 A:80 X:33 Y:8E P:A5 SP:FB PPU:258, 42 CYC:4867 $D332:20 C9 F8 JSR $F8C9 A:80 X:33 Y:8E P:NvUbdIzc SP:FB PPU:267, 42 CYC:4870 $F8C9:B0 05 BCS $F8D0 A:80 X:33 Y:8E P:NvUbdIzc SP:F9 PPU:285, 42 CYC:4876 $F8CB:F0 03 BEQ $F8D0 A:80 X:33 Y:8E P:NvUbdIzc SP:F9 PPU:291, 42 CYC:4878 $F8CD:10 01 BPL $F8D0 A:80 X:33 Y:8E P:NvUbdIzc SP:F9 PPU:297, 42 CYC:4880 $F8CF:60 RTS A:80 X:33 Y:8E P:NvUbdIzc SP:F9 PPU:303, 42 CYC:4882 $D335:C8 INY A:80 X:33 Y:8E P:NvUbdIzc SP:FB PPU:321, 42 CYC:4888 $D336:48 PHA A:80 X:33 Y:8F P:NvUbdIzc SP:FB PPU:327, 42 CYC:4890 $D337:A9 7F LDA #$7F A:80 X:33 Y:8F P:NvUbdIzc SP:FA PPU:336, 42 CYC:4893 $D339:85 78 STA $78 = #$81 A:7F X:33 Y:8F P:nvUbdIzc SP:FA PPU: 1, 43 CYC:4895 $D33B:68 PLA A:7F X:33 Y:8F P:nvUbdIzc SP:FA PPU: 10, 43 CYC:4898 $D33C:C5 78 CMP $78 = #$7F A:80 X:33 Y:8F P:NvUbdIzc SP:FB PPU: 22, 43 CYC:4902 $D33E:20 D3 F8 JSR $F8D3 A:80 X:33 Y:8F P:25 SP:FB PPU: 31, 43 CYC:4905 $F8D3:90 05 BCC $F8DA A:80 X:33 Y:8F P:25 SP:F9 PPU: 49, 43 CYC:4911 $F8D5:F0 03 BEQ $F8DA A:80 X:33 Y:8F P:25 SP:F9 PPU: 55, 43 CYC:4913 $F8D7:30 01 BMI $F8DA A:80 X:33 Y:8F P:25 SP:F9 PPU: 61, 43 CYC:4915 $F8D9:60 RTS A:80 X:33 Y:8F P:25 SP:F9 PPU: 67, 43 CYC:4917 $D341:C8 INY A:80 X:33 Y:8F P:25 SP:FB PPU: 85, 43 CYC:4923 $D342:A9 40 LDA #$40 A:80 X:33 Y:90 P:A5 SP:FB PPU: 91, 43 CYC:4925 $D344:85 78 STA $78 = #$7F A:40 X:33 Y:90 P:25 SP:FB PPU: 97, 43 CYC:4927 $D346:20 31 F9 JSR $F931 A:40 X:33 Y:90 P:25 SP:FB PPU:106, 43 CYC:4930 $F931:24 01 BIT $01 = #$FF A:40 X:33 Y:90 P:25 SP:F9 PPU:124, 43 CYC:4936 $F933:A9 40 LDA #$40 A:40 X:33 Y:90 P:E5 SP:F9 PPU:133, 43 CYC:4939 $F935:38 SEC A:40 X:33 Y:90 P:65 SP:F9 PPU:139, 43 CYC:4941 $F936:60 RTS A:40 X:33 Y:90 P:65 SP:F9 PPU:145, 43 CYC:4943 $D349:E5 78 SBC $78 = #$40 A:40 X:33 Y:90 P:65 SP:FB PPU:163, 43 CYC:4949 $D34B:20 37 F9 JSR $F937 A:00 X:33 Y:90 P:nvUbdIZC SP:FB PPU:172, 43 CYC:4952 $F937:30 0B BMI $F944 A:00 X:33 Y:90 P:nvUbdIZC SP:F9 PPU:190, 43 CYC:4958 $F939:90 09 BCC $F944 A:00 X:33 Y:90 P:nvUbdIZC SP:F9 PPU:196, 43 CYC:4960 $F93B:D0 07 BNE $F944 A:00 X:33 Y:90 P:nvUbdIZC SP:F9 PPU:202, 43 CYC:4962 $F93D:70 05 BVS $F944 A:00 X:33 Y:90 P:nvUbdIZC SP:F9 PPU:208, 43 CYC:4964 $F93F:C9 00 CMP #$00 A:00 X:33 Y:90 P:nvUbdIZC SP:F9 PPU:214, 43 CYC:4966 $F941:D0 01 BNE $F944 A:00 X:33 Y:90 P:nvUbdIZC SP:F9 PPU:220, 43 CYC:4968 $F943:60 RTS A:00 X:33 Y:90 P:nvUbdIZC SP:F9 PPU:226, 43 CYC:4970 $D34E:C8 INY A:00 X:33 Y:90 P:nvUbdIZC SP:FB PPU:244, 43 CYC:4976 $D34F:A9 3F LDA #$3F A:00 X:33 Y:91 P:A5 SP:FB PPU:250, 43 CYC:4978 $D351:85 78 STA $78 = #$40 A:3F X:33 Y:91 P:25 SP:FB PPU:256, 43 CYC:4980 $D353:20 47 F9 JSR $F947 A:3F X:33 Y:91 P:25 SP:FB PPU:265, 43 CYC:4983 $F947:B8 CLV A:3F X:33 Y:91 P:25 SP:F9 PPU:283, 43 CYC:4989 $F948:38 SEC A:3F X:33 Y:91 P:25 SP:F9 PPU:289, 43 CYC:4991 $F949:A9 40 LDA #$40 A:3F X:33 Y:91 P:25 SP:F9 PPU:295, 43 CYC:4993 $F94B:60 RTS A:40 X:33 Y:91 P:25 SP:F9 PPU:301, 43 CYC:4995 $D356:E5 78 SBC $78 = #$3F A:40 X:33 Y:91 P:25 SP:FB PPU:319, 43 CYC:5001 $D358:20 4C F9 JSR $F94C A:01 X:33 Y:91 P:25 SP:FB PPU:328, 43 CYC:5004 $F94C:F0 0B BEQ $F959 A:01 X:33 Y:91 P:25 SP:F9 PPU: 5, 44 CYC:5010 $F94E:30 09 BMI $F959 A:01 X:33 Y:91 P:25 SP:F9 PPU: 11, 44 CYC:5012 $F950:90 07 BCC $F959 A:01 X:33 Y:91 P:25 SP:F9 PPU: 17, 44 CYC:5014 $F952:70 05 BVS $F959 A:01 X:33 Y:91 P:25 SP:F9 PPU: 23, 44 CYC:5016 $F954:C9 01 CMP #$01 A:01 X:33 Y:91 P:25 SP:F9 PPU: 29, 44 CYC:5018 $F956:D0 01 BNE $F959 A:01 X:33 Y:91 P:nvUbdIZC SP:F9 PPU: 35, 44 CYC:5020 $F958:60 RTS A:01 X:33 Y:91 P:nvUbdIZC SP:F9 PPU: 41, 44 CYC:5022 $D35B:C8 INY A:01 X:33 Y:91 P:nvUbdIZC SP:FB PPU: 59, 44 CYC:5028 $D35C:A9 41 LDA #$41 A:01 X:33 Y:92 P:A5 SP:FB PPU: 65, 44 CYC:5030 $D35E:85 78 STA $78 = #$3F A:41 X:33 Y:92 P:25 SP:FB PPU: 71, 44 CYC:5032 $D360:20 5C F9 JSR $F95C A:41 X:33 Y:92 P:25 SP:FB PPU: 80, 44 CYC:5035 $F95C:A9 40 LDA #$40 A:41 X:33 Y:92 P:25 SP:F9 PPU: 98, 44 CYC:5041 $F95E:38 SEC A:40 X:33 Y:92 P:25 SP:F9 PPU:104, 44 CYC:5043 $F95F:24 01 BIT $01 = #$FF A:40 X:33 Y:92 P:25 SP:F9 PPU:110, 44 CYC:5045 $F961:60 RTS A:40 X:33 Y:92 P:E5 SP:F9 PPU:119, 44 CYC:5048 $D363:E5 78 SBC $78 = #$41 A:40 X:33 Y:92 P:E5 SP:FB PPU:137, 44 CYC:5054 $D365:20 62 F9 JSR $F962 A:FF X:33 Y:92 P:NvUbdIzc SP:FB PPU:146, 44 CYC:5057 $F962:B0 0B BCS $F96F A:FF X:33 Y:92 P:NvUbdIzc SP:F9 PPU:164, 44 CYC:5063 $F964:F0 09 BEQ $F96F A:FF X:33 Y:92 P:NvUbdIzc SP:F9 PPU:170, 44 CYC:5065 $F966:10 07 BPL $F96F A:FF X:33 Y:92 P:NvUbdIzc SP:F9 PPU:176, 44 CYC:5067 $F968:70 05 BVS $F96F A:FF X:33 Y:92 P:NvUbdIzc SP:F9 PPU:182, 44 CYC:5069 $F96A:C9 FF CMP #$FF A:FF X:33 Y:92 P:NvUbdIzc SP:F9 PPU:188, 44 CYC:5071 $F96C:D0 01 BNE $F96F A:FF X:33 Y:92 P:nvUbdIZC SP:F9 PPU:194, 44 CYC:5073 $F96E:60 RTS A:FF X:33 Y:92 P:nvUbdIZC SP:F9 PPU:200, 44 CYC:5075 $D368:C8 INY A:FF X:33 Y:92 P:nvUbdIZC SP:FB PPU:218, 44 CYC:5081 $D369:A9 00 LDA #$00 A:FF X:33 Y:93 P:A5 SP:FB PPU:224, 44 CYC:5083 $D36B:85 78 STA $78 = #$41 A:00 X:33 Y:93 P:nvUbdIZC SP:FB PPU:230, 44 CYC:5085 $D36D:20 72 F9 JSR $F972 A:00 X:33 Y:93 P:nvUbdIZC SP:FB PPU:239, 44 CYC:5088 $F972:18 CLC A:00 X:33 Y:93 P:nvUbdIZC SP:F9 PPU:257, 44 CYC:5094 $F973:A9 80 LDA #$80 A:00 X:33 Y:93 P:nvUbdIZc SP:F9 PPU:263, 44 CYC:5096 $F975:60 RTS A:80 X:33 Y:93 P:NvUbdIzc SP:F9 PPU:269, 44 CYC:5098 $D370:E5 78 SBC $78 = #$00 A:80 X:33 Y:93 P:NvUbdIzc SP:FB PPU:287, 44 CYC:5104 $D372:20 76 F9 JSR $F976 A:7F X:33 Y:93 P:65 SP:FB PPU:296, 44 CYC:5107 $F976:90 05 BCC $F97D A:7F X:33 Y:93 P:65 SP:F9 PPU:314, 44 CYC:5113 $F978:C9 7F CMP #$7F A:7F X:33 Y:93 P:65 SP:F9 PPU:320, 44 CYC:5115 $F97A:D0 01 BNE $F97D A:7F X:33 Y:93 P:67 SP:F9 PPU:326, 44 CYC:5117 $F97C:60 RTS A:7F X:33 Y:93 P:67 SP:F9 PPU:332, 44 CYC:5119 $D375:C8 INY A:7F X:33 Y:93 P:67 SP:FB PPU: 9, 45 CYC:5125 $D376:A9 7F LDA #$7F A:7F X:33 Y:94 P:E5 SP:FB PPU: 15, 45 CYC:5127 $D378:85 78 STA $78 = #$00 A:7F X:33 Y:94 P:65 SP:FB PPU: 21, 45 CYC:5129 $D37A:20 80 F9 JSR $F980 A:7F X:33 Y:94 P:65 SP:FB PPU: 30, 45 CYC:5132 $F980:38 SEC A:7F X:33 Y:94 P:65 SP:F9 PPU: 48, 45 CYC:5138 $F981:A9 81 LDA #$81 A:7F X:33 Y:94 P:65 SP:F9 PPU: 54, 45 CYC:5140 $F983:60 RTS A:81 X:33 Y:94 P:E5 SP:F9 PPU: 60, 45 CYC:5142 $D37D:E5 78 SBC $78 = #$7F A:81 X:33 Y:94 P:E5 SP:FB PPU: 78, 45 CYC:5148 $D37F:20 84 F9 JSR $F984 A:02 X:33 Y:94 P:65 SP:FB PPU: 87, 45 CYC:5151 $F984:50 07 BVC $F98D A:02 X:33 Y:94 P:65 SP:F9 PPU:105, 45 CYC:5157 $F986:90 05 BCC $F98D A:02 X:33 Y:94 P:65 SP:F9 PPU:111, 45 CYC:5159 $F988:C9 02 CMP #$02 A:02 X:33 Y:94 P:65 SP:F9 PPU:117, 45 CYC:5161 $F98A:D0 01 BNE $F98D A:02 X:33 Y:94 P:67 SP:F9 PPU:123, 45 CYC:5163 $F98C:60 RTS A:02 X:33 Y:94 P:67 SP:F9 PPU:129, 45 CYC:5165 $D382:C8 INY A:02 X:33 Y:94 P:67 SP:FB PPU:147, 45 CYC:5171 $D383:A9 40 LDA #$40 A:02 X:33 Y:95 P:E5 SP:FB PPU:153, 45 CYC:5173 $D385:85 78 STA $78 = #$7F A:40 X:33 Y:95 P:65 SP:FB PPU:159, 45 CYC:5175 $D387:20 89 F8 JSR $F889 A:40 X:33 Y:95 P:65 SP:FB PPU:168, 45 CYC:5178 $F889:24 01 BIT $01 = #$FF A:40 X:33 Y:95 P:65 SP:F9 PPU:186, 45 CYC:5184 $F88B:A9 40 LDA #$40 A:40 X:33 Y:95 P:E5 SP:F9 PPU:195, 45 CYC:5187 $F88D:60 RTS A:40 X:33 Y:95 P:65 SP:F9 PPU:201, 45 CYC:5189 $D38A:AA TAX A:40 X:33 Y:95 P:65 SP:FB PPU:219, 45 CYC:5195 $D38B:E4 78 CPX $78 = #$40 A:40 X:40 Y:95 P:65 SP:FB PPU:225, 45 CYC:5197 $D38D:20 8E F8 JSR $F88E A:40 X:40 Y:95 P:67 SP:FB PPU:234, 45 CYC:5200 $F88E:30 07 BMI $F897 A:40 X:40 Y:95 P:67 SP:F9 PPU:252, 45 CYC:5206 $F890:90 05 BCC $F897 A:40 X:40 Y:95 P:67 SP:F9 PPU:258, 45 CYC:5208 $F892:D0 03 BNE $F897 A:40 X:40 Y:95 P:67 SP:F9 PPU:264, 45 CYC:5210 $F894:50 01 BVC $F897 A:40 X:40 Y:95 P:67 SP:F9 PPU:270, 45 CYC:5212 $F896:60 RTS A:40 X:40 Y:95 P:67 SP:F9 PPU:276, 45 CYC:5214 $D390:C8 INY A:40 X:40 Y:95 P:67 SP:FB PPU:294, 45 CYC:5220 $D391:A9 3F LDA #$3F A:40 X:40 Y:96 P:E5 SP:FB PPU:300, 45 CYC:5222 $D393:85 78 STA $78 = #$40 A:3F X:40 Y:96 P:65 SP:FB PPU:306, 45 CYC:5224 $D395:20 9A F8 JSR $F89A A:3F X:40 Y:96 P:65 SP:FB PPU:315, 45 CYC:5227 $F89A:B8 CLV A:3F X:40 Y:96 P:65 SP:F9 PPU:333, 45 CYC:5233 $F89B:60 RTS A:3F X:40 Y:96 P:25 SP:F9 PPU:339, 45 CYC:5235 $D398:E4 78 CPX $78 = #$3F A:3F X:40 Y:96 P:25 SP:FB PPU: 16, 46 CYC:5241 $D39A:20 9C F8 JSR $F89C A:3F X:40 Y:96 P:25 SP:FB PPU: 25, 46 CYC:5244 $F89C:F0 07 BEQ $F8A5 A:3F X:40 Y:96 P:25 SP:F9 PPU: 43, 46 CYC:5250 $F89E:30 05 BMI $F8A5 A:3F X:40 Y:96 P:25 SP:F9 PPU: 49, 46 CYC:5252 $F8A0:90 03 BCC $F8A5 A:3F X:40 Y:96 P:25 SP:F9 PPU: 55, 46 CYC:5254 $F8A2:70 01 BVS $F8A5 A:3F X:40 Y:96 P:25 SP:F9 PPU: 61, 46 CYC:5256 $F8A4:60 RTS A:3F X:40 Y:96 P:25 SP:F9 PPU: 67, 46 CYC:5258 $D39D:C8 INY A:3F X:40 Y:96 P:25 SP:FB PPU: 85, 46 CYC:5264 $D39E:A9 41 LDA #$41 A:3F X:40 Y:97 P:A5 SP:FB PPU: 91, 46 CYC:5266 $D3A0:85 78 STA $78 = #$3F A:41 X:40 Y:97 P:25 SP:FB PPU: 97, 46 CYC:5268 $D3A2:E4 78 CPX $78 = #$41 A:41 X:40 Y:97 P:25 SP:FB PPU:106, 46 CYC:5271 $D3A4:20 A8 F8 JSR $F8A8 A:41 X:40 Y:97 P:NvUbdIzc SP:FB PPU:115, 46 CYC:5274 $F8A8:F0 05 BEQ $F8AF A:41 X:40 Y:97 P:NvUbdIzc SP:F9 PPU:133, 46 CYC:5280 $F8AA:10 03 BPL $F8AF A:41 X:40 Y:97 P:NvUbdIzc SP:F9 PPU:139, 46 CYC:5282 $F8AC:10 01 BPL $F8AF A:41 X:40 Y:97 P:NvUbdIzc SP:F9 PPU:145, 46 CYC:5284 $F8AE:60 RTS A:41 X:40 Y:97 P:NvUbdIzc SP:F9 PPU:151, 46 CYC:5286 $D3A7:C8 INY A:41 X:40 Y:97 P:NvUbdIzc SP:FB PPU:169, 46 CYC:5292 $D3A8:A9 00 LDA #$00 A:41 X:40 Y:98 P:NvUbdIzc SP:FB PPU:175, 46 CYC:5294 $D3AA:85 78 STA $78 = #$41 A:00 X:40 Y:98 P:nvUbdIZc SP:FB PPU:181, 46 CYC:5296 $D3AC:20 B2 F8 JSR $F8B2 A:00 X:40 Y:98 P:nvUbdIZc SP:FB PPU:190, 46 CYC:5299 $F8B2:A9 80 LDA #$80 A:00 X:40 Y:98 P:nvUbdIZc SP:F9 PPU:208, 46 CYC:5305 $F8B4:60 RTS A:80 X:40 Y:98 P:NvUbdIzc SP:F9 PPU:214, 46 CYC:5307 $D3AF:AA TAX A:80 X:40 Y:98 P:NvUbdIzc SP:FB PPU:232, 46 CYC:5313 $D3B0:E4 78 CPX $78 = #$00 A:80 X:80 Y:98 P:NvUbdIzc SP:FB PPU:238, 46 CYC:5315 $D3B2:20 B5 F8 JSR $F8B5 A:80 X:80 Y:98 P:A5 SP:FB PPU:247, 46 CYC:5318 $F8B5:F0 05 BEQ $F8BC A:80 X:80 Y:98 P:A5 SP:F9 PPU:265, 46 CYC:5324 $F8B7:10 03 BPL $F8BC A:80 X:80 Y:98 P:A5 SP:F9 PPU:271, 46 CYC:5326 $F8B9:90 01 BCC $F8BC A:80 X:80 Y:98 P:A5 SP:F9 PPU:277, 46 CYC:5328 $F8BB:60 RTS A:80 X:80 Y:98 P:A5 SP:F9 PPU:283, 46 CYC:5330 $D3B5:C8 INY A:80 X:80 Y:98 P:A5 SP:FB PPU:301, 46 CYC:5336 $D3B6:A9 80 LDA #$80 A:80 X:80 Y:99 P:A5 SP:FB PPU:307, 46 CYC:5338 $D3B8:85 78 STA $78 = #$00 A:80 X:80 Y:99 P:A5 SP:FB PPU:313, 46 CYC:5340 $D3BA:E4 78 CPX $78 = #$80 A:80 X:80 Y:99 P:A5 SP:FB PPU:322, 46 CYC:5343 $D3BC:20 BF F8 JSR $F8BF A:80 X:80 Y:99 P:nvUbdIZC SP:FB PPU:331, 46 CYC:5346 $F8BF:D0 05 BNE $F8C6 A:80 X:80 Y:99 P:nvUbdIZC SP:F9 PPU: 8, 47 CYC:5352 $F8C1:30 03 BMI $F8C6 A:80 X:80 Y:99 P:nvUbdIZC SP:F9 PPU: 14, 47 CYC:5354 $F8C3:90 01 BCC $F8C6 A:80 X:80 Y:99 P:nvUbdIZC SP:F9 PPU: 20, 47 CYC:5356 $F8C5:60 RTS A:80 X:80 Y:99 P:nvUbdIZC SP:F9 PPU: 26, 47 CYC:5358 $D3BF:C8 INY A:80 X:80 Y:99 P:nvUbdIZC SP:FB PPU: 44, 47 CYC:5364 $D3C0:A9 81 LDA #$81 A:80 X:80 Y:9A P:A5 SP:FB PPU: 50, 47 CYC:5366 $D3C2:85 78 STA $78 = #$80 A:81 X:80 Y:9A P:A5 SP:FB PPU: 56, 47 CYC:5368 $D3C4:E4 78 CPX $78 = #$81 A:81 X:80 Y:9A P:A5 SP:FB PPU: 65, 47 CYC:5371 $D3C6:20 C9 F8 JSR $F8C9 A:81 X:80 Y:9A P:NvUbdIzc SP:FB PPU: 74, 47 CYC:5374 $F8C9:B0 05 BCS $F8D0 A:81 X:80 Y:9A P:NvUbdIzc SP:F9 PPU: 92, 47 CYC:5380 $F8CB:F0 03 BEQ $F8D0 A:81 X:80 Y:9A P:NvUbdIzc SP:F9 PPU: 98, 47 CYC:5382 $F8CD:10 01 BPL $F8D0 A:81 X:80 Y:9A P:NvUbdIzc SP:F9 PPU:104, 47 CYC:5384 $F8CF:60 RTS A:81 X:80 Y:9A P:NvUbdIzc SP:F9 PPU:110, 47 CYC:5386 $D3C9:C8 INY A:81 X:80 Y:9A P:NvUbdIzc SP:FB PPU:128, 47 CYC:5392 $D3CA:A9 7F LDA #$7F A:81 X:80 Y:9B P:NvUbdIzc SP:FB PPU:134, 47 CYC:5394 $D3CC:85 78 STA $78 = #$81 A:7F X:80 Y:9B P:nvUbdIzc SP:FB PPU:140, 47 CYC:5396 $D3CE:E4 78 CPX $78 = #$7F A:7F X:80 Y:9B P:nvUbdIzc SP:FB PPU:149, 47 CYC:5399 $D3D0:20 D3 F8 JSR $F8D3 A:7F X:80 Y:9B P:25 SP:FB PPU:158, 47 CYC:5402 $F8D3:90 05 BCC $F8DA A:7F X:80 Y:9B P:25 SP:F9 PPU:176, 47 CYC:5408 $F8D5:F0 03 BEQ $F8DA A:7F X:80 Y:9B P:25 SP:F9 PPU:182, 47 CYC:5410 $F8D7:30 01 BMI $F8DA A:7F X:80 Y:9B P:25 SP:F9 PPU:188, 47 CYC:5412 $F8D9:60 RTS A:7F X:80 Y:9B P:25 SP:F9 PPU:194, 47 CYC:5414 $D3D3:C8 INY A:7F X:80 Y:9B P:25 SP:FB PPU:212, 47 CYC:5420 $D3D4:98 TYA A:7F X:80 Y:9C P:A5 SP:FB PPU:218, 47 CYC:5422 $D3D5:AA TAX A:9C X:80 Y:9C P:A5 SP:FB PPU:224, 47 CYC:5424 $D3D6:A9 40 LDA #$40 A:9C X:9C Y:9C P:A5 SP:FB PPU:230, 47 CYC:5426 $D3D8:85 78 STA $78 = #$7F A:40 X:9C Y:9C P:25 SP:FB PPU:236, 47 CYC:5428 $D3DA:20 DD F8 JSR $F8DD A:40 X:9C Y:9C P:25 SP:FB PPU:245, 47 CYC:5431 $F8DD:24 01 BIT $01 = #$FF A:40 X:9C Y:9C P:25 SP:F9 PPU:263, 47 CYC:5437 $F8DF:A0 40 LDY #$40 A:40 X:9C Y:9C P:E5 SP:F9 PPU:272, 47 CYC:5440 $F8E1:60 RTS A:40 X:9C Y:40 P:65 SP:F9 PPU:278, 47 CYC:5442 $D3DD:C4 78 CPY $78 = #$40 A:40 X:9C Y:40 P:65 SP:FB PPU:296, 47 CYC:5448 $D3DF:20 E2 F8 JSR $F8E2 A:40 X:9C Y:40 P:67 SP:FB PPU:305, 47 CYC:5451 $F8E2:30 07 BMI $F8EB A:40 X:9C Y:40 P:67 SP:F9 PPU:323, 47 CYC:5457 $F8E4:90 05 BCC $F8EB A:40 X:9C Y:40 P:67 SP:F9 PPU:329, 47 CYC:5459 $F8E6:D0 03 BNE $F8EB A:40 X:9C Y:40 P:67 SP:F9 PPU:335, 47 CYC:5461 $F8E8:50 01 BVC $F8EB A:40 X:9C Y:40 P:67 SP:F9 PPU: 0, 48 CYC:5463 $F8EA:60 RTS A:40 X:9C Y:40 P:67 SP:F9 PPU: 6, 48 CYC:5465 $D3E2:E8 INX A:40 X:9C Y:40 P:67 SP:FB PPU: 24, 48 CYC:5471 $D3E3:A9 3F LDA #$3F A:40 X:9D Y:40 P:E5 SP:FB PPU: 30, 48 CYC:5473 $D3E5:85 78 STA $78 = #$40 A:3F X:9D Y:40 P:65 SP:FB PPU: 36, 48 CYC:5475 $D3E7:20 EE F8 JSR $F8EE A:3F X:9D Y:40 P:65 SP:FB PPU: 45, 48 CYC:5478 $F8EE:B8 CLV A:3F X:9D Y:40 P:65 SP:F9 PPU: 63, 48 CYC:5484 $F8EF:60 RTS A:3F X:9D Y:40 P:25 SP:F9 PPU: 69, 48 CYC:5486 $D3EA:C4 78 CPY $78 = #$3F A:3F X:9D Y:40 P:25 SP:FB PPU: 87, 48 CYC:5492 $D3EC:20 F0 F8 JSR $F8F0 A:3F X:9D Y:40 P:25 SP:FB PPU: 96, 48 CYC:5495 $F8F0:F0 07 BEQ $F8F9 A:3F X:9D Y:40 P:25 SP:F9 PPU:114, 48 CYC:5501 $F8F2:30 05 BMI $F8F9 A:3F X:9D Y:40 P:25 SP:F9 PPU:120, 48 CYC:5503 $F8F4:90 03 BCC $F8F9 A:3F X:9D Y:40 P:25 SP:F9 PPU:126, 48 CYC:5505 $F8F6:70 01 BVS $F8F9 A:3F X:9D Y:40 P:25 SP:F9 PPU:132, 48 CYC:5507 $F8F8:60 RTS A:3F X:9D Y:40 P:25 SP:F9 PPU:138, 48 CYC:5509 $D3EF:E8 INX A:3F X:9D Y:40 P:25 SP:FB PPU:156, 48 CYC:5515 $D3F0:A9 41 LDA #$41 A:3F X:9E Y:40 P:A5 SP:FB PPU:162, 48 CYC:5517 $D3F2:85 78 STA $78 = #$3F A:41 X:9E Y:40 P:25 SP:FB PPU:168, 48 CYC:5519 $D3F4:C4 78 CPY $78 = #$41 A:41 X:9E Y:40 P:25 SP:FB PPU:177, 48 CYC:5522 $D3F6:20 FC F8 JSR $F8FC A:41 X:9E Y:40 P:NvUbdIzc SP:FB PPU:186, 48 CYC:5525 $F8FC:F0 05 BEQ $F903 A:41 X:9E Y:40 P:NvUbdIzc SP:F9 PPU:204, 48 CYC:5531 $F8FE:10 03 BPL $F903 A:41 X:9E Y:40 P:NvUbdIzc SP:F9 PPU:210, 48 CYC:5533 $F900:10 01 BPL $F903 A:41 X:9E Y:40 P:NvUbdIzc SP:F9 PPU:216, 48 CYC:5535 $F902:60 RTS A:41 X:9E Y:40 P:NvUbdIzc SP:F9 PPU:222, 48 CYC:5537 $D3F9:E8 INX A:41 X:9E Y:40 P:NvUbdIzc SP:FB PPU:240, 48 CYC:5543 $D3FA:A9 00 LDA #$00 A:41 X:9F Y:40 P:NvUbdIzc SP:FB PPU:246, 48 CYC:5545 $D3FC:85 78 STA $78 = #$41 A:00 X:9F Y:40 P:nvUbdIZc SP:FB PPU:252, 48 CYC:5547 $D3FE:20 06 F9 JSR $F906 A:00 X:9F Y:40 P:nvUbdIZc SP:FB PPU:261, 48 CYC:5550 $F906:A0 80 LDY #$80 A:00 X:9F Y:40 P:nvUbdIZc SP:F9 PPU:279, 48 CYC:5556 $F908:60 RTS A:00 X:9F Y:80 P:NvUbdIzc SP:F9 PPU:285, 48 CYC:5558 $D401:C4 78 CPY $78 = #$00 A:00 X:9F Y:80 P:NvUbdIzc SP:FB PPU:303, 48 CYC:5564 $D403:20 09 F9 JSR $F909 A:00 X:9F Y:80 P:A5 SP:FB PPU:312, 48 CYC:5567 $F909:F0 05 BEQ $F910 A:00 X:9F Y:80 P:A5 SP:F9 PPU:330, 48 CYC:5573 $F90B:10 03 BPL $F910 A:00 X:9F Y:80 P:A5 SP:F9 PPU:336, 48 CYC:5575 $F90D:90 01 BCC $F910 A:00 X:9F Y:80 P:A5 SP:F9 PPU: 1, 49 CYC:5577 $F90F:60 RTS A:00 X:9F Y:80 P:A5 SP:F9 PPU: 7, 49 CYC:5579 $D406:E8 INX A:00 X:9F Y:80 P:A5 SP:FB PPU: 25, 49 CYC:5585 $D407:A9 80 LDA #$80 A:00 X:A0 Y:80 P:A5 SP:FB PPU: 31, 49 CYC:5587 $D409:85 78 STA $78 = #$00 A:80 X:A0 Y:80 P:A5 SP:FB PPU: 37, 49 CYC:5589 $D40B:C4 78 CPY $78 = #$80 A:80 X:A0 Y:80 P:A5 SP:FB PPU: 46, 49 CYC:5592 $D40D:20 13 F9 JSR $F913 A:80 X:A0 Y:80 P:nvUbdIZC SP:FB PPU: 55, 49 CYC:5595 $F913:D0 05 BNE $F91A A:80 X:A0 Y:80 P:nvUbdIZC SP:F9 PPU: 73, 49 CYC:5601 $F915:30 03 BMI $F91A A:80 X:A0 Y:80 P:nvUbdIZC SP:F9 PPU: 79, 49 CYC:5603 $F917:90 01 BCC $F91A A:80 X:A0 Y:80 P:nvUbdIZC SP:F9 PPU: 85, 49 CYC:5605 $F919:60 RTS A:80 X:A0 Y:80 P:nvUbdIZC SP:F9 PPU: 91, 49 CYC:5607 $D410:E8 INX A:80 X:A0 Y:80 P:nvUbdIZC SP:FB PPU:109, 49 CYC:5613 $D411:A9 81 LDA #$81 A:80 X:A1 Y:80 P:A5 SP:FB PPU:115, 49 CYC:5615 $D413:85 78 STA $78 = #$80 A:81 X:A1 Y:80 P:A5 SP:FB PPU:121, 49 CYC:5617 $D415:C4 78 CPY $78 = #$81 A:81 X:A1 Y:80 P:A5 SP:FB PPU:130, 49 CYC:5620 $D417:20 1D F9 JSR $F91D A:81 X:A1 Y:80 P:NvUbdIzc SP:FB PPU:139, 49 CYC:5623 $F91D:B0 05 BCS $F924 A:81 X:A1 Y:80 P:NvUbdIzc SP:F9 PPU:157, 49 CYC:5629 $F91F:F0 03 BEQ $F924 A:81 X:A1 Y:80 P:NvUbdIzc SP:F9 PPU:163, 49 CYC:5631 $F921:10 01 BPL $F924 A:81 X:A1 Y:80 P:NvUbdIzc SP:F9 PPU:169, 49 CYC:5633 $F923:60 RTS A:81 X:A1 Y:80 P:NvUbdIzc SP:F9 PPU:175, 49 CYC:5635 $D41A:E8 INX A:81 X:A1 Y:80 P:NvUbdIzc SP:FB PPU:193, 49 CYC:5641 $D41B:A9 7F LDA #$7F A:81 X:A2 Y:80 P:NvUbdIzc SP:FB PPU:199, 49 CYC:5643 $D41D:85 78 STA $78 = #$81 A:7F X:A2 Y:80 P:nvUbdIzc SP:FB PPU:205, 49 CYC:5645 $D41F:C4 78 CPY $78 = #$7F A:7F X:A2 Y:80 P:nvUbdIzc SP:FB PPU:214, 49 CYC:5648 $D421:20 27 F9 JSR $F927 A:7F X:A2 Y:80 P:25 SP:FB PPU:223, 49 CYC:5651 $F927:90 05 BCC $F92E A:7F X:A2 Y:80 P:25 SP:F9 PPU:241, 49 CYC:5657 $F929:F0 03 BEQ $F92E A:7F X:A2 Y:80 P:25 SP:F9 PPU:247, 49 CYC:5659 $F92B:30 01 BMI $F92E A:7F X:A2 Y:80 P:25 SP:F9 PPU:253, 49 CYC:5661 $F92D:60 RTS A:7F X:A2 Y:80 P:25 SP:F9 PPU:259, 49 CYC:5663 $D424:E8 INX A:7F X:A2 Y:80 P:25 SP:FB PPU:277, 49 CYC:5669 $D425:8A TXA A:7F X:A3 Y:80 P:A5 SP:FB PPU:283, 49 CYC:5671 $D426:A8 TAY A:A3 X:A3 Y:80 P:A5 SP:FB PPU:289, 49 CYC:5673 $D427:20 90 F9 JSR $F990 A:A3 X:A3 Y:A3 P:A5 SP:FB PPU:295, 49 CYC:5675 $F990:A2 55 LDX #$55 A:A3 X:A3 Y:A3 P:A5 SP:F9 PPU:313, 49 CYC:5681 $F992:A9 FF LDA #$FF A:A3 X:55 Y:A3 P:25 SP:F9 PPU:319, 49 CYC:5683 $F994:85 01 STA $01 = #$FF A:FF X:55 Y:A3 P:A5 SP:F9 PPU:325, 49 CYC:5685 $F996:EA NOP A:FF X:55 Y:A3 P:A5 SP:F9 PPU:334, 49 CYC:5688 $F997:24 01 BIT $01 = #$FF A:FF X:55 Y:A3 P:A5 SP:F9 PPU:340, 49 CYC:5690 $F999:38 SEC A:FF X:55 Y:A3 P:E5 SP:F9 PPU: 8, 50 CYC:5693 $F99A:A9 01 LDA #$01 A:FF X:55 Y:A3 P:E5 SP:F9 PPU: 14, 50 CYC:5695 $F99C:60 RTS A:01 X:55 Y:A3 P:65 SP:F9 PPU: 20, 50 CYC:5697 $D42A:85 78 STA $78 = #$7F A:01 X:55 Y:A3 P:65 SP:FB PPU: 38, 50 CYC:5703 $D42C:46 78 LSR $78 = #$01 A:01 X:55 Y:A3 P:65 SP:FB PPU: 47, 50 CYC:5706 $D42E:A5 78 LDA $78 = #$00 A:01 X:55 Y:A3 P:67 SP:FB PPU: 62, 50 CYC:5711 $D430:20 9D F9 JSR $F99D A:00 X:55 Y:A3 P:67 SP:FB PPU: 71, 50 CYC:5714 $F99D:90 1B BCC $F9BA A:00 X:55 Y:A3 P:67 SP:F9 PPU: 89, 50 CYC:5720 $F99F:D0 19 BNE $F9BA A:00 X:55 Y:A3 P:67 SP:F9 PPU: 95, 50 CYC:5722 $F9A1:30 17 BMI $F9BA A:00 X:55 Y:A3 P:67 SP:F9 PPU:101, 50 CYC:5724 $F9A3:50 15 BVC $F9BA A:00 X:55 Y:A3 P:67 SP:F9 PPU:107, 50 CYC:5726 $F9A5:C9 00 CMP #$00 A:00 X:55 Y:A3 P:67 SP:F9 PPU:113, 50 CYC:5728 $F9A7:D0 11 BNE $F9BA A:00 X:55 Y:A3 P:67 SP:F9 PPU:119, 50 CYC:5730 $F9A9:B8 CLV A:00 X:55 Y:A3 P:67 SP:F9 PPU:125, 50 CYC:5732 $F9AA:A9 AA LDA #$AA A:00 X:55 Y:A3 P:nvUbdIZC SP:F9 PPU:131, 50 CYC:5734 $F9AC:60 RTS A:AA X:55 Y:A3 P:A5 SP:F9 PPU:137, 50 CYC:5736 $D433:C8 INY A:AA X:55 Y:A3 P:A5 SP:FB PPU:155, 50 CYC:5742 $D434:85 78 STA $78 = #$00 A:AA X:55 Y:A4 P:A5 SP:FB PPU:161, 50 CYC:5744 $D436:46 78 LSR $78 = #$AA A:AA X:55 Y:A4 P:A5 SP:FB PPU:170, 50 CYC:5747 $D438:A5 78 LDA $78 = #$55 A:AA X:55 Y:A4 P:nvUbdIzc SP:FB PPU:185, 50 CYC:5752 $D43A:20 AD F9 JSR $F9AD A:55 X:55 Y:A4 P:nvUbdIzc SP:FB PPU:194, 50 CYC:5755 $F9AD:B0 0B BCS $F9BA A:55 X:55 Y:A4 P:nvUbdIzc SP:F9 PPU:212, 50 CYC:5761 $F9AF:F0 09 BEQ $F9BA A:55 X:55 Y:A4 P:nvUbdIzc SP:F9 PPU:218, 50 CYC:5763 $F9B1:30 07 BMI $F9BA A:55 X:55 Y:A4 P:nvUbdIzc SP:F9 PPU:224, 50 CYC:5765 $F9B3:70 05 BVS $F9BA A:55 X:55 Y:A4 P:nvUbdIzc SP:F9 PPU:230, 50 CYC:5767 $F9B5:C9 55 CMP #$55 A:55 X:55 Y:A4 P:nvUbdIzc SP:F9 PPU:236, 50 CYC:5769 $F9B7:D0 01 BNE $F9BA A:55 X:55 Y:A4 P:nvUbdIZC SP:F9 PPU:242, 50 CYC:5771 $F9B9:60 RTS A:55 X:55 Y:A4 P:nvUbdIZC SP:F9 PPU:248, 50 CYC:5773 $D43D:C8 INY A:55 X:55 Y:A4 P:nvUbdIZC SP:FB PPU:266, 50 CYC:5779 $D43E:20 BD F9 JSR $F9BD A:55 X:55 Y:A5 P:A5 SP:FB PPU:272, 50 CYC:5781 $F9BD:24 01 BIT $01 = #$FF A:55 X:55 Y:A5 P:A5 SP:F9 PPU:290, 50 CYC:5787 $F9BF:38 SEC A:55 X:55 Y:A5 P:E5 SP:F9 PPU:299, 50 CYC:5790 $F9C0:A9 80 LDA #$80 A:55 X:55 Y:A5 P:E5 SP:F9 PPU:305, 50 CYC:5792 $F9C2:60 RTS A:80 X:55 Y:A5 P:E5 SP:F9 PPU:311, 50 CYC:5794 $D441:85 78 STA $78 = #$55 A:80 X:55 Y:A5 P:E5 SP:FB PPU:329, 50 CYC:5800 $D443:06 78 ASL $78 = #$80 A:80 X:55 Y:A5 P:E5 SP:FB PPU:338, 50 CYC:5803 $D445:A5 78 LDA $78 = #$00 A:80 X:55 Y:A5 P:67 SP:FB PPU: 12, 51 CYC:5808 $D447:20 C3 F9 JSR $F9C3 A:00 X:55 Y:A5 P:67 SP:FB PPU: 21, 51 CYC:5811 $F9C3:90 1C BCC $F9E1 A:00 X:55 Y:A5 P:67 SP:F9 PPU: 39, 51 CYC:5817 $F9C5:D0 1A BNE $F9E1 A:00 X:55 Y:A5 P:67 SP:F9 PPU: 45, 51 CYC:5819 $F9C7:30 18 BMI $F9E1 A:00 X:55 Y:A5 P:67 SP:F9 PPU: 51, 51 CYC:5821 $F9C9:50 16 BVC $F9E1 A:00 X:55 Y:A5 P:67 SP:F9 PPU: 57, 51 CYC:5823 $F9CB:C9 00 CMP #$00 A:00 X:55 Y:A5 P:67 SP:F9 PPU: 63, 51 CYC:5825 $F9CD:D0 12 BNE $F9E1 A:00 X:55 Y:A5 P:67 SP:F9 PPU: 69, 51 CYC:5827 $F9CF:B8 CLV A:00 X:55 Y:A5 P:67 SP:F9 PPU: 75, 51 CYC:5829 $F9D0:A9 55 LDA #$55 A:00 X:55 Y:A5 P:nvUbdIZC SP:F9 PPU: 81, 51 CYC:5831 $F9D2:38 SEC A:55 X:55 Y:A5 P:25 SP:F9 PPU: 87, 51 CYC:5833 $F9D3:60 RTS A:55 X:55 Y:A5 P:25 SP:F9 PPU: 93, 51 CYC:5835 $D44A:C8 INY A:55 X:55 Y:A5 P:25 SP:FB PPU:111, 51 CYC:5841 $D44B:85 78 STA $78 = #$00 A:55 X:55 Y:A6 P:A5 SP:FB PPU:117, 51 CYC:5843 $D44D:06 78 ASL $78 = #$55 A:55 X:55 Y:A6 P:A5 SP:FB PPU:126, 51 CYC:5846 $D44F:A5 78 LDA $78 = #$AA A:55 X:55 Y:A6 P:NvUbdIzc SP:FB PPU:141, 51 CYC:5851 $D451:20 D4 F9 JSR $F9D4 A:AA X:55 Y:A6 P:NvUbdIzc SP:FB PPU:150, 51 CYC:5854 $F9D4:B0 0B BCS $F9E1 A:AA X:55 Y:A6 P:NvUbdIzc SP:F9 PPU:168, 51 CYC:5860 $F9D6:F0 09 BEQ $F9E1 A:AA X:55 Y:A6 P:NvUbdIzc SP:F9 PPU:174, 51 CYC:5862 $F9D8:10 07 BPL $F9E1 A:AA X:55 Y:A6 P:NvUbdIzc SP:F9 PPU:180, 51 CYC:5864 $F9DA:70 05 BVS $F9E1 A:AA X:55 Y:A6 P:NvUbdIzc SP:F9 PPU:186, 51 CYC:5866 $F9DC:C9 AA CMP #$AA A:AA X:55 Y:A6 P:NvUbdIzc SP:F9 PPU:192, 51 CYC:5868 $F9DE:D0 01 BNE $F9E1 A:AA X:55 Y:A6 P:nvUbdIZC SP:F9 PPU:198, 51 CYC:5870 $F9E0:60 RTS A:AA X:55 Y:A6 P:nvUbdIZC SP:F9 PPU:204, 51 CYC:5872 $D454:C8 INY A:AA X:55 Y:A6 P:nvUbdIZC SP:FB PPU:222, 51 CYC:5878 $D455:20 E4 F9 JSR $F9E4 A:AA X:55 Y:A7 P:A5 SP:FB PPU:228, 51 CYC:5880 $F9E4:24 01 BIT $01 = #$FF A:AA X:55 Y:A7 P:A5 SP:F9 PPU:246, 51 CYC:5886 $F9E6:38 SEC A:AA X:55 Y:A7 P:E5 SP:F9 PPU:255, 51 CYC:5889 $F9E7:A9 01 LDA #$01 A:AA X:55 Y:A7 P:E5 SP:F9 PPU:261, 51 CYC:5891 $F9E9:60 RTS A:01 X:55 Y:A7 P:65 SP:F9 PPU:267, 51 CYC:5893 $D458:85 78 STA $78 = #$AA A:01 X:55 Y:A7 P:65 SP:FB PPU:285, 51 CYC:5899 $D45A:66 78 ROR $78 = #$01 A:01 X:55 Y:A7 P:65 SP:FB PPU:294, 51 CYC:5902 $D45C:A5 78 LDA $78 = #$80 A:01 X:55 Y:A7 P:E5 SP:FB PPU:309, 51 CYC:5907 $D45E:20 EA F9 JSR $F9EA A:80 X:55 Y:A7 P:E5 SP:FB PPU:318, 51 CYC:5910 $F9EA:90 1C BCC $FA08 A:80 X:55 Y:A7 P:E5 SP:F9 PPU:336, 51 CYC:5916 $F9EC:F0 1A BEQ $FA08 A:80 X:55 Y:A7 P:E5 SP:F9 PPU: 1, 52 CYC:5918 $F9EE:10 18 BPL $FA08 A:80 X:55 Y:A7 P:E5 SP:F9 PPU: 7, 52 CYC:5920 $F9F0:50 16 BVC $FA08 A:80 X:55 Y:A7 P:E5 SP:F9 PPU: 13, 52 CYC:5922 $F9F2:C9 80 CMP #$80 A:80 X:55 Y:A7 P:E5 SP:F9 PPU: 19, 52 CYC:5924 $F9F4:D0 12 BNE $FA08 A:80 X:55 Y:A7 P:67 SP:F9 PPU: 25, 52 CYC:5926 $F9F6:B8 CLV A:80 X:55 Y:A7 P:67 SP:F9 PPU: 31, 52 CYC:5928 $F9F7:18 CLC A:80 X:55 Y:A7 P:nvUbdIZC SP:F9 PPU: 37, 52 CYC:5930 $F9F8:A9 55 LDA #$55 A:80 X:55 Y:A7 P:nvUbdIZc SP:F9 PPU: 43, 52 CYC:5932 $F9FA:60 RTS A:55 X:55 Y:A7 P:nvUbdIzc SP:F9 PPU: 49, 52 CYC:5934 $D461:C8 INY A:55 X:55 Y:A7 P:nvUbdIzc SP:FB PPU: 67, 52 CYC:5940 $D462:85 78 STA $78 = #$80 A:55 X:55 Y:A8 P:NvUbdIzc SP:FB PPU: 73, 52 CYC:5942 $D464:66 78 ROR $78 = #$55 A:55 X:55 Y:A8 P:NvUbdIzc SP:FB PPU: 82, 52 CYC:5945 $D466:A5 78 LDA $78 = #$2A A:55 X:55 Y:A8 P:25 SP:FB PPU: 97, 52 CYC:5950 $D468:20 FB F9 JSR $F9FB A:2A X:55 Y:A8 P:25 SP:FB PPU:106, 52 CYC:5953 $F9FB:90 0B BCC $FA08 A:2A X:55 Y:A8 P:25 SP:F9 PPU:124, 52 CYC:5959 $F9FD:F0 09 BEQ $FA08 A:2A X:55 Y:A8 P:25 SP:F9 PPU:130, 52 CYC:5961 $F9FF:30 07 BMI $FA08 A:2A X:55 Y:A8 P:25 SP:F9 PPU:136, 52 CYC:5963 $FA01:70 05 BVS $FA08 A:2A X:55 Y:A8 P:25 SP:F9 PPU:142, 52 CYC:5965 $FA03:C9 2A CMP #$2A A:2A X:55 Y:A8 P:25 SP:F9 PPU:148, 52 CYC:5967 $FA05:D0 01 BNE $FA08 A:2A X:55 Y:A8 P:nvUbdIZC SP:F9 PPU:154, 52 CYC:5969 $FA07:60 RTS A:2A X:55 Y:A8 P:nvUbdIZC SP:F9 PPU:160, 52 CYC:5971 $D46B:C8 INY A:2A X:55 Y:A8 P:nvUbdIZC SP:FB PPU:178, 52 CYC:5977 $D46C:20 0A FA JSR $FA0A A:2A X:55 Y:A9 P:A5 SP:FB PPU:184, 52 CYC:5979 $FA0A:24 01 BIT $01 = #$FF A:2A X:55 Y:A9 P:A5 SP:F9 PPU:202, 52 CYC:5985 $FA0C:38 SEC A:2A X:55 Y:A9 P:E5 SP:F9 PPU:211, 52 CYC:5988 $FA0D:A9 80 LDA #$80 A:2A X:55 Y:A9 P:E5 SP:F9 PPU:217, 52 CYC:5990 $FA0F:60 RTS A:80 X:55 Y:A9 P:E5 SP:F9 PPU:223, 52 CYC:5992 $D46F:85 78 STA $78 = #$2A A:80 X:55 Y:A9 P:E5 SP:FB PPU:241, 52 CYC:5998 $D471:26 78 ROL $78 = #$80 A:80 X:55 Y:A9 P:E5 SP:FB PPU:250, 52 CYC:6001 $D473:A5 78 LDA $78 = #$01 A:80 X:55 Y:A9 P:65 SP:FB PPU:265, 52 CYC:6006 $D475:20 10 FA JSR $FA10 A:01 X:55 Y:A9 P:65 SP:FB PPU:274, 52 CYC:6009 $FA10:90 1C BCC $FA2E A:01 X:55 Y:A9 P:65 SP:F9 PPU:292, 52 CYC:6015 $FA12:F0 1A BEQ $FA2E A:01 X:55 Y:A9 P:65 SP:F9 PPU:298, 52 CYC:6017 $FA14:30 18 BMI $FA2E A:01 X:55 Y:A9 P:65 SP:F9 PPU:304, 52 CYC:6019 $FA16:50 16 BVC $FA2E A:01 X:55 Y:A9 P:65 SP:F9 PPU:310, 52 CYC:6021 $FA18:C9 01 CMP #$01 A:01 X:55 Y:A9 P:65 SP:F9 PPU:316, 52 CYC:6023 $FA1A:D0 12 BNE $FA2E A:01 X:55 Y:A9 P:67 SP:F9 PPU:322, 52 CYC:6025 $FA1C:B8 CLV A:01 X:55 Y:A9 P:67 SP:F9 PPU:328, 52 CYC:6027 $FA1D:18 CLC A:01 X:55 Y:A9 P:nvUbdIZC SP:F9 PPU:334, 52 CYC:6029 $FA1E:A9 55 LDA #$55 A:01 X:55 Y:A9 P:nvUbdIZc SP:F9 PPU:340, 52 CYC:6031 $FA20:60 RTS A:55 X:55 Y:A9 P:nvUbdIzc SP:F9 PPU: 5, 53 CYC:6033 $D478:C8 INY A:55 X:55 Y:A9 P:nvUbdIzc SP:FB PPU: 23, 53 CYC:6039 $D479:85 78 STA $78 = #$01 A:55 X:55 Y:AA P:NvUbdIzc SP:FB PPU: 29, 53 CYC:6041 $D47B:26 78 ROL $78 = #$55 A:55 X:55 Y:AA P:NvUbdIzc SP:FB PPU: 38, 53 CYC:6044 $D47D:A5 78 LDA $78 = #$AA A:55 X:55 Y:AA P:NvUbdIzc SP:FB PPU: 53, 53 CYC:6049 $D47F:20 21 FA JSR $FA21 A:AA X:55 Y:AA P:NvUbdIzc SP:FB PPU: 62, 53 CYC:6052 $FA21:B0 0B BCS $FA2E A:AA X:55 Y:AA P:NvUbdIzc SP:F9 PPU: 80, 53 CYC:6058 $FA23:F0 09 BEQ $FA2E A:AA X:55 Y:AA P:NvUbdIzc SP:F9 PPU: 86, 53 CYC:6060 $FA25:10 07 BPL $FA2E A:AA X:55 Y:AA P:NvUbdIzc SP:F9 PPU: 92, 53 CYC:6062 $FA27:70 05 BVS $FA2E A:AA X:55 Y:AA P:NvUbdIzc SP:F9 PPU: 98, 53 CYC:6064 $FA29:C9 AA CMP #$AA A:AA X:55 Y:AA P:NvUbdIzc SP:F9 PPU:104, 53 CYC:6066 $FA2B:D0 01 BNE $FA2E A:AA X:55 Y:AA P:nvUbdIZC SP:F9 PPU:110, 53 CYC:6068 $FA2D:60 RTS A:AA X:55 Y:AA P:nvUbdIZC SP:F9 PPU:116, 53 CYC:6070 $D482:A9 FF LDA #$FF A:AA X:55 Y:AA P:nvUbdIZC SP:FB PPU:134, 53 CYC:6076 $D484:85 78 STA $78 = #$AA A:FF X:55 Y:AA P:A5 SP:FB PPU:140, 53 CYC:6078 $D486:85 01 STA $01 = #$FF A:FF X:55 Y:AA P:A5 SP:FB PPU:149, 53 CYC:6081 $D488:24 01 BIT $01 = #$FF A:FF X:55 Y:AA P:A5 SP:FB PPU:158, 53 CYC:6084 $D48A:38 SEC A:FF X:55 Y:AA P:E5 SP:FB PPU:167, 53 CYC:6087 $D48B:E6 78 INC $78 = #$FF A:FF X:55 Y:AA P:E5 SP:FB PPU:173, 53 CYC:6089 $D48D:D0 0C BNE $D49B A:FF X:55 Y:AA P:67 SP:FB PPU:188, 53 CYC:6094 $D48F:30 0A BMI $D49B A:FF X:55 Y:AA P:67 SP:FB PPU:194, 53 CYC:6096 $D491:50 08 BVC $D49B A:FF X:55 Y:AA P:67 SP:FB PPU:200, 53 CYC:6098 $D493:90 06 BCC $D49B A:FF X:55 Y:AA P:67 SP:FB PPU:206, 53 CYC:6100 $D495:A5 78 LDA $78 = #$00 A:FF X:55 Y:AA P:67 SP:FB PPU:212, 53 CYC:6102 $D497:C9 00 CMP #$00 A:00 X:55 Y:AA P:67 SP:FB PPU:221, 53 CYC:6105 $D499:F0 04 BEQ $D49F A:00 X:55 Y:AA P:67 SP:FB PPU:227, 53 CYC:6107 $D49F:A9 7F LDA #$7F A:00 X:55 Y:AA P:67 SP:FB PPU:236, 53 CYC:6110 $D4A1:85 78 STA $78 = #$00 A:7F X:55 Y:AA P:65 SP:FB PPU:242, 53 CYC:6112 $D4A3:B8 CLV A:7F X:55 Y:AA P:65 SP:FB PPU:251, 53 CYC:6115 $D4A4:18 CLC A:7F X:55 Y:AA P:25 SP:FB PPU:257, 53 CYC:6117 $D4A5:E6 78 INC $78 = #$7F A:7F X:55 Y:AA P:nvUbdIzc SP:FB PPU:263, 53 CYC:6119 $D4A7:F0 0C BEQ $D4B5 A:7F X:55 Y:AA P:NvUbdIzc SP:FB PPU:278, 53 CYC:6124 $D4A9:10 0A BPL $D4B5 A:7F X:55 Y:AA P:NvUbdIzc SP:FB PPU:284, 53 CYC:6126 $D4AB:70 08 BVS $D4B5 A:7F X:55 Y:AA P:NvUbdIzc SP:FB PPU:290, 53 CYC:6128 $D4AD:B0 06 BCS $D4B5 A:7F X:55 Y:AA P:NvUbdIzc SP:FB PPU:296, 53 CYC:6130 $D4AF:A5 78 LDA $78 = #$80 A:7F X:55 Y:AA P:NvUbdIzc SP:FB PPU:302, 53 CYC:6132 $D4B1:C9 80 CMP #$80 A:80 X:55 Y:AA P:NvUbdIzc SP:FB PPU:311, 53 CYC:6135 $D4B3:F0 04 BEQ $D4B9 A:80 X:55 Y:AA P:nvUbdIZC SP:FB PPU:317, 53 CYC:6137 $D4B9:A9 00 LDA #$00 A:80 X:55 Y:AA P:nvUbdIZC SP:FB PPU:326, 53 CYC:6140 $D4BB:85 78 STA $78 = #$80 A:00 X:55 Y:AA P:nvUbdIZC SP:FB PPU:332, 53 CYC:6142 $D4BD:24 01 BIT $01 = #$FF A:00 X:55 Y:AA P:nvUbdIZC SP:FB PPU: 0, 54 CYC:6145 $D4BF:38 SEC A:00 X:55 Y:AA P:E7 SP:FB PPU: 9, 54 CYC:6148 $D4C0:C6 78 DEC $78 = #$00 A:00 X:55 Y:AA P:E7 SP:FB PPU: 15, 54 CYC:6150 $D4C2:F0 0C BEQ $D4D0 A:00 X:55 Y:AA P:E5 SP:FB PPU: 30, 54 CYC:6155 $D4C4:10 0A BPL $D4D0 A:00 X:55 Y:AA P:E5 SP:FB PPU: 36, 54 CYC:6157 $D4C6:50 08 BVC $D4D0 A:00 X:55 Y:AA P:E5 SP:FB PPU: 42, 54 CYC:6159 $D4C8:90 06 BCC $D4D0 A:00 X:55 Y:AA P:E5 SP:FB PPU: 48, 54 CYC:6161 $D4CA:A5 78 LDA $78 = #$FF A:00 X:55 Y:AA P:E5 SP:FB PPU: 54, 54 CYC:6163 $D4CC:C9 FF CMP #$FF A:FF X:55 Y:AA P:E5 SP:FB PPU: 63, 54 CYC:6166 $D4CE:F0 04 BEQ $D4D4 A:FF X:55 Y:AA P:67 SP:FB PPU: 69, 54 CYC:6168 $D4D4:A9 80 LDA #$80 A:FF X:55 Y:AA P:67 SP:FB PPU: 78, 54 CYC:6171 $D4D6:85 78 STA $78 = #$FF A:80 X:55 Y:AA P:E5 SP:FB PPU: 84, 54 CYC:6173 $D4D8:B8 CLV A:80 X:55 Y:AA P:E5 SP:FB PPU: 93, 54 CYC:6176 $D4D9:18 CLC A:80 X:55 Y:AA P:A5 SP:FB PPU: 99, 54 CYC:6178 $D4DA:C6 78 DEC $78 = #$80 A:80 X:55 Y:AA P:NvUbdIzc SP:FB PPU:105, 54 CYC:6180 $D4DC:F0 0C BEQ $D4EA A:80 X:55 Y:AA P:nvUbdIzc SP:FB PPU:120, 54 CYC:6185 $D4DE:30 0A BMI $D4EA A:80 X:55 Y:AA P:nvUbdIzc SP:FB PPU:126, 54 CYC:6187 $D4E0:70 08 BVS $D4EA A:80 X:55 Y:AA P:nvUbdIzc SP:FB PPU:132, 54 CYC:6189 $D4E2:B0 06 BCS $D4EA A:80 X:55 Y:AA P:nvUbdIzc SP:FB PPU:138, 54 CYC:6191 $D4E4:A5 78 LDA $78 = #$7F A:80 X:55 Y:AA P:nvUbdIzc SP:FB PPU:144, 54 CYC:6193 $D4E6:C9 7F CMP #$7F A:7F X:55 Y:AA P:nvUbdIzc SP:FB PPU:153, 54 CYC:6196 $D4E8:F0 04 BEQ $D4EE A:7F X:55 Y:AA P:nvUbdIZC SP:FB PPU:159, 54 CYC:6198 $D4EE:A9 01 LDA #$01 A:7F X:55 Y:AA P:nvUbdIZC SP:FB PPU:168, 54 CYC:6201 $D4F0:85 78 STA $78 = #$7F A:01 X:55 Y:AA P:25 SP:FB PPU:174, 54 CYC:6203 $D4F2:C6 78 DEC $78 = #$01 A:01 X:55 Y:AA P:25 SP:FB PPU:183, 54 CYC:6206 $D4F4:F0 04 BEQ $D4FA A:01 X:55 Y:AA P:nvUbdIZC SP:FB PPU:198, 54 CYC:6211 $D4FA:60 RTS A:01 X:55 Y:AA P:nvUbdIZC SP:FB PPU:207, 54 CYC:6214 $C615:20 FB D4 JSR $D4FB A:01 X:55 Y:AA P:nvUbdIZC SP:FD PPU:225, 54 CYC:6220 $D4FB:A9 55 LDA #$55 A:01 X:55 Y:AA P:nvUbdIZC SP:FB PPU:243, 54 CYC:6226 $D4FD:8D 78 06 STA $0678 = #$00 A:55 X:55 Y:AA P:25 SP:FB PPU:249, 54 CYC:6228 $D500:A9 FF LDA #$FF A:55 X:55 Y:AA P:25 SP:FB PPU:261, 54 CYC:6232 $D502:85 01 STA $01 = #$FF A:FF X:55 Y:AA P:A5 SP:FB PPU:267, 54 CYC:6234 $D504:24 01 BIT $01 = #$FF A:FF X:55 Y:AA P:A5 SP:FB PPU:276, 54 CYC:6237 $D506:A0 11 LDY #$11 A:FF X:55 Y:AA P:E5 SP:FB PPU:285, 54 CYC:6240 $D508:A2 23 LDX #$23 A:FF X:55 Y:11 P:65 SP:FB PPU:291, 54 CYC:6242 $D50A:A9 00 LDA #$00 A:FF X:23 Y:11 P:65 SP:FB PPU:297, 54 CYC:6244 $D50C:AD 78 06 LDA $0678 = #$55 A:00 X:23 Y:11 P:67 SP:FB PPU:303, 54 CYC:6246 $D50F:F0 10 BEQ $D521 A:55 X:23 Y:11 P:65 SP:FB PPU:315, 54 CYC:6250 $D511:30 0E BMI $D521 A:55 X:23 Y:11 P:65 SP:FB PPU:321, 54 CYC:6252 $D513:C9 55 CMP #$55 A:55 X:23 Y:11 P:65 SP:FB PPU:327, 54 CYC:6254 $D515:D0 0A BNE $D521 A:55 X:23 Y:11 P:67 SP:FB PPU:333, 54 CYC:6256 $D517:C0 11 CPY #$11 A:55 X:23 Y:11 P:67 SP:FB PPU:339, 54 CYC:6258 $D519:D0 06 BNE $D521 A:55 X:23 Y:11 P:67 SP:FB PPU: 4, 55 CYC:6260 $D51B:E0 23 CPX #$23 A:55 X:23 Y:11 P:67 SP:FB PPU: 10, 55 CYC:6262 $D51D:50 02 BVC $D521 A:55 X:23 Y:11 P:67 SP:FB PPU: 16, 55 CYC:6264 $D51F:F0 04 BEQ $D525 A:55 X:23 Y:11 P:67 SP:FB PPU: 22, 55 CYC:6266 $D525:A9 46 LDA #$46 A:55 X:23 Y:11 P:67 SP:FB PPU: 31, 55 CYC:6269 $D527:24 01 BIT $01 = #$FF A:46 X:23 Y:11 P:65 SP:FB PPU: 37, 55 CYC:6271 $D529:8D 78 06 STA $0678 = #$55 A:46 X:23 Y:11 P:E5 SP:FB PPU: 46, 55 CYC:6274 $D52C:F0 0B BEQ $D539 A:46 X:23 Y:11 P:E5 SP:FB PPU: 58, 55 CYC:6278 $D52E:10 09 BPL $D539 A:46 X:23 Y:11 P:E5 SP:FB PPU: 64, 55 CYC:6280 $D530:50 07 BVC $D539 A:46 X:23 Y:11 P:E5 SP:FB PPU: 70, 55 CYC:6282 $D532:AD 78 06 LDA $0678 = #$46 A:46 X:23 Y:11 P:E5 SP:FB PPU: 76, 55 CYC:6284 $D535:C9 46 CMP #$46 A:46 X:23 Y:11 P:65 SP:FB PPU: 88, 55 CYC:6288 $D537:F0 04 BEQ $D53D A:46 X:23 Y:11 P:67 SP:FB PPU: 94, 55 CYC:6290 $D53D:A9 55 LDA #$55 A:46 X:23 Y:11 P:67 SP:FB PPU:103, 55 CYC:6293 $D53F:8D 78 06 STA $0678 = #$46 A:55 X:23 Y:11 P:65 SP:FB PPU:109, 55 CYC:6295 $D542:24 01 BIT $01 = #$FF A:55 X:23 Y:11 P:65 SP:FB PPU:121, 55 CYC:6299 $D544:A9 11 LDA #$11 A:55 X:23 Y:11 P:E5 SP:FB PPU:130, 55 CYC:6302 $D546:A2 23 LDX #$23 A:11 X:23 Y:11 P:65 SP:FB PPU:136, 55 CYC:6304 $D548:A0 00 LDY #$00 A:11 X:23 Y:11 P:65 SP:FB PPU:142, 55 CYC:6306 $D54A:AC 78 06 LDY $0678 = #$55 A:11 X:23 Y:00 P:67 SP:FB PPU:148, 55 CYC:6308 $D54D:F0 10 BEQ $D55F A:11 X:23 Y:55 P:65 SP:FB PPU:160, 55 CYC:6312 $D54F:30 0E BMI $D55F A:11 X:23 Y:55 P:65 SP:FB PPU:166, 55 CYC:6314 $D551:C0 55 CPY #$55 A:11 X:23 Y:55 P:65 SP:FB PPU:172, 55 CYC:6316 $D553:D0 0A BNE $D55F A:11 X:23 Y:55 P:67 SP:FB PPU:178, 55 CYC:6318 $D555:C9 11 CMP #$11 A:11 X:23 Y:55 P:67 SP:FB PPU:184, 55 CYC:6320 $D557:D0 06 BNE $D55F A:11 X:23 Y:55 P:67 SP:FB PPU:190, 55 CYC:6322 $D559:E0 23 CPX #$23 A:11 X:23 Y:55 P:67 SP:FB PPU:196, 55 CYC:6324 $D55B:50 02 BVC $D55F A:11 X:23 Y:55 P:67 SP:FB PPU:202, 55 CYC:6326 $D55D:F0 04 BEQ $D563 A:11 X:23 Y:55 P:67 SP:FB PPU:208, 55 CYC:6328 $D563:A0 46 LDY #$46 A:11 X:23 Y:55 P:67 SP:FB PPU:217, 55 CYC:6331 $D565:24 01 BIT $01 = #$FF A:11 X:23 Y:46 P:65 SP:FB PPU:223, 55 CYC:6333 $D567:8C 78 06 STY $0678 = #$55 A:11 X:23 Y:46 P:E5 SP:FB PPU:232, 55 CYC:6336 $D56A:F0 0B BEQ $D577 A:11 X:23 Y:46 P:E5 SP:FB PPU:244, 55 CYC:6340 $D56C:10 09 BPL $D577 A:11 X:23 Y:46 P:E5 SP:FB PPU:250, 55 CYC:6342 $D56E:50 07 BVC $D577 A:11 X:23 Y:46 P:E5 SP:FB PPU:256, 55 CYC:6344 $D570:AC 78 06 LDY $0678 = #$46 A:11 X:23 Y:46 P:E5 SP:FB PPU:262, 55 CYC:6346 $D573:C0 46 CPY #$46 A:11 X:23 Y:46 P:65 SP:FB PPU:274, 55 CYC:6350 $D575:F0 04 BEQ $D57B A:11 X:23 Y:46 P:67 SP:FB PPU:280, 55 CYC:6352 $D57B:24 01 BIT $01 = #$FF A:11 X:23 Y:46 P:67 SP:FB PPU:289, 55 CYC:6355 $D57D:A9 55 LDA #$55 A:11 X:23 Y:46 P:E5 SP:FB PPU:298, 55 CYC:6358 $D57F:8D 78 06 STA $0678 = #$46 A:55 X:23 Y:46 P:65 SP:FB PPU:304, 55 CYC:6360 $D582:A0 11 LDY #$11 A:55 X:23 Y:46 P:65 SP:FB PPU:316, 55 CYC:6364 $D584:A9 23 LDA #$23 A:55 X:23 Y:11 P:65 SP:FB PPU:322, 55 CYC:6366 $D586:A2 00 LDX #$00 A:23 X:23 Y:11 P:65 SP:FB PPU:328, 55 CYC:6368 $D588:AE 78 06 LDX $0678 = #$55 A:23 X:00 Y:11 P:67 SP:FB PPU:334, 55 CYC:6370 $D58B:F0 10 BEQ $D59D A:23 X:55 Y:11 P:65 SP:FB PPU: 5, 56 CYC:6374 $D58D:30 0E BMI $D59D A:23 X:55 Y:11 P:65 SP:FB PPU: 11, 56 CYC:6376 $D58F:E0 55 CPX #$55 A:23 X:55 Y:11 P:65 SP:FB PPU: 17, 56 CYC:6378 $D591:D0 0A BNE $D59D A:23 X:55 Y:11 P:67 SP:FB PPU: 23, 56 CYC:6380 $D593:C0 11 CPY #$11 A:23 X:55 Y:11 P:67 SP:FB PPU: 29, 56 CYC:6382 $D595:D0 06 BNE $D59D A:23 X:55 Y:11 P:67 SP:FB PPU: 35, 56 CYC:6384 $D597:C9 23 CMP #$23 A:23 X:55 Y:11 P:67 SP:FB PPU: 41, 56 CYC:6386 $D599:50 02 BVC $D59D A:23 X:55 Y:11 P:67 SP:FB PPU: 47, 56 CYC:6388 $D59B:F0 04 BEQ $D5A1 A:23 X:55 Y:11 P:67 SP:FB PPU: 53, 56 CYC:6390 $D5A1:A2 46 LDX #$46 A:23 X:55 Y:11 P:67 SP:FB PPU: 62, 56 CYC:6393 $D5A3:24 01 BIT $01 = #$FF A:23 X:46 Y:11 P:65 SP:FB PPU: 68, 56 CYC:6395 $D5A5:8E 78 06 STX $0678 = #$55 A:23 X:46 Y:11 P:E5 SP:FB PPU: 77, 56 CYC:6398 $D5A8:F0 0B BEQ $D5B5 A:23 X:46 Y:11 P:E5 SP:FB PPU: 89, 56 CYC:6402 $D5AA:10 09 BPL $D5B5 A:23 X:46 Y:11 P:E5 SP:FB PPU: 95, 56 CYC:6404 $D5AC:50 07 BVC $D5B5 A:23 X:46 Y:11 P:E5 SP:FB PPU:101, 56 CYC:6406 $D5AE:AE 78 06 LDX $0678 = #$46 A:23 X:46 Y:11 P:E5 SP:FB PPU:107, 56 CYC:6408 $D5B1:E0 46 CPX #$46 A:23 X:46 Y:11 P:65 SP:FB PPU:119, 56 CYC:6412 $D5B3:F0 04 BEQ $D5B9 A:23 X:46 Y:11 P:67 SP:FB PPU:125, 56 CYC:6414 $D5B9:A9 C0 LDA #$C0 A:23 X:46 Y:11 P:67 SP:FB PPU:134, 56 CYC:6417 $D5BB:8D 78 06 STA $0678 = #$46 A:C0 X:46 Y:11 P:E5 SP:FB PPU:140, 56 CYC:6419 $D5BE:A2 33 LDX #$33 A:C0 X:46 Y:11 P:E5 SP:FB PPU:152, 56 CYC:6423 $D5C0:A0 88 LDY #$88 A:C0 X:33 Y:11 P:65 SP:FB PPU:158, 56 CYC:6425 $D5C2:A9 05 LDA #$05 A:C0 X:33 Y:88 P:E5 SP:FB PPU:164, 56 CYC:6427 $D5C4:2C 78 06 BIT $0678 = #$C0 A:05 X:33 Y:88 P:65 SP:FB PPU:170, 56 CYC:6429 $D5C7:10 10 BPL $D5D9 A:05 X:33 Y:88 P:E7 SP:FB PPU:182, 56 CYC:6433 $D5C9:50 0E BVC $D5D9 A:05 X:33 Y:88 P:E7 SP:FB PPU:188, 56 CYC:6435 $D5CB:D0 0C BNE $D5D9 A:05 X:33 Y:88 P:E7 SP:FB PPU:194, 56 CYC:6437 $D5CD:C9 05 CMP #$05 A:05 X:33 Y:88 P:E7 SP:FB PPU:200, 56 CYC:6439 $D5CF:D0 08 BNE $D5D9 A:05 X:33 Y:88 P:67 SP:FB PPU:206, 56 CYC:6441 $D5D1:E0 33 CPX #$33 A:05 X:33 Y:88 P:67 SP:FB PPU:212, 56 CYC:6443 $D5D3:D0 04 BNE $D5D9 A:05 X:33 Y:88 P:67 SP:FB PPU:218, 56 CYC:6445 $D5D5:C0 88 CPY #$88 A:05 X:33 Y:88 P:67 SP:FB PPU:224, 56 CYC:6447 $D5D7:F0 04 BEQ $D5DD A:05 X:33 Y:88 P:67 SP:FB PPU:230, 56 CYC:6449 $D5DD:A9 03 LDA #$03 A:05 X:33 Y:88 P:67 SP:FB PPU:239, 56 CYC:6452 $D5DF:8D 78 06 STA $0678 = #$C0 A:03 X:33 Y:88 P:65 SP:FB PPU:245, 56 CYC:6454 $D5E2:A9 01 LDA #$01 A:03 X:33 Y:88 P:65 SP:FB PPU:257, 56 CYC:6458 $D5E4:2C 78 06 BIT $0678 = #$03 A:01 X:33 Y:88 P:65 SP:FB PPU:263, 56 CYC:6460 $D5E7:30 08 BMI $D5F1 A:01 X:33 Y:88 P:25 SP:FB PPU:275, 56 CYC:6464 $D5E9:70 06 BVS $D5F1 A:01 X:33 Y:88 P:25 SP:FB PPU:281, 56 CYC:6466 $D5EB:F0 04 BEQ $D5F1 A:01 X:33 Y:88 P:25 SP:FB PPU:287, 56 CYC:6468 $D5ED:C9 01 CMP #$01 A:01 X:33 Y:88 P:25 SP:FB PPU:293, 56 CYC:6470 $D5EF:F0 04 BEQ $D5F5 A:01 X:33 Y:88 P:nvUbdIZC SP:FB PPU:299, 56 CYC:6472 $D5F5:A0 B8 LDY #$B8 A:01 X:33 Y:88 P:nvUbdIZC SP:FB PPU:308, 56 CYC:6475 $D5F7:A9 AA LDA #$AA A:01 X:33 Y:B8 P:A5 SP:FB PPU:314, 56 CYC:6477 $D5F9:8D 78 06 STA $0678 = #$03 A:AA X:33 Y:B8 P:A5 SP:FB PPU:320, 56 CYC:6479 $D5FC:20 B6 F7 JSR $F7B6 A:AA X:33 Y:B8 P:A5 SP:FB PPU:332, 56 CYC:6483 $F7B6:18 CLC A:AA X:33 Y:B8 P:A5 SP:F9 PPU: 9, 57 CYC:6489 $F7B7:A9 FF LDA #$FF A:AA X:33 Y:B8 P:NvUbdIzc SP:F9 PPU: 15, 57 CYC:6491 $F7B9:85 01 STA $01 = #$FF A:FF X:33 Y:B8 P:NvUbdIzc SP:F9 PPU: 21, 57 CYC:6493 $F7BB:24 01 BIT $01 = #$FF A:FF X:33 Y:B8 P:NvUbdIzc SP:F9 PPU: 30, 57 CYC:6496 $F7BD:A9 55 LDA #$55 A:FF X:33 Y:B8 P:NVUbdIzc SP:F9 PPU: 39, 57 CYC:6499 $F7BF:60 RTS A:55 X:33 Y:B8 P:64 SP:F9 PPU: 45, 57 CYC:6501 $D5FF:0D 78 06 ORA $0678 = #$AA A:55 X:33 Y:B8 P:64 SP:FB PPU: 63, 57 CYC:6507 $D602:20 C0 F7 JSR $F7C0 A:FF X:33 Y:B8 P:NVUbdIzc SP:FB PPU: 75, 57 CYC:6511 $F7C0:B0 09 BCS $F7CB A:FF X:33 Y:B8 P:NVUbdIzc SP:F9 PPU: 93, 57 CYC:6517 $F7C2:10 07 BPL $F7CB A:FF X:33 Y:B8 P:NVUbdIzc SP:F9 PPU: 99, 57 CYC:6519 $F7C4:C9 FF CMP #$FF A:FF X:33 Y:B8 P:NVUbdIzc SP:F9 PPU:105, 57 CYC:6521 $F7C6:D0 03 BNE $F7CB A:FF X:33 Y:B8 P:67 SP:F9 PPU:111, 57 CYC:6523 $F7C8:50 01 BVC $F7CB A:FF X:33 Y:B8 P:67 SP:F9 PPU:117, 57 CYC:6525 $F7CA:60 RTS A:FF X:33 Y:B8 P:67 SP:F9 PPU:123, 57 CYC:6527 $D605:C8 INY A:FF X:33 Y:B8 P:67 SP:FB PPU:141, 57 CYC:6533 $D606:A9 00 LDA #$00 A:FF X:33 Y:B9 P:E5 SP:FB PPU:147, 57 CYC:6535 $D608:8D 78 06 STA $0678 = #$AA A:00 X:33 Y:B9 P:67 SP:FB PPU:153, 57 CYC:6537 $D60B:20 CE F7 JSR $F7CE A:00 X:33 Y:B9 P:67 SP:FB PPU:165, 57 CYC:6541 $F7CE:38 SEC A:00 X:33 Y:B9 P:67 SP:F9 PPU:183, 57 CYC:6547 $F7CF:B8 CLV A:00 X:33 Y:B9 P:67 SP:F9 PPU:189, 57 CYC:6549 $F7D0:A9 00 LDA #$00 A:00 X:33 Y:B9 P:nvUbdIZC SP:F9 PPU:195, 57 CYC:6551 $F7D2:60 RTS A:00 X:33 Y:B9 P:nvUbdIZC SP:F9 PPU:201, 57 CYC:6553 $D60E:0D 78 06 ORA $0678 = #$00 A:00 X:33 Y:B9 P:nvUbdIZC SP:FB PPU:219, 57 CYC:6559 $D611:20 D3 F7 JSR $F7D3 A:00 X:33 Y:B9 P:nvUbdIZC SP:FB PPU:231, 57 CYC:6563 $F7D3:D0 07 BNE $F7DC A:00 X:33 Y:B9 P:nvUbdIZC SP:F9 PPU:249, 57 CYC:6569 $F7D5:70 05 BVS $F7DC A:00 X:33 Y:B9 P:nvUbdIZC SP:F9 PPU:255, 57 CYC:6571 $F7D7:90 03 BCC $F7DC A:00 X:33 Y:B9 P:nvUbdIZC SP:F9 PPU:261, 57 CYC:6573 $F7D9:30 01 BMI $F7DC A:00 X:33 Y:B9 P:nvUbdIZC SP:F9 PPU:267, 57 CYC:6575 $F7DB:60 RTS A:00 X:33 Y:B9 P:nvUbdIZC SP:F9 PPU:273, 57 CYC:6577 $D614:C8 INY A:00 X:33 Y:B9 P:nvUbdIZC SP:FB PPU:291, 57 CYC:6583 $D615:A9 AA LDA #$AA A:00 X:33 Y:BA P:A5 SP:FB PPU:297, 57 CYC:6585 $D617:8D 78 06 STA $0678 = #$00 A:AA X:33 Y:BA P:A5 SP:FB PPU:303, 57 CYC:6587 $D61A:20 DF F7 JSR $F7DF A:AA X:33 Y:BA P:A5 SP:FB PPU:315, 57 CYC:6591 $F7DF:18 CLC A:AA X:33 Y:BA P:A5 SP:F9 PPU:333, 57 CYC:6597 $F7E0:24 01 BIT $01 = #$FF A:AA X:33 Y:BA P:NvUbdIzc SP:F9 PPU:339, 57 CYC:6599 $F7E2:A9 55 LDA #$55 A:AA X:33 Y:BA P:NVUbdIzc SP:F9 PPU: 7, 58 CYC:6602 $F7E4:60 RTS A:55 X:33 Y:BA P:64 SP:F9 PPU: 13, 58 CYC:6604 $D61D:2D 78 06 AND $0678 = #$AA A:55 X:33 Y:BA P:64 SP:FB PPU: 31, 58 CYC:6610 $D620:20 E5 F7 JSR $F7E5 A:00 X:33 Y:BA P:nVUbdIZc SP:FB PPU: 43, 58 CYC:6614 $F7E5:D0 07 BNE $F7EE A:00 X:33 Y:BA P:nVUbdIZc SP:F9 PPU: 61, 58 CYC:6620 $F7E7:50 05 BVC $F7EE A:00 X:33 Y:BA P:nVUbdIZc SP:F9 PPU: 67, 58 CYC:6622 $F7E9:B0 03 BCS $F7EE A:00 X:33 Y:BA P:nVUbdIZc SP:F9 PPU: 73, 58 CYC:6624 $F7EB:30 01 BMI $F7EE A:00 X:33 Y:BA P:nVUbdIZc SP:F9 PPU: 79, 58 CYC:6626 $F7ED:60 RTS A:00 X:33 Y:BA P:nVUbdIZc SP:F9 PPU: 85, 58 CYC:6628 $D623:C8 INY A:00 X:33 Y:BA P:nVUbdIZc SP:FB PPU:103, 58 CYC:6634 $D624:A9 EF LDA #$EF A:00 X:33 Y:BB P:NVUbdIzc SP:FB PPU:109, 58 CYC:6636 $D626:8D 78 06 STA $0678 = #$AA A:EF X:33 Y:BB P:NVUbdIzc SP:FB PPU:115, 58 CYC:6638 $D629:20 F1 F7 JSR $F7F1 A:EF X:33 Y:BB P:NVUbdIzc SP:FB PPU:127, 58 CYC:6642 $F7F1:38 SEC A:EF X:33 Y:BB P:NVUbdIzc SP:F9 PPU:145, 58 CYC:6648 $F7F2:B8 CLV A:EF X:33 Y:BB P:E5 SP:F9 PPU:151, 58 CYC:6650 $F7F3:A9 F8 LDA #$F8 A:EF X:33 Y:BB P:A5 SP:F9 PPU:157, 58 CYC:6652 $F7F5:60 RTS A:F8 X:33 Y:BB P:A5 SP:F9 PPU:163, 58 CYC:6654 $D62C:2D 78 06 AND $0678 = #$EF A:F8 X:33 Y:BB P:A5 SP:FB PPU:181, 58 CYC:6660 $D62F:20 F6 F7 JSR $F7F6 A:E8 X:33 Y:BB P:A5 SP:FB PPU:193, 58 CYC:6664 $F7F6:90 09 BCC $F801 A:E8 X:33 Y:BB P:A5 SP:F9 PPU:211, 58 CYC:6670 $F7F8:10 07 BPL $F801 A:E8 X:33 Y:BB P:A5 SP:F9 PPU:217, 58 CYC:6672 $F7FA:C9 E8 CMP #$E8 A:E8 X:33 Y:BB P:A5 SP:F9 PPU:223, 58 CYC:6674 $F7FC:D0 03 BNE $F801 A:E8 X:33 Y:BB P:nvUbdIZC SP:F9 PPU:229, 58 CYC:6676 $F7FE:70 01 BVS $F801 A:E8 X:33 Y:BB P:nvUbdIZC SP:F9 PPU:235, 58 CYC:6678 $F800:60 RTS A:E8 X:33 Y:BB P:nvUbdIZC SP:F9 PPU:241, 58 CYC:6680 $D632:C8 INY A:E8 X:33 Y:BB P:nvUbdIZC SP:FB PPU:259, 58 CYC:6686 $D633:A9 AA LDA #$AA A:E8 X:33 Y:BC P:A5 SP:FB PPU:265, 58 CYC:6688 $D635:8D 78 06 STA $0678 = #$EF A:AA X:33 Y:BC P:A5 SP:FB PPU:271, 58 CYC:6690 $D638:20 04 F8 JSR $F804 A:AA X:33 Y:BC P:A5 SP:FB PPU:283, 58 CYC:6694 $F804:18 CLC A:AA X:33 Y:BC P:A5 SP:F9 PPU:301, 58 CYC:6700 $F805:24 01 BIT $01 = #$FF A:AA X:33 Y:BC P:NvUbdIzc SP:F9 PPU:307, 58 CYC:6702 $F807:A9 5F LDA #$5F A:AA X:33 Y:BC P:NVUbdIzc SP:F9 PPU:316, 58 CYC:6705 $F809:60 RTS A:5F X:33 Y:BC P:64 SP:F9 PPU:322, 58 CYC:6707 $D63B:4D 78 06 EOR $0678 = #$AA A:5F X:33 Y:BC P:64 SP:FB PPU:340, 58 CYC:6713 $D63E:20 0A F8 JSR $F80A A:F5 X:33 Y:BC P:NVUbdIzc SP:FB PPU: 11, 59 CYC:6717 $F80A:B0 09 BCS $F815 A:F5 X:33 Y:BC P:NVUbdIzc SP:F9 PPU: 29, 59 CYC:6723 $F80C:10 07 BPL $F815 A:F5 X:33 Y:BC P:NVUbdIzc SP:F9 PPU: 35, 59 CYC:6725 $F80E:C9 F5 CMP #$F5 A:F5 X:33 Y:BC P:NVUbdIzc SP:F9 PPU: 41, 59 CYC:6727 $F810:D0 03 BNE $F815 A:F5 X:33 Y:BC P:67 SP:F9 PPU: 47, 59 CYC:6729 $F812:50 01 BVC $F815 A:F5 X:33 Y:BC P:67 SP:F9 PPU: 53, 59 CYC:6731 $F814:60 RTS A:F5 X:33 Y:BC P:67 SP:F9 PPU: 59, 59 CYC:6733 $D641:C8 INY A:F5 X:33 Y:BC P:67 SP:FB PPU: 77, 59 CYC:6739 $D642:A9 70 LDA #$70 A:F5 X:33 Y:BD P:E5 SP:FB PPU: 83, 59 CYC:6741 $D644:8D 78 06 STA $0678 = #$AA A:70 X:33 Y:BD P:65 SP:FB PPU: 89, 59 CYC:6743 $D647:20 18 F8 JSR $F818 A:70 X:33 Y:BD P:65 SP:FB PPU:101, 59 CYC:6747 $F818:38 SEC A:70 X:33 Y:BD P:65 SP:F9 PPU:119, 59 CYC:6753 $F819:B8 CLV A:70 X:33 Y:BD P:65 SP:F9 PPU:125, 59 CYC:6755 $F81A:A9 70 LDA #$70 A:70 X:33 Y:BD P:25 SP:F9 PPU:131, 59 CYC:6757 $F81C:60 RTS A:70 X:33 Y:BD P:25 SP:F9 PPU:137, 59 CYC:6759 $D64A:4D 78 06 EOR $0678 = #$70 A:70 X:33 Y:BD P:25 SP:FB PPU:155, 59 CYC:6765 $D64D:20 1D F8 JSR $F81D A:00 X:33 Y:BD P:nvUbdIZC SP:FB PPU:167, 59 CYC:6769 $F81D:D0 07 BNE $F826 A:00 X:33 Y:BD P:nvUbdIZC SP:F9 PPU:185, 59 CYC:6775 $F81F:70 05 BVS $F826 A:00 X:33 Y:BD P:nvUbdIZC SP:F9 PPU:191, 59 CYC:6777 $F821:90 03 BCC $F826 A:00 X:33 Y:BD P:nvUbdIZC SP:F9 PPU:197, 59 CYC:6779 $F823:30 01 BMI $F826 A:00 X:33 Y:BD P:nvUbdIZC SP:F9 PPU:203, 59 CYC:6781 $F825:60 RTS A:00 X:33 Y:BD P:nvUbdIZC SP:F9 PPU:209, 59 CYC:6783 $D650:C8 INY A:00 X:33 Y:BD P:nvUbdIZC SP:FB PPU:227, 59 CYC:6789 $D651:A9 69 LDA #$69 A:00 X:33 Y:BE P:A5 SP:FB PPU:233, 59 CYC:6791 $D653:8D 78 06 STA $0678 = #$70 A:69 X:33 Y:BE P:25 SP:FB PPU:239, 59 CYC:6793 $D656:20 29 F8 JSR $F829 A:69 X:33 Y:BE P:25 SP:FB PPU:251, 59 CYC:6797 $F829:18 CLC A:69 X:33 Y:BE P:25 SP:F9 PPU:269, 59 CYC:6803 $F82A:24 01 BIT $01 = #$FF A:69 X:33 Y:BE P:nvUbdIzc SP:F9 PPU:275, 59 CYC:6805 $F82C:A9 00 LDA #$00 A:69 X:33 Y:BE P:NVUbdIzc SP:F9 PPU:284, 59 CYC:6808 $F82E:60 RTS A:00 X:33 Y:BE P:nVUbdIZc SP:F9 PPU:290, 59 CYC:6810 $D659:6D 78 06 ADC $0678 = #$69 A:00 X:33 Y:BE P:nVUbdIZc SP:FB PPU:308, 59 CYC:6816 $D65C:20 2F F8 JSR $F82F A:69 X:33 Y:BE P:nvUbdIzc SP:FB PPU:320, 59 CYC:6820 $F82F:30 09 BMI $F83A A:69 X:33 Y:BE P:nvUbdIzc SP:F9 PPU:338, 59 CYC:6826 $F831:B0 07 BCS $F83A A:69 X:33 Y:BE P:nvUbdIzc SP:F9 PPU: 3, 60 CYC:6828 $F833:C9 69 CMP #$69 A:69 X:33 Y:BE P:nvUbdIzc SP:F9 PPU: 9, 60 CYC:6830 $F835:D0 03 BNE $F83A A:69 X:33 Y:BE P:nvUbdIZC SP:F9 PPU: 15, 60 CYC:6832 $F837:70 01 BVS $F83A A:69 X:33 Y:BE P:nvUbdIZC SP:F9 PPU: 21, 60 CYC:6834 $F839:60 RTS A:69 X:33 Y:BE P:nvUbdIZC SP:F9 PPU: 27, 60 CYC:6836 $D65F:C8 INY A:69 X:33 Y:BE P:nvUbdIZC SP:FB PPU: 45, 60 CYC:6842 $D660:20 3D F8 JSR $F83D A:69 X:33 Y:BF P:A5 SP:FB PPU: 51, 60 CYC:6844 $F83D:38 SEC A:69 X:33 Y:BF P:A5 SP:F9 PPU: 69, 60 CYC:6850 $F83E:24 01 BIT $01 = #$FF A:69 X:33 Y:BF P:A5 SP:F9 PPU: 75, 60 CYC:6852 $F840:A9 00 LDA #$00 A:69 X:33 Y:BF P:E5 SP:F9 PPU: 84, 60 CYC:6855 $F842:60 RTS A:00 X:33 Y:BF P:67 SP:F9 PPU: 90, 60 CYC:6857 $D663:6D 78 06 ADC $0678 = #$69 A:00 X:33 Y:BF P:67 SP:FB PPU:108, 60 CYC:6863 $D666:20 43 F8 JSR $F843 A:6A X:33 Y:BF P:nvUbdIzc SP:FB PPU:120, 60 CYC:6867 $F843:30 09 BMI $F84E A:6A X:33 Y:BF P:nvUbdIzc SP:F9 PPU:138, 60 CYC:6873 $F845:B0 07 BCS $F84E A:6A X:33 Y:BF P:nvUbdIzc SP:F9 PPU:144, 60 CYC:6875 $F847:C9 6A CMP #$6A A:6A X:33 Y:BF P:nvUbdIzc SP:F9 PPU:150, 60 CYC:6877 $F849:D0 03 BNE $F84E A:6A X:33 Y:BF P:nvUbdIZC SP:F9 PPU:156, 60 CYC:6879 $F84B:70 01 BVS $F84E A:6A X:33 Y:BF P:nvUbdIZC SP:F9 PPU:162, 60 CYC:6881 $F84D:60 RTS A:6A X:33 Y:BF P:nvUbdIZC SP:F9 PPU:168, 60 CYC:6883 $D669:C8 INY A:6A X:33 Y:BF P:nvUbdIZC SP:FB PPU:186, 60 CYC:6889 $D66A:A9 7F LDA #$7F A:6A X:33 Y:C0 P:A5 SP:FB PPU:192, 60 CYC:6891 $D66C:8D 78 06 STA $0678 = #$69 A:7F X:33 Y:C0 P:25 SP:FB PPU:198, 60 CYC:6893 $D66F:20 51 F8 JSR $F851 A:7F X:33 Y:C0 P:25 SP:FB PPU:210, 60 CYC:6897 $F851:38 SEC A:7F X:33 Y:C0 P:25 SP:F9 PPU:228, 60 CYC:6903 $F852:B8 CLV A:7F X:33 Y:C0 P:25 SP:F9 PPU:234, 60 CYC:6905 $F853:A9 7F LDA #$7F A:7F X:33 Y:C0 P:25 SP:F9 PPU:240, 60 CYC:6907 $F855:60 RTS A:7F X:33 Y:C0 P:25 SP:F9 PPU:246, 60 CYC:6909 $D672:6D 78 06 ADC $0678 = #$7F A:7F X:33 Y:C0 P:25 SP:FB PPU:264, 60 CYC:6915 $D675:20 56 F8 JSR $F856 A:FF X:33 Y:C0 P:NVUbdIzc SP:FB PPU:276, 60 CYC:6919 $F856:10 09 BPL $F861 A:FF X:33 Y:C0 P:NVUbdIzc SP:F9 PPU:294, 60 CYC:6925 $F858:B0 07 BCS $F861 A:FF X:33 Y:C0 P:NVUbdIzc SP:F9 PPU:300, 60 CYC:6927 $F85A:C9 FF CMP #$FF A:FF X:33 Y:C0 P:NVUbdIzc SP:F9 PPU:306, 60 CYC:6929 $F85C:D0 03 BNE $F861 A:FF X:33 Y:C0 P:67 SP:F9 PPU:312, 60 CYC:6931 $F85E:50 01 BVC $F861 A:FF X:33 Y:C0 P:67 SP:F9 PPU:318, 60 CYC:6933 $F860:60 RTS A:FF X:33 Y:C0 P:67 SP:F9 PPU:324, 60 CYC:6935 $D678:C8 INY A:FF X:33 Y:C0 P:67 SP:FB PPU: 1, 61 CYC:6941 $D679:A9 80 LDA #$80 A:FF X:33 Y:C1 P:E5 SP:FB PPU: 7, 61 CYC:6943 $D67B:8D 78 06 STA $0678 = #$7F A:80 X:33 Y:C1 P:E5 SP:FB PPU: 13, 61 CYC:6945 $D67E:20 64 F8 JSR $F864 A:80 X:33 Y:C1 P:E5 SP:FB PPU: 25, 61 CYC:6949 $F864:18 CLC A:80 X:33 Y:C1 P:E5 SP:F9 PPU: 43, 61 CYC:6955 $F865:24 01 BIT $01 = #$FF A:80 X:33 Y:C1 P:NVUbdIzc SP:F9 PPU: 49, 61 CYC:6957 $F867:A9 7F LDA #$7F A:80 X:33 Y:C1 P:NVUbdIzc SP:F9 PPU: 58, 61 CYC:6960 $F869:60 RTS A:7F X:33 Y:C1 P:64 SP:F9 PPU: 64, 61 CYC:6962 $D681:6D 78 06 ADC $0678 = #$80 A:7F X:33 Y:C1 P:64 SP:FB PPU: 82, 61 CYC:6968 $D684:20 6A F8 JSR $F86A A:FF X:33 Y:C1 P:NvUbdIzc SP:FB PPU: 94, 61 CYC:6972 $F86A:10 09 BPL $F875 A:FF X:33 Y:C1 P:NvUbdIzc SP:F9 PPU:112, 61 CYC:6978 $F86C:B0 07 BCS $F875 A:FF X:33 Y:C1 P:NvUbdIzc SP:F9 PPU:118, 61 CYC:6980 $F86E:C9 FF CMP #$FF A:FF X:33 Y:C1 P:NvUbdIzc SP:F9 PPU:124, 61 CYC:6982 $F870:D0 03 BNE $F875 A:FF X:33 Y:C1 P:nvUbdIZC SP:F9 PPU:130, 61 CYC:6984 $F872:70 01 BVS $F875 A:FF X:33 Y:C1 P:nvUbdIZC SP:F9 PPU:136, 61 CYC:6986 $F874:60 RTS A:FF X:33 Y:C1 P:nvUbdIZC SP:F9 PPU:142, 61 CYC:6988 $D687:C8 INY A:FF X:33 Y:C1 P:nvUbdIZC SP:FB PPU:160, 61 CYC:6994 $D688:20 78 F8 JSR $F878 A:FF X:33 Y:C2 P:A5 SP:FB PPU:166, 61 CYC:6996 $F878:38 SEC A:FF X:33 Y:C2 P:A5 SP:F9 PPU:184, 61 CYC:7002 $F879:B8 CLV A:FF X:33 Y:C2 P:A5 SP:F9 PPU:190, 61 CYC:7004 $F87A:A9 7F LDA #$7F A:FF X:33 Y:C2 P:A5 SP:F9 PPU:196, 61 CYC:7006 $F87C:60 RTS A:7F X:33 Y:C2 P:25 SP:F9 PPU:202, 61 CYC:7008 $D68B:6D 78 06 ADC $0678 = #$80 A:7F X:33 Y:C2 P:25 SP:FB PPU:220, 61 CYC:7014 $D68E:20 7D F8 JSR $F87D A:00 X:33 Y:C2 P:nvUbdIZC SP:FB PPU:232, 61 CYC:7018 $F87D:D0 07 BNE $F886 A:00 X:33 Y:C2 P:nvUbdIZC SP:F9 PPU:250, 61 CYC:7024 $F87F:30 05 BMI $F886 A:00 X:33 Y:C2 P:nvUbdIZC SP:F9 PPU:256, 61 CYC:7026 $F881:70 03 BVS $F886 A:00 X:33 Y:C2 P:nvUbdIZC SP:F9 PPU:262, 61 CYC:7028 $F883:90 01 BCC $F886 A:00 X:33 Y:C2 P:nvUbdIZC SP:F9 PPU:268, 61 CYC:7030 $F885:60 RTS A:00 X:33 Y:C2 P:nvUbdIZC SP:F9 PPU:274, 61 CYC:7032 $D691:C8 INY A:00 X:33 Y:C2 P:nvUbdIZC SP:FB PPU:292, 61 CYC:7038 $D692:A9 40 LDA #$40 A:00 X:33 Y:C3 P:A5 SP:FB PPU:298, 61 CYC:7040 $D694:8D 78 06 STA $0678 = #$80 A:40 X:33 Y:C3 P:25 SP:FB PPU:304, 61 CYC:7042 $D697:20 89 F8 JSR $F889 A:40 X:33 Y:C3 P:25 SP:FB PPU:316, 61 CYC:7046 $F889:24 01 BIT $01 = #$FF A:40 X:33 Y:C3 P:25 SP:F9 PPU:334, 61 CYC:7052 $F88B:A9 40 LDA #$40 A:40 X:33 Y:C3 P:E5 SP:F9 PPU: 2, 62 CYC:7055 $F88D:60 RTS A:40 X:33 Y:C3 P:65 SP:F9 PPU: 8, 62 CYC:7057 $D69A:CD 78 06 CMP $0678 = #$40 A:40 X:33 Y:C3 P:65 SP:FB PPU: 26, 62 CYC:7063 $D69D:20 8E F8 JSR $F88E A:40 X:33 Y:C3 P:67 SP:FB PPU: 38, 62 CYC:7067 $F88E:30 07 BMI $F897 A:40 X:33 Y:C3 P:67 SP:F9 PPU: 56, 62 CYC:7073 $F890:90 05 BCC $F897 A:40 X:33 Y:C3 P:67 SP:F9 PPU: 62, 62 CYC:7075 $F892:D0 03 BNE $F897 A:40 X:33 Y:C3 P:67 SP:F9 PPU: 68, 62 CYC:7077 $F894:50 01 BVC $F897 A:40 X:33 Y:C3 P:67 SP:F9 PPU: 74, 62 CYC:7079 $F896:60 RTS A:40 X:33 Y:C3 P:67 SP:F9 PPU: 80, 62 CYC:7081 $D6A0:C8 INY A:40 X:33 Y:C3 P:67 SP:FB PPU: 98, 62 CYC:7087 $D6A1:48 PHA A:40 X:33 Y:C4 P:E5 SP:FB PPU:104, 62 CYC:7089 $D6A2:A9 3F LDA #$3F A:40 X:33 Y:C4 P:E5 SP:FA PPU:113, 62 CYC:7092 $D6A4:8D 78 06 STA $0678 = #$40 A:3F X:33 Y:C4 P:65 SP:FA PPU:119, 62 CYC:7094 $D6A7:68 PLA A:3F X:33 Y:C4 P:65 SP:FA PPU:131, 62 CYC:7098 $D6A8:20 9A F8 JSR $F89A A:40 X:33 Y:C4 P:65 SP:FB PPU:143, 62 CYC:7102 $F89A:B8 CLV A:40 X:33 Y:C4 P:65 SP:F9 PPU:161, 62 CYC:7108 $F89B:60 RTS A:40 X:33 Y:C4 P:25 SP:F9 PPU:167, 62 CYC:7110 $D6AB:CD 78 06 CMP $0678 = #$3F A:40 X:33 Y:C4 P:25 SP:FB PPU:185, 62 CYC:7116 $D6AE:20 9C F8 JSR $F89C A:40 X:33 Y:C4 P:25 SP:FB PPU:197, 62 CYC:7120 $F89C:F0 07 BEQ $F8A5 A:40 X:33 Y:C4 P:25 SP:F9 PPU:215, 62 CYC:7126 $F89E:30 05 BMI $F8A5 A:40 X:33 Y:C4 P:25 SP:F9 PPU:221, 62 CYC:7128 $F8A0:90 03 BCC $F8A5 A:40 X:33 Y:C4 P:25 SP:F9 PPU:227, 62 CYC:7130 $F8A2:70 01 BVS $F8A5 A:40 X:33 Y:C4 P:25 SP:F9 PPU:233, 62 CYC:7132 $F8A4:60 RTS A:40 X:33 Y:C4 P:25 SP:F9 PPU:239, 62 CYC:7134 $D6B1:C8 INY A:40 X:33 Y:C4 P:25 SP:FB PPU:257, 62 CYC:7140 $D6B2:48 PHA A:40 X:33 Y:C5 P:A5 SP:FB PPU:263, 62 CYC:7142 $D6B3:A9 41 LDA #$41 A:40 X:33 Y:C5 P:A5 SP:FA PPU:272, 62 CYC:7145 $D6B5:8D 78 06 STA $0678 = #$3F A:41 X:33 Y:C5 P:25 SP:FA PPU:278, 62 CYC:7147 $D6B8:68 PLA A:41 X:33 Y:C5 P:25 SP:FA PPU:290, 62 CYC:7151 $D6B9:CD 78 06 CMP $0678 = #$41 A:40 X:33 Y:C5 P:25 SP:FB PPU:302, 62 CYC:7155 $D6BC:20 A8 F8 JSR $F8A8 A:40 X:33 Y:C5 P:NvUbdIzc SP:FB PPU:314, 62 CYC:7159 $F8A8:F0 05 BEQ $F8AF A:40 X:33 Y:C5 P:NvUbdIzc SP:F9 PPU:332, 62 CYC:7165 $F8AA:10 03 BPL $F8AF A:40 X:33 Y:C5 P:NvUbdIzc SP:F9 PPU:338, 62 CYC:7167 $F8AC:10 01 BPL $F8AF A:40 X:33 Y:C5 P:NvUbdIzc SP:F9 PPU: 3, 63 CYC:7169 $F8AE:60 RTS A:40 X:33 Y:C5 P:NvUbdIzc SP:F9 PPU: 9, 63 CYC:7171 $D6BF:C8 INY A:40 X:33 Y:C5 P:NvUbdIzc SP:FB PPU: 27, 63 CYC:7177 $D6C0:48 PHA A:40 X:33 Y:C6 P:NvUbdIzc SP:FB PPU: 33, 63 CYC:7179 $D6C1:A9 00 LDA #$00 A:40 X:33 Y:C6 P:NvUbdIzc SP:FA PPU: 42, 63 CYC:7182 $D6C3:8D 78 06 STA $0678 = #$41 A:00 X:33 Y:C6 P:nvUbdIZc SP:FA PPU: 48, 63 CYC:7184 $D6C6:68 PLA A:00 X:33 Y:C6 P:nvUbdIZc SP:FA PPU: 60, 63 CYC:7188 $D6C7:20 B2 F8 JSR $F8B2 A:40 X:33 Y:C6 P:nvUbdIzc SP:FB PPU: 72, 63 CYC:7192 $F8B2:A9 80 LDA #$80 A:40 X:33 Y:C6 P:nvUbdIzc SP:F9 PPU: 90, 63 CYC:7198 $F8B4:60 RTS A:80 X:33 Y:C6 P:NvUbdIzc SP:F9 PPU: 96, 63 CYC:7200 $D6CA:CD 78 06 CMP $0678 = #$00 A:80 X:33 Y:C6 P:NvUbdIzc SP:FB PPU:114, 63 CYC:7206 $D6CD:20 B5 F8 JSR $F8B5 A:80 X:33 Y:C6 P:A5 SP:FB PPU:126, 63 CYC:7210 $F8B5:F0 05 BEQ $F8BC A:80 X:33 Y:C6 P:A5 SP:F9 PPU:144, 63 CYC:7216 $F8B7:10 03 BPL $F8BC A:80 X:33 Y:C6 P:A5 SP:F9 PPU:150, 63 CYC:7218 $F8B9:90 01 BCC $F8BC A:80 X:33 Y:C6 P:A5 SP:F9 PPU:156, 63 CYC:7220 $F8BB:60 RTS A:80 X:33 Y:C6 P:A5 SP:F9 PPU:162, 63 CYC:7222 $D6D0:C8 INY A:80 X:33 Y:C6 P:A5 SP:FB PPU:180, 63 CYC:7228 $D6D1:48 PHA A:80 X:33 Y:C7 P:A5 SP:FB PPU:186, 63 CYC:7230 $D6D2:A9 80 LDA #$80 A:80 X:33 Y:C7 P:A5 SP:FA PPU:195, 63 CYC:7233 $D6D4:8D 78 06 STA $0678 = #$00 A:80 X:33 Y:C7 P:A5 SP:FA PPU:201, 63 CYC:7235 $D6D7:68 PLA A:80 X:33 Y:C7 P:A5 SP:FA PPU:213, 63 CYC:7239 $D6D8:CD 78 06 CMP $0678 = #$80 A:80 X:33 Y:C7 P:A5 SP:FB PPU:225, 63 CYC:7243 $D6DB:20 BF F8 JSR $F8BF A:80 X:33 Y:C7 P:nvUbdIZC SP:FB PPU:237, 63 CYC:7247 $F8BF:D0 05 BNE $F8C6 A:80 X:33 Y:C7 P:nvUbdIZC SP:F9 PPU:255, 63 CYC:7253 $F8C1:30 03 BMI $F8C6 A:80 X:33 Y:C7 P:nvUbdIZC SP:F9 PPU:261, 63 CYC:7255 $F8C3:90 01 BCC $F8C6 A:80 X:33 Y:C7 P:nvUbdIZC SP:F9 PPU:267, 63 CYC:7257 $F8C5:60 RTS A:80 X:33 Y:C7 P:nvUbdIZC SP:F9 PPU:273, 63 CYC:7259 $D6DE:C8 INY A:80 X:33 Y:C7 P:nvUbdIZC SP:FB PPU:291, 63 CYC:7265 $D6DF:48 PHA A:80 X:33 Y:C8 P:A5 SP:FB PPU:297, 63 CYC:7267 $D6E0:A9 81 LDA #$81 A:80 X:33 Y:C8 P:A5 SP:FA PPU:306, 63 CYC:7270 $D6E2:8D 78 06 STA $0678 = #$80 A:81 X:33 Y:C8 P:A5 SP:FA PPU:312, 63 CYC:7272 $D6E5:68 PLA A:81 X:33 Y:C8 P:A5 SP:FA PPU:324, 63 CYC:7276 $D6E6:CD 78 06 CMP $0678 = #$81 A:80 X:33 Y:C8 P:A5 SP:FB PPU:336, 63 CYC:7280 $D6E9:20 C9 F8 JSR $F8C9 A:80 X:33 Y:C8 P:NvUbdIzc SP:FB PPU: 7, 64 CYC:7284 $F8C9:B0 05 BCS $F8D0 A:80 X:33 Y:C8 P:NvUbdIzc SP:F9 PPU: 25, 64 CYC:7290 $F8CB:F0 03 BEQ $F8D0 A:80 X:33 Y:C8 P:NvUbdIzc SP:F9 PPU: 31, 64 CYC:7292 $F8CD:10 01 BPL $F8D0 A:80 X:33 Y:C8 P:NvUbdIzc SP:F9 PPU: 37, 64 CYC:7294 $F8CF:60 RTS A:80 X:33 Y:C8 P:NvUbdIzc SP:F9 PPU: 43, 64 CYC:7296 $D6EC:C8 INY A:80 X:33 Y:C8 P:NvUbdIzc SP:FB PPU: 61, 64 CYC:7302 $D6ED:48 PHA A:80 X:33 Y:C9 P:NvUbdIzc SP:FB PPU: 67, 64 CYC:7304 $D6EE:A9 7F LDA #$7F A:80 X:33 Y:C9 P:NvUbdIzc SP:FA PPU: 76, 64 CYC:7307 $D6F0:8D 78 06 STA $0678 = #$81 A:7F X:33 Y:C9 P:nvUbdIzc SP:FA PPU: 82, 64 CYC:7309 $D6F3:68 PLA A:7F X:33 Y:C9 P:nvUbdIzc SP:FA PPU: 94, 64 CYC:7313 $D6F4:CD 78 06 CMP $0678 = #$7F A:80 X:33 Y:C9 P:NvUbdIzc SP:FB PPU:106, 64 CYC:7317 $D6F7:20 D3 F8 JSR $F8D3 A:80 X:33 Y:C9 P:25 SP:FB PPU:118, 64 CYC:7321 $F8D3:90 05 BCC $F8DA A:80 X:33 Y:C9 P:25 SP:F9 PPU:136, 64 CYC:7327 $F8D5:F0 03 BEQ $F8DA A:80 X:33 Y:C9 P:25 SP:F9 PPU:142, 64 CYC:7329 $F8D7:30 01 BMI $F8DA A:80 X:33 Y:C9 P:25 SP:F9 PPU:148, 64 CYC:7331 $F8D9:60 RTS A:80 X:33 Y:C9 P:25 SP:F9 PPU:154, 64 CYC:7333 $D6FA:C8 INY A:80 X:33 Y:C9 P:25 SP:FB PPU:172, 64 CYC:7339 $D6FB:A9 40 LDA #$40 A:80 X:33 Y:CA P:A5 SP:FB PPU:178, 64 CYC:7341 $D6FD:8D 78 06 STA $0678 = #$7F A:40 X:33 Y:CA P:25 SP:FB PPU:184, 64 CYC:7343 $D700:20 31 F9 JSR $F931 A:40 X:33 Y:CA P:25 SP:FB PPU:196, 64 CYC:7347 $F931:24 01 BIT $01 = #$FF A:40 X:33 Y:CA P:25 SP:F9 PPU:214, 64 CYC:7353 $F933:A9 40 LDA #$40 A:40 X:33 Y:CA P:E5 SP:F9 PPU:223, 64 CYC:7356 $F935:38 SEC A:40 X:33 Y:CA P:65 SP:F9 PPU:229, 64 CYC:7358 $F936:60 RTS A:40 X:33 Y:CA P:65 SP:F9 PPU:235, 64 CYC:7360 $D703:ED 78 06 SBC $0678 = #$40 A:40 X:33 Y:CA P:65 SP:FB PPU:253, 64 CYC:7366 $D706:20 37 F9 JSR $F937 A:00 X:33 Y:CA P:nvUbdIZC SP:FB PPU:265, 64 CYC:7370 $F937:30 0B BMI $F944 A:00 X:33 Y:CA P:nvUbdIZC SP:F9 PPU:283, 64 CYC:7376 $F939:90 09 BCC $F944 A:00 X:33 Y:CA P:nvUbdIZC SP:F9 PPU:289, 64 CYC:7378 $F93B:D0 07 BNE $F944 A:00 X:33 Y:CA P:nvUbdIZC SP:F9 PPU:295, 64 CYC:7380 $F93D:70 05 BVS $F944 A:00 X:33 Y:CA P:nvUbdIZC SP:F9 PPU:301, 64 CYC:7382 $F93F:C9 00 CMP #$00 A:00 X:33 Y:CA P:nvUbdIZC SP:F9 PPU:307, 64 CYC:7384 $F941:D0 01 BNE $F944 A:00 X:33 Y:CA P:nvUbdIZC SP:F9 PPU:313, 64 CYC:7386 $F943:60 RTS A:00 X:33 Y:CA P:nvUbdIZC SP:F9 PPU:319, 64 CYC:7388 $D709:C8 INY A:00 X:33 Y:CA P:nvUbdIZC SP:FB PPU:337, 64 CYC:7394 $D70A:A9 3F LDA #$3F A:00 X:33 Y:CB P:A5 SP:FB PPU: 2, 65 CYC:7396 $D70C:8D 78 06 STA $0678 = #$40 A:3F X:33 Y:CB P:25 SP:FB PPU: 8, 65 CYC:7398 $D70F:20 47 F9 JSR $F947 A:3F X:33 Y:CB P:25 SP:FB PPU: 20, 65 CYC:7402 $F947:B8 CLV A:3F X:33 Y:CB P:25 SP:F9 PPU: 38, 65 CYC:7408 $F948:38 SEC A:3F X:33 Y:CB P:25 SP:F9 PPU: 44, 65 CYC:7410 $F949:A9 40 LDA #$40 A:3F X:33 Y:CB P:25 SP:F9 PPU: 50, 65 CYC:7412 $F94B:60 RTS A:40 X:33 Y:CB P:25 SP:F9 PPU: 56, 65 CYC:7414 $D712:ED 78 06 SBC $0678 = #$3F A:40 X:33 Y:CB P:25 SP:FB PPU: 74, 65 CYC:7420 $D715:20 4C F9 JSR $F94C A:01 X:33 Y:CB P:25 SP:FB PPU: 86, 65 CYC:7424 $F94C:F0 0B BEQ $F959 A:01 X:33 Y:CB P:25 SP:F9 PPU:104, 65 CYC:7430 $F94E:30 09 BMI $F959 A:01 X:33 Y:CB P:25 SP:F9 PPU:110, 65 CYC:7432 $F950:90 07 BCC $F959 A:01 X:33 Y:CB P:25 SP:F9 PPU:116, 65 CYC:7434 $F952:70 05 BVS $F959 A:01 X:33 Y:CB P:25 SP:F9 PPU:122, 65 CYC:7436 $F954:C9 01 CMP #$01 A:01 X:33 Y:CB P:25 SP:F9 PPU:128, 65 CYC:7438 $F956:D0 01 BNE $F959 A:01 X:33 Y:CB P:nvUbdIZC SP:F9 PPU:134, 65 CYC:7440 $F958:60 RTS A:01 X:33 Y:CB P:nvUbdIZC SP:F9 PPU:140, 65 CYC:7442 $D718:C8 INY A:01 X:33 Y:CB P:nvUbdIZC SP:FB PPU:158, 65 CYC:7448 $D719:A9 41 LDA #$41 A:01 X:33 Y:CC P:A5 SP:FB PPU:164, 65 CYC:7450 $D71B:8D 78 06 STA $0678 = #$3F A:41 X:33 Y:CC P:25 SP:FB PPU:170, 65 CYC:7452 $D71E:20 5C F9 JSR $F95C A:41 X:33 Y:CC P:25 SP:FB PPU:182, 65 CYC:7456 $F95C:A9 40 LDA #$40 A:41 X:33 Y:CC P:25 SP:F9 PPU:200, 65 CYC:7462 $F95E:38 SEC A:40 X:33 Y:CC P:25 SP:F9 PPU:206, 65 CYC:7464 $F95F:24 01 BIT $01 = #$FF A:40 X:33 Y:CC P:25 SP:F9 PPU:212, 65 CYC:7466 $F961:60 RTS A:40 X:33 Y:CC P:E5 SP:F9 PPU:221, 65 CYC:7469 $D721:ED 78 06 SBC $0678 = #$41 A:40 X:33 Y:CC P:E5 SP:FB PPU:239, 65 CYC:7475 $D724:20 62 F9 JSR $F962 A:FF X:33 Y:CC P:NvUbdIzc SP:FB PPU:251, 65 CYC:7479 $F962:B0 0B BCS $F96F A:FF X:33 Y:CC P:NvUbdIzc SP:F9 PPU:269, 65 CYC:7485 $F964:F0 09 BEQ $F96F A:FF X:33 Y:CC P:NvUbdIzc SP:F9 PPU:275, 65 CYC:7487 $F966:10 07 BPL $F96F A:FF X:33 Y:CC P:NvUbdIzc SP:F9 PPU:281, 65 CYC:7489 $F968:70 05 BVS $F96F A:FF X:33 Y:CC P:NvUbdIzc SP:F9 PPU:287, 65 CYC:7491 $F96A:C9 FF CMP #$FF A:FF X:33 Y:CC P:NvUbdIzc SP:F9 PPU:293, 65 CYC:7493 $F96C:D0 01 BNE $F96F A:FF X:33 Y:CC P:nvUbdIZC SP:F9 PPU:299, 65 CYC:7495 $F96E:60 RTS A:FF X:33 Y:CC P:nvUbdIZC SP:F9 PPU:305, 65 CYC:7497 $D727:C8 INY A:FF X:33 Y:CC P:nvUbdIZC SP:FB PPU:323, 65 CYC:7503 $D728:A9 00 LDA #$00 A:FF X:33 Y:CD P:A5 SP:FB PPU:329, 65 CYC:7505 $D72A:8D 78 06 STA $0678 = #$41 A:00 X:33 Y:CD P:nvUbdIZC SP:FB PPU:335, 65 CYC:7507 $D72D:20 72 F9 JSR $F972 A:00 X:33 Y:CD P:nvUbdIZC SP:FB PPU: 6, 66 CYC:7511 $F972:18 CLC A:00 X:33 Y:CD P:nvUbdIZC SP:F9 PPU: 24, 66 CYC:7517 $F973:A9 80 LDA #$80 A:00 X:33 Y:CD P:nvUbdIZc SP:F9 PPU: 30, 66 CYC:7519 $F975:60 RTS A:80 X:33 Y:CD P:NvUbdIzc SP:F9 PPU: 36, 66 CYC:7521 $D730:ED 78 06 SBC $0678 = #$00 A:80 X:33 Y:CD P:NvUbdIzc SP:FB PPU: 54, 66 CYC:7527 $D733:20 76 F9 JSR $F976 A:7F X:33 Y:CD P:65 SP:FB PPU: 66, 66 CYC:7531 $F976:90 05 BCC $F97D A:7F X:33 Y:CD P:65 SP:F9 PPU: 84, 66 CYC:7537 $F978:C9 7F CMP #$7F A:7F X:33 Y:CD P:65 SP:F9 PPU: 90, 66 CYC:7539 $F97A:D0 01 BNE $F97D A:7F X:33 Y:CD P:67 SP:F9 PPU: 96, 66 CYC:7541 $F97C:60 RTS A:7F X:33 Y:CD P:67 SP:F9 PPU:102, 66 CYC:7543 $D736:C8 INY A:7F X:33 Y:CD P:67 SP:FB PPU:120, 66 CYC:7549 $D737:A9 7F LDA #$7F A:7F X:33 Y:CE P:E5 SP:FB PPU:126, 66 CYC:7551 $D739:8D 78 06 STA $0678 = #$00 A:7F X:33 Y:CE P:65 SP:FB PPU:132, 66 CYC:7553 $D73C:20 80 F9 JSR $F980 A:7F X:33 Y:CE P:65 SP:FB PPU:144, 66 CYC:7557 $F980:38 SEC A:7F X:33 Y:CE P:65 SP:F9 PPU:162, 66 CYC:7563 $F981:A9 81 LDA #$81 A:7F X:33 Y:CE P:65 SP:F9 PPU:168, 66 CYC:7565 $F983:60 RTS A:81 X:33 Y:CE P:E5 SP:F9 PPU:174, 66 CYC:7567 $D73F:ED 78 06 SBC $0678 = #$7F A:81 X:33 Y:CE P:E5 SP:FB PPU:192, 66 CYC:7573 $D742:20 84 F9 JSR $F984 A:02 X:33 Y:CE P:65 SP:FB PPU:204, 66 CYC:7577 $F984:50 07 BVC $F98D A:02 X:33 Y:CE P:65 SP:F9 PPU:222, 66 CYC:7583 $F986:90 05 BCC $F98D A:02 X:33 Y:CE P:65 SP:F9 PPU:228, 66 CYC:7585 $F988:C9 02 CMP #$02 A:02 X:33 Y:CE P:65 SP:F9 PPU:234, 66 CYC:7587 $F98A:D0 01 BNE $F98D A:02 X:33 Y:CE P:67 SP:F9 PPU:240, 66 CYC:7589 $F98C:60 RTS A:02 X:33 Y:CE P:67 SP:F9 PPU:246, 66 CYC:7591 $D745:C8 INY A:02 X:33 Y:CE P:67 SP:FB PPU:264, 66 CYC:7597 $D746:A9 40 LDA #$40 A:02 X:33 Y:CF P:E5 SP:FB PPU:270, 66 CYC:7599 $D748:8D 78 06 STA $0678 = #$7F A:40 X:33 Y:CF P:65 SP:FB PPU:276, 66 CYC:7601 $D74B:20 89 F8 JSR $F889 A:40 X:33 Y:CF P:65 SP:FB PPU:288, 66 CYC:7605 $F889:24 01 BIT $01 = #$FF A:40 X:33 Y:CF P:65 SP:F9 PPU:306, 66 CYC:7611 $F88B:A9 40 LDA #$40 A:40 X:33 Y:CF P:E5 SP:F9 PPU:315, 66 CYC:7614 $F88D:60 RTS A:40 X:33 Y:CF P:65 SP:F9 PPU:321, 66 CYC:7616 $D74E:AA TAX A:40 X:33 Y:CF P:65 SP:FB PPU:339, 66 CYC:7622 $D74F:EC 78 06 CPX $0678 = #$40 A:40 X:40 Y:CF P:65 SP:FB PPU: 4, 67 CYC:7624 $D752:20 8E F8 JSR $F88E A:40 X:40 Y:CF P:67 SP:FB PPU: 16, 67 CYC:7628 $F88E:30 07 BMI $F897 A:40 X:40 Y:CF P:67 SP:F9 PPU: 34, 67 CYC:7634 $F890:90 05 BCC $F897 A:40 X:40 Y:CF P:67 SP:F9 PPU: 40, 67 CYC:7636 $F892:D0 03 BNE $F897 A:40 X:40 Y:CF P:67 SP:F9 PPU: 46, 67 CYC:7638 $F894:50 01 BVC $F897 A:40 X:40 Y:CF P:67 SP:F9 PPU: 52, 67 CYC:7640 $F896:60 RTS A:40 X:40 Y:CF P:67 SP:F9 PPU: 58, 67 CYC:7642 $D755:C8 INY A:40 X:40 Y:CF P:67 SP:FB PPU: 76, 67 CYC:7648 $D756:A9 3F LDA #$3F A:40 X:40 Y:D0 P:E5 SP:FB PPU: 82, 67 CYC:7650 $D758:8D 78 06 STA $0678 = #$40 A:3F X:40 Y:D0 P:65 SP:FB PPU: 88, 67 CYC:7652 $D75B:20 9A F8 JSR $F89A A:3F X:40 Y:D0 P:65 SP:FB PPU:100, 67 CYC:7656 $F89A:B8 CLV A:3F X:40 Y:D0 P:65 SP:F9 PPU:118, 67 CYC:7662 $F89B:60 RTS A:3F X:40 Y:D0 P:25 SP:F9 PPU:124, 67 CYC:7664 $D75E:EC 78 06 CPX $0678 = #$3F A:3F X:40 Y:D0 P:25 SP:FB PPU:142, 67 CYC:7670 $D761:20 9C F8 JSR $F89C A:3F X:40 Y:D0 P:25 SP:FB PPU:154, 67 CYC:7674 $F89C:F0 07 BEQ $F8A5 A:3F X:40 Y:D0 P:25 SP:F9 PPU:172, 67 CYC:7680 $F89E:30 05 BMI $F8A5 A:3F X:40 Y:D0 P:25 SP:F9 PPU:178, 67 CYC:7682 $F8A0:90 03 BCC $F8A5 A:3F X:40 Y:D0 P:25 SP:F9 PPU:184, 67 CYC:7684 $F8A2:70 01 BVS $F8A5 A:3F X:40 Y:D0 P:25 SP:F9 PPU:190, 67 CYC:7686 $F8A4:60 RTS A:3F X:40 Y:D0 P:25 SP:F9 PPU:196, 67 CYC:7688 $D764:C8 INY A:3F X:40 Y:D0 P:25 SP:FB PPU:214, 67 CYC:7694 $D765:A9 41 LDA #$41 A:3F X:40 Y:D1 P:A5 SP:FB PPU:220, 67 CYC:7696 $D767:8D 78 06 STA $0678 = #$3F A:41 X:40 Y:D1 P:25 SP:FB PPU:226, 67 CYC:7698 $D76A:EC 78 06 CPX $0678 = #$41 A:41 X:40 Y:D1 P:25 SP:FB PPU:238, 67 CYC:7702 $D76D:20 A8 F8 JSR $F8A8 A:41 X:40 Y:D1 P:NvUbdIzc SP:FB PPU:250, 67 CYC:7706 $F8A8:F0 05 BEQ $F8AF A:41 X:40 Y:D1 P:NvUbdIzc SP:F9 PPU:268, 67 CYC:7712 $F8AA:10 03 BPL $F8AF A:41 X:40 Y:D1 P:NvUbdIzc SP:F9 PPU:274, 67 CYC:7714 $F8AC:10 01 BPL $F8AF A:41 X:40 Y:D1 P:NvUbdIzc SP:F9 PPU:280, 67 CYC:7716 $F8AE:60 RTS A:41 X:40 Y:D1 P:NvUbdIzc SP:F9 PPU:286, 67 CYC:7718 $D770:C8 INY A:41 X:40 Y:D1 P:NvUbdIzc SP:FB PPU:304, 67 CYC:7724 $D771:A9 00 LDA #$00 A:41 X:40 Y:D2 P:NvUbdIzc SP:FB PPU:310, 67 CYC:7726 $D773:8D 78 06 STA $0678 = #$41 A:00 X:40 Y:D2 P:nvUbdIZc SP:FB PPU:316, 67 CYC:7728 $D776:20 B2 F8 JSR $F8B2 A:00 X:40 Y:D2 P:nvUbdIZc SP:FB PPU:328, 67 CYC:7732 $F8B2:A9 80 LDA #$80 A:00 X:40 Y:D2 P:nvUbdIZc SP:F9 PPU: 5, 68 CYC:7738 $F8B4:60 RTS A:80 X:40 Y:D2 P:NvUbdIzc SP:F9 PPU: 11, 68 CYC:7740 $D779:AA TAX A:80 X:40 Y:D2 P:NvUbdIzc SP:FB PPU: 29, 68 CYC:7746 $D77A:EC 78 06 CPX $0678 = #$00 A:80 X:80 Y:D2 P:NvUbdIzc SP:FB PPU: 35, 68 CYC:7748 $D77D:20 B5 F8 JSR $F8B5 A:80 X:80 Y:D2 P:A5 SP:FB PPU: 47, 68 CYC:7752 $F8B5:F0 05 BEQ $F8BC A:80 X:80 Y:D2 P:A5 SP:F9 PPU: 65, 68 CYC:7758 $F8B7:10 03 BPL $F8BC A:80 X:80 Y:D2 P:A5 SP:F9 PPU: 71, 68 CYC:7760 $F8B9:90 01 BCC $F8BC A:80 X:80 Y:D2 P:A5 SP:F9 PPU: 77, 68 CYC:7762 $F8BB:60 RTS A:80 X:80 Y:D2 P:A5 SP:F9 PPU: 83, 68 CYC:7764 $D780:C8 INY A:80 X:80 Y:D2 P:A5 SP:FB PPU:101, 68 CYC:7770 $D781:A9 80 LDA #$80 A:80 X:80 Y:D3 P:A5 SP:FB PPU:107, 68 CYC:7772 $D783:8D 78 06 STA $0678 = #$00 A:80 X:80 Y:D3 P:A5 SP:FB PPU:113, 68 CYC:7774 $D786:EC 78 06 CPX $0678 = #$80 A:80 X:80 Y:D3 P:A5 SP:FB PPU:125, 68 CYC:7778 $D789:20 BF F8 JSR $F8BF A:80 X:80 Y:D3 P:nvUbdIZC SP:FB PPU:137, 68 CYC:7782 $F8BF:D0 05 BNE $F8C6 A:80 X:80 Y:D3 P:nvUbdIZC SP:F9 PPU:155, 68 CYC:7788 $F8C1:30 03 BMI $F8C6 A:80 X:80 Y:D3 P:nvUbdIZC SP:F9 PPU:161, 68 CYC:7790 $F8C3:90 01 BCC $F8C6 A:80 X:80 Y:D3 P:nvUbdIZC SP:F9 PPU:167, 68 CYC:7792 $F8C5:60 RTS A:80 X:80 Y:D3 P:nvUbdIZC SP:F9 PPU:173, 68 CYC:7794 $D78C:C8 INY A:80 X:80 Y:D3 P:nvUbdIZC SP:FB PPU:191, 68 CYC:7800 $D78D:A9 81 LDA #$81 A:80 X:80 Y:D4 P:A5 SP:FB PPU:197, 68 CYC:7802 $D78F:8D 78 06 STA $0678 = #$80 A:81 X:80 Y:D4 P:A5 SP:FB PPU:203, 68 CYC:7804 $D792:EC 78 06 CPX $0678 = #$81 A:81 X:80 Y:D4 P:A5 SP:FB PPU:215, 68 CYC:7808 $D795:20 C9 F8 JSR $F8C9 A:81 X:80 Y:D4 P:NvUbdIzc SP:FB PPU:227, 68 CYC:7812 $F8C9:B0 05 BCS $F8D0 A:81 X:80 Y:D4 P:NvUbdIzc SP:F9 PPU:245, 68 CYC:7818 $F8CB:F0 03 BEQ $F8D0 A:81 X:80 Y:D4 P:NvUbdIzc SP:F9 PPU:251, 68 CYC:7820 $F8CD:10 01 BPL $F8D0 A:81 X:80 Y:D4 P:NvUbdIzc SP:F9 PPU:257, 68 CYC:7822 $F8CF:60 RTS A:81 X:80 Y:D4 P:NvUbdIzc SP:F9 PPU:263, 68 CYC:7824 $D798:C8 INY A:81 X:80 Y:D4 P:NvUbdIzc SP:FB PPU:281, 68 CYC:7830 $D799:A9 7F LDA #$7F A:81 X:80 Y:D5 P:NvUbdIzc SP:FB PPU:287, 68 CYC:7832 $D79B:8D 78 06 STA $0678 = #$81 A:7F X:80 Y:D5 P:nvUbdIzc SP:FB PPU:293, 68 CYC:7834 $D79E:EC 78 06 CPX $0678 = #$7F A:7F X:80 Y:D5 P:nvUbdIzc SP:FB PPU:305, 68 CYC:7838 $D7A1:20 D3 F8 JSR $F8D3 A:7F X:80 Y:D5 P:25 SP:FB PPU:317, 68 CYC:7842 $F8D3:90 05 BCC $F8DA A:7F X:80 Y:D5 P:25 SP:F9 PPU:335, 68 CYC:7848 $F8D5:F0 03 BEQ $F8DA A:7F X:80 Y:D5 P:25 SP:F9 PPU: 0, 69 CYC:7850 $F8D7:30 01 BMI $F8DA A:7F X:80 Y:D5 P:25 SP:F9 PPU: 6, 69 CYC:7852 $F8D9:60 RTS A:7F X:80 Y:D5 P:25 SP:F9 PPU: 12, 69 CYC:7854 $D7A4:C8 INY A:7F X:80 Y:D5 P:25 SP:FB PPU: 30, 69 CYC:7860 $D7A5:98 TYA A:7F X:80 Y:D6 P:A5 SP:FB PPU: 36, 69 CYC:7862 $D7A6:AA TAX A:D6 X:80 Y:D6 P:A5 SP:FB PPU: 42, 69 CYC:7864 $D7A7:A9 40 LDA #$40 A:D6 X:D6 Y:D6 P:A5 SP:FB PPU: 48, 69 CYC:7866 $D7A9:8D 78 06 STA $0678 = #$7F A:40 X:D6 Y:D6 P:25 SP:FB PPU: 54, 69 CYC:7868 $D7AC:20 DD F8 JSR $F8DD A:40 X:D6 Y:D6 P:25 SP:FB PPU: 66, 69 CYC:7872 $F8DD:24 01 BIT $01 = #$FF A:40 X:D6 Y:D6 P:25 SP:F9 PPU: 84, 69 CYC:7878 $F8DF:A0 40 LDY #$40 A:40 X:D6 Y:D6 P:E5 SP:F9 PPU: 93, 69 CYC:7881 $F8E1:60 RTS A:40 X:D6 Y:40 P:65 SP:F9 PPU: 99, 69 CYC:7883 $D7AF:CC 78 06 CPY $0678 = #$40 A:40 X:D6 Y:40 P:65 SP:FB PPU:117, 69 CYC:7889 $D7B2:20 E2 F8 JSR $F8E2 A:40 X:D6 Y:40 P:67 SP:FB PPU:129, 69 CYC:7893 $F8E2:30 07 BMI $F8EB A:40 X:D6 Y:40 P:67 SP:F9 PPU:147, 69 CYC:7899 $F8E4:90 05 BCC $F8EB A:40 X:D6 Y:40 P:67 SP:F9 PPU:153, 69 CYC:7901 $F8E6:D0 03 BNE $F8EB A:40 X:D6 Y:40 P:67 SP:F9 PPU:159, 69 CYC:7903 $F8E8:50 01 BVC $F8EB A:40 X:D6 Y:40 P:67 SP:F9 PPU:165, 69 CYC:7905 $F8EA:60 RTS A:40 X:D6 Y:40 P:67 SP:F9 PPU:171, 69 CYC:7907 $D7B5:E8 INX A:40 X:D6 Y:40 P:67 SP:FB PPU:189, 69 CYC:7913 $D7B6:A9 3F LDA #$3F A:40 X:D7 Y:40 P:E5 SP:FB PPU:195, 69 CYC:7915 $D7B8:8D 78 06 STA $0678 = #$40 A:3F X:D7 Y:40 P:65 SP:FB PPU:201, 69 CYC:7917 $D7BB:20 EE F8 JSR $F8EE A:3F X:D7 Y:40 P:65 SP:FB PPU:213, 69 CYC:7921 $F8EE:B8 CLV A:3F X:D7 Y:40 P:65 SP:F9 PPU:231, 69 CYC:7927 $F8EF:60 RTS A:3F X:D7 Y:40 P:25 SP:F9 PPU:237, 69 CYC:7929 $D7BE:CC 78 06 CPY $0678 = #$3F A:3F X:D7 Y:40 P:25 SP:FB PPU:255, 69 CYC:7935 $D7C1:20 F0 F8 JSR $F8F0 A:3F X:D7 Y:40 P:25 SP:FB PPU:267, 69 CYC:7939 $F8F0:F0 07 BEQ $F8F9 A:3F X:D7 Y:40 P:25 SP:F9 PPU:285, 69 CYC:7945 $F8F2:30 05 BMI $F8F9 A:3F X:D7 Y:40 P:25 SP:F9 PPU:291, 69 CYC:7947 $F8F4:90 03 BCC $F8F9 A:3F X:D7 Y:40 P:25 SP:F9 PPU:297, 69 CYC:7949 $F8F6:70 01 BVS $F8F9 A:3F X:D7 Y:40 P:25 SP:F9 PPU:303, 69 CYC:7951 $F8F8:60 RTS A:3F X:D7 Y:40 P:25 SP:F9 PPU:309, 69 CYC:7953 $D7C4:E8 INX A:3F X:D7 Y:40 P:25 SP:FB PPU:327, 69 CYC:7959 $D7C5:A9 41 LDA #$41 A:3F X:D8 Y:40 P:A5 SP:FB PPU:333, 69 CYC:7961 $D7C7:8D 78 06 STA $0678 = #$3F A:41 X:D8 Y:40 P:25 SP:FB PPU:339, 69 CYC:7963 $D7CA:CC 78 06 CPY $0678 = #$41 A:41 X:D8 Y:40 P:25 SP:FB PPU: 10, 70 CYC:7967 $D7CD:20 FC F8 JSR $F8FC A:41 X:D8 Y:40 P:NvUbdIzc SP:FB PPU: 22, 70 CYC:7971 $F8FC:F0 05 BEQ $F903 A:41 X:D8 Y:40 P:NvUbdIzc SP:F9 PPU: 40, 70 CYC:7977 $F8FE:10 03 BPL $F903 A:41 X:D8 Y:40 P:NvUbdIzc SP:F9 PPU: 46, 70 CYC:7979 $F900:10 01 BPL $F903 A:41 X:D8 Y:40 P:NvUbdIzc SP:F9 PPU: 52, 70 CYC:7981 $F902:60 RTS A:41 X:D8 Y:40 P:NvUbdIzc SP:F9 PPU: 58, 70 CYC:7983 $D7D0:E8 INX A:41 X:D8 Y:40 P:NvUbdIzc SP:FB PPU: 76, 70 CYC:7989 $D7D1:A9 00 LDA #$00 A:41 X:D9 Y:40 P:NvUbdIzc SP:FB PPU: 82, 70 CYC:7991 $D7D3:8D 78 06 STA $0678 = #$41 A:00 X:D9 Y:40 P:nvUbdIZc SP:FB PPU: 88, 70 CYC:7993 $D7D6:20 06 F9 JSR $F906 A:00 X:D9 Y:40 P:nvUbdIZc SP:FB PPU:100, 70 CYC:7997 $F906:A0 80 LDY #$80 A:00 X:D9 Y:40 P:nvUbdIZc SP:F9 PPU:118, 70 CYC:8003 $F908:60 RTS A:00 X:D9 Y:80 P:NvUbdIzc SP:F9 PPU:124, 70 CYC:8005 $D7D9:CC 78 06 CPY $0678 = #$00 A:00 X:D9 Y:80 P:NvUbdIzc SP:FB PPU:142, 70 CYC:8011 $D7DC:20 09 F9 JSR $F909 A:00 X:D9 Y:80 P:A5 SP:FB PPU:154, 70 CYC:8015 $F909:F0 05 BEQ $F910 A:00 X:D9 Y:80 P:A5 SP:F9 PPU:172, 70 CYC:8021 $F90B:10 03 BPL $F910 A:00 X:D9 Y:80 P:A5 SP:F9 PPU:178, 70 CYC:8023 $F90D:90 01 BCC $F910 A:00 X:D9 Y:80 P:A5 SP:F9 PPU:184, 70 CYC:8025 $F90F:60 RTS A:00 X:D9 Y:80 P:A5 SP:F9 PPU:190, 70 CYC:8027 $D7DF:E8 INX A:00 X:D9 Y:80 P:A5 SP:FB PPU:208, 70 CYC:8033 $D7E0:A9 80 LDA #$80 A:00 X:DA Y:80 P:A5 SP:FB PPU:214, 70 CYC:8035 $D7E2:8D 78 06 STA $0678 = #$00 A:80 X:DA Y:80 P:A5 SP:FB PPU:220, 70 CYC:8037 $D7E5:CC 78 06 CPY $0678 = #$80 A:80 X:DA Y:80 P:A5 SP:FB PPU:232, 70 CYC:8041 $D7E8:20 13 F9 JSR $F913 A:80 X:DA Y:80 P:nvUbdIZC SP:FB PPU:244, 70 CYC:8045 $F913:D0 05 BNE $F91A A:80 X:DA Y:80 P:nvUbdIZC SP:F9 PPU:262, 70 CYC:8051 $F915:30 03 BMI $F91A A:80 X:DA Y:80 P:nvUbdIZC SP:F9 PPU:268, 70 CYC:8053 $F917:90 01 BCC $F91A A:80 X:DA Y:80 P:nvUbdIZC SP:F9 PPU:274, 70 CYC:8055 $F919:60 RTS A:80 X:DA Y:80 P:nvUbdIZC SP:F9 PPU:280, 70 CYC:8057 $D7EB:E8 INX A:80 X:DA Y:80 P:nvUbdIZC SP:FB PPU:298, 70 CYC:8063 $D7EC:A9 81 LDA #$81 A:80 X:DB Y:80 P:A5 SP:FB PPU:304, 70 CYC:8065 $D7EE:8D 78 06 STA $0678 = #$80 A:81 X:DB Y:80 P:A5 SP:FB PPU:310, 70 CYC:8067 $D7F1:CC 78 06 CPY $0678 = #$81 A:81 X:DB Y:80 P:A5 SP:FB PPU:322, 70 CYC:8071 $D7F4:20 1D F9 JSR $F91D A:81 X:DB Y:80 P:NvUbdIzc SP:FB PPU:334, 70 CYC:8075 $F91D:B0 05 BCS $F924 A:81 X:DB Y:80 P:NvUbdIzc SP:F9 PPU: 11, 71 CYC:8081 $F91F:F0 03 BEQ $F924 A:81 X:DB Y:80 P:NvUbdIzc SP:F9 PPU: 17, 71 CYC:8083 $F921:10 01 BPL $F924 A:81 X:DB Y:80 P:NvUbdIzc SP:F9 PPU: 23, 71 CYC:8085 $F923:60 RTS A:81 X:DB Y:80 P:NvUbdIzc SP:F9 PPU: 29, 71 CYC:8087 $D7F7:E8 INX A:81 X:DB Y:80 P:NvUbdIzc SP:FB PPU: 47, 71 CYC:8093 $D7F8:A9 7F LDA #$7F A:81 X:DC Y:80 P:NvUbdIzc SP:FB PPU: 53, 71 CYC:8095 $D7FA:8D 78 06 STA $0678 = #$81 A:7F X:DC Y:80 P:nvUbdIzc SP:FB PPU: 59, 71 CYC:8097 $D7FD:CC 78 06 CPY $0678 = #$7F A:7F X:DC Y:80 P:nvUbdIzc SP:FB PPU: 71, 71 CYC:8101 $D800:20 27 F9 JSR $F927 A:7F X:DC Y:80 P:25 SP:FB PPU: 83, 71 CYC:8105 $F927:90 05 BCC $F92E A:7F X:DC Y:80 P:25 SP:F9 PPU:101, 71 CYC:8111 $F929:F0 03 BEQ $F92E A:7F X:DC Y:80 P:25 SP:F9 PPU:107, 71 CYC:8113 $F92B:30 01 BMI $F92E A:7F X:DC Y:80 P:25 SP:F9 PPU:113, 71 CYC:8115 $F92D:60 RTS A:7F X:DC Y:80 P:25 SP:F9 PPU:119, 71 CYC:8117 $D803:E8 INX A:7F X:DC Y:80 P:25 SP:FB PPU:137, 71 CYC:8123 $D804:8A TXA A:7F X:DD Y:80 P:A5 SP:FB PPU:143, 71 CYC:8125 $D805:A8 TAY A:DD X:DD Y:80 P:A5 SP:FB PPU:149, 71 CYC:8127 $D806:20 90 F9 JSR $F990 A:DD X:DD Y:DD P:A5 SP:FB PPU:155, 71 CYC:8129 $F990:A2 55 LDX #$55 A:DD X:DD Y:DD P:A5 SP:F9 PPU:173, 71 CYC:8135 $F992:A9 FF LDA #$FF A:DD X:55 Y:DD P:25 SP:F9 PPU:179, 71 CYC:8137 $F994:85 01 STA $01 = #$FF A:FF X:55 Y:DD P:A5 SP:F9 PPU:185, 71 CYC:8139 $F996:EA NOP A:FF X:55 Y:DD P:A5 SP:F9 PPU:194, 71 CYC:8142 $F997:24 01 BIT $01 = #$FF A:FF X:55 Y:DD P:A5 SP:F9 PPU:200, 71 CYC:8144 $F999:38 SEC A:FF X:55 Y:DD P:E5 SP:F9 PPU:209, 71 CYC:8147 $F99A:A9 01 LDA #$01 A:FF X:55 Y:DD P:E5 SP:F9 PPU:215, 71 CYC:8149 $F99C:60 RTS A:01 X:55 Y:DD P:65 SP:F9 PPU:221, 71 CYC:8151 $D809:8D 78 06 STA $0678 = #$7F A:01 X:55 Y:DD P:65 SP:FB PPU:239, 71 CYC:8157 $D80C:4E 78 06 LSR $0678 = #$01 A:01 X:55 Y:DD P:65 SP:FB PPU:251, 71 CYC:8161 $D80F:AD 78 06 LDA $0678 = #$00 A:01 X:55 Y:DD P:67 SP:FB PPU:269, 71 CYC:8167 $D812:20 9D F9 JSR $F99D A:00 X:55 Y:DD P:67 SP:FB PPU:281, 71 CYC:8171 $F99D:90 1B BCC $F9BA A:00 X:55 Y:DD P:67 SP:F9 PPU:299, 71 CYC:8177 $F99F:D0 19 BNE $F9BA A:00 X:55 Y:DD P:67 SP:F9 PPU:305, 71 CYC:8179 $F9A1:30 17 BMI $F9BA A:00 X:55 Y:DD P:67 SP:F9 PPU:311, 71 CYC:8181 $F9A3:50 15 BVC $F9BA A:00 X:55 Y:DD P:67 SP:F9 PPU:317, 71 CYC:8183 $F9A5:C9 00 CMP #$00 A:00 X:55 Y:DD P:67 SP:F9 PPU:323, 71 CYC:8185 $F9A7:D0 11 BNE $F9BA A:00 X:55 Y:DD P:67 SP:F9 PPU:329, 71 CYC:8187 $F9A9:B8 CLV A:00 X:55 Y:DD P:67 SP:F9 PPU:335, 71 CYC:8189 $F9AA:A9 AA LDA #$AA A:00 X:55 Y:DD P:nvUbdIZC SP:F9 PPU: 0, 72 CYC:8191 $F9AC:60 RTS A:AA X:55 Y:DD P:A5 SP:F9 PPU: 6, 72 CYC:8193 $D815:C8 INY A:AA X:55 Y:DD P:A5 SP:FB PPU: 24, 72 CYC:8199 $D816:8D 78 06 STA $0678 = #$00 A:AA X:55 Y:DE P:A5 SP:FB PPU: 30, 72 CYC:8201 $D819:4E 78 06 LSR $0678 = #$AA A:AA X:55 Y:DE P:A5 SP:FB PPU: 42, 72 CYC:8205 $D81C:AD 78 06 LDA $0678 = #$55 A:AA X:55 Y:DE P:nvUbdIzc SP:FB PPU: 60, 72 CYC:8211 $D81F:20 AD F9 JSR $F9AD A:55 X:55 Y:DE P:nvUbdIzc SP:FB PPU: 72, 72 CYC:8215 $F9AD:B0 0B BCS $F9BA A:55 X:55 Y:DE P:nvUbdIzc SP:F9 PPU: 90, 72 CYC:8221 $F9AF:F0 09 BEQ $F9BA A:55 X:55 Y:DE P:nvUbdIzc SP:F9 PPU: 96, 72 CYC:8223 $F9B1:30 07 BMI $F9BA A:55 X:55 Y:DE P:nvUbdIzc SP:F9 PPU:102, 72 CYC:8225 $F9B3:70 05 BVS $F9BA A:55 X:55 Y:DE P:nvUbdIzc SP:F9 PPU:108, 72 CYC:8227 $F9B5:C9 55 CMP #$55 A:55 X:55 Y:DE P:nvUbdIzc SP:F9 PPU:114, 72 CYC:8229 $F9B7:D0 01 BNE $F9BA A:55 X:55 Y:DE P:nvUbdIZC SP:F9 PPU:120, 72 CYC:8231 $F9B9:60 RTS A:55 X:55 Y:DE P:nvUbdIZC SP:F9 PPU:126, 72 CYC:8233 $D822:C8 INY A:55 X:55 Y:DE P:nvUbdIZC SP:FB PPU:144, 72 CYC:8239 $D823:20 BD F9 JSR $F9BD A:55 X:55 Y:DF P:A5 SP:FB PPU:150, 72 CYC:8241 $F9BD:24 01 BIT $01 = #$FF A:55 X:55 Y:DF P:A5 SP:F9 PPU:168, 72 CYC:8247 $F9BF:38 SEC A:55 X:55 Y:DF P:E5 SP:F9 PPU:177, 72 CYC:8250 $F9C0:A9 80 LDA #$80 A:55 X:55 Y:DF P:E5 SP:F9 PPU:183, 72 CYC:8252 $F9C2:60 RTS A:80 X:55 Y:DF P:E5 SP:F9 PPU:189, 72 CYC:8254 $D826:8D 78 06 STA $0678 = #$55 A:80 X:55 Y:DF P:E5 SP:FB PPU:207, 72 CYC:8260 $D829:0E 78 06 ASL $0678 = #$80 A:80 X:55 Y:DF P:E5 SP:FB PPU:219, 72 CYC:8264 $D82C:AD 78 06 LDA $0678 = #$00 A:80 X:55 Y:DF P:67 SP:FB PPU:237, 72 CYC:8270 $D82F:20 C3 F9 JSR $F9C3 A:00 X:55 Y:DF P:67 SP:FB PPU:249, 72 CYC:8274 $F9C3:90 1C BCC $F9E1 A:00 X:55 Y:DF P:67 SP:F9 PPU:267, 72 CYC:8280 $F9C5:D0 1A BNE $F9E1 A:00 X:55 Y:DF P:67 SP:F9 PPU:273, 72 CYC:8282 $F9C7:30 18 BMI $F9E1 A:00 X:55 Y:DF P:67 SP:F9 PPU:279, 72 CYC:8284 $F9C9:50 16 BVC $F9E1 A:00 X:55 Y:DF P:67 SP:F9 PPU:285, 72 CYC:8286 $F9CB:C9 00 CMP #$00 A:00 X:55 Y:DF P:67 SP:F9 PPU:291, 72 CYC:8288 $F9CD:D0 12 BNE $F9E1 A:00 X:55 Y:DF P:67 SP:F9 PPU:297, 72 CYC:8290 $F9CF:B8 CLV A:00 X:55 Y:DF P:67 SP:F9 PPU:303, 72 CYC:8292 $F9D0:A9 55 LDA #$55 A:00 X:55 Y:DF P:nvUbdIZC SP:F9 PPU:309, 72 CYC:8294 $F9D2:38 SEC A:55 X:55 Y:DF P:25 SP:F9 PPU:315, 72 CYC:8296 $F9D3:60 RTS A:55 X:55 Y:DF P:25 SP:F9 PPU:321, 72 CYC:8298 $D832:C8 INY A:55 X:55 Y:DF P:25 SP:FB PPU:339, 72 CYC:8304 $D833:8D 78 06 STA $0678 = #$00 A:55 X:55 Y:E0 P:A5 SP:FB PPU: 4, 73 CYC:8306 $D836:0E 78 06 ASL $0678 = #$55 A:55 X:55 Y:E0 P:A5 SP:FB PPU: 16, 73 CYC:8310 $D839:AD 78 06 LDA $0678 = #$AA A:55 X:55 Y:E0 P:NvUbdIzc SP:FB PPU: 34, 73 CYC:8316 $D83C:20 D4 F9 JSR $F9D4 A:AA X:55 Y:E0 P:NvUbdIzc SP:FB PPU: 46, 73 CYC:8320 $F9D4:B0 0B BCS $F9E1 A:AA X:55 Y:E0 P:NvUbdIzc SP:F9 PPU: 64, 73 CYC:8326 $F9D6:F0 09 BEQ $F9E1 A:AA X:55 Y:E0 P:NvUbdIzc SP:F9 PPU: 70, 73 CYC:8328 $F9D8:10 07 BPL $F9E1 A:AA X:55 Y:E0 P:NvUbdIzc SP:F9 PPU: 76, 73 CYC:8330 $F9DA:70 05 BVS $F9E1 A:AA X:55 Y:E0 P:NvUbdIzc SP:F9 PPU: 82, 73 CYC:8332 $F9DC:C9 AA CMP #$AA A:AA X:55 Y:E0 P:NvUbdIzc SP:F9 PPU: 88, 73 CYC:8334 $F9DE:D0 01 BNE $F9E1 A:AA X:55 Y:E0 P:nvUbdIZC SP:F9 PPU: 94, 73 CYC:8336 $F9E0:60 RTS A:AA X:55 Y:E0 P:nvUbdIZC SP:F9 PPU:100, 73 CYC:8338 $D83F:C8 INY A:AA X:55 Y:E0 P:nvUbdIZC SP:FB PPU:118, 73 CYC:8344 $D840:20 E4 F9 JSR $F9E4 A:AA X:55 Y:E1 P:A5 SP:FB PPU:124, 73 CYC:8346 $F9E4:24 01 BIT $01 = #$FF A:AA X:55 Y:E1 P:A5 SP:F9 PPU:142, 73 CYC:8352 $F9E6:38 SEC A:AA X:55 Y:E1 P:E5 SP:F9 PPU:151, 73 CYC:8355 $F9E7:A9 01 LDA #$01 A:AA X:55 Y:E1 P:E5 SP:F9 PPU:157, 73 CYC:8357 $F9E9:60 RTS A:01 X:55 Y:E1 P:65 SP:F9 PPU:163, 73 CYC:8359 $D843:8D 78 06 STA $0678 = #$AA A:01 X:55 Y:E1 P:65 SP:FB PPU:181, 73 CYC:8365 $D846:6E 78 06 ROR $0678 = #$01 A:01 X:55 Y:E1 P:65 SP:FB PPU:193, 73 CYC:8369 $D849:AD 78 06 LDA $0678 = #$80 A:01 X:55 Y:E1 P:E5 SP:FB PPU:211, 73 CYC:8375 $D84C:20 EA F9 JSR $F9EA A:80 X:55 Y:E1 P:E5 SP:FB PPU:223, 73 CYC:8379 $F9EA:90 1C BCC $FA08 A:80 X:55 Y:E1 P:E5 SP:F9 PPU:241, 73 CYC:8385 $F9EC:F0 1A BEQ $FA08 A:80 X:55 Y:E1 P:E5 SP:F9 PPU:247, 73 CYC:8387 $F9EE:10 18 BPL $FA08 A:80 X:55 Y:E1 P:E5 SP:F9 PPU:253, 73 CYC:8389 $F9F0:50 16 BVC $FA08 A:80 X:55 Y:E1 P:E5 SP:F9 PPU:259, 73 CYC:8391 $F9F2:C9 80 CMP #$80 A:80 X:55 Y:E1 P:E5 SP:F9 PPU:265, 73 CYC:8393 $F9F4:D0 12 BNE $FA08 A:80 X:55 Y:E1 P:67 SP:F9 PPU:271, 73 CYC:8395 $F9F6:B8 CLV A:80 X:55 Y:E1 P:67 SP:F9 PPU:277, 73 CYC:8397 $F9F7:18 CLC A:80 X:55 Y:E1 P:nvUbdIZC SP:F9 PPU:283, 73 CYC:8399 $F9F8:A9 55 LDA #$55 A:80 X:55 Y:E1 P:nvUbdIZc SP:F9 PPU:289, 73 CYC:8401 $F9FA:60 RTS A:55 X:55 Y:E1 P:nvUbdIzc SP:F9 PPU:295, 73 CYC:8403 $D84F:C8 INY A:55 X:55 Y:E1 P:nvUbdIzc SP:FB PPU:313, 73 CYC:8409 $D850:8D 78 06 STA $0678 = #$80 A:55 X:55 Y:E2 P:NvUbdIzc SP:FB PPU:319, 73 CYC:8411 $D853:6E 78 06 ROR $0678 = #$55 A:55 X:55 Y:E2 P:NvUbdIzc SP:FB PPU:331, 73 CYC:8415 $D856:AD 78 06 LDA $0678 = #$2A A:55 X:55 Y:E2 P:25 SP:FB PPU: 8, 74 CYC:8421 $D859:20 FB F9 JSR $F9FB A:2A X:55 Y:E2 P:25 SP:FB PPU: 20, 74 CYC:8425 $F9FB:90 0B BCC $FA08 A:2A X:55 Y:E2 P:25 SP:F9 PPU: 38, 74 CYC:8431 $F9FD:F0 09 BEQ $FA08 A:2A X:55 Y:E2 P:25 SP:F9 PPU: 44, 74 CYC:8433 $F9FF:30 07 BMI $FA08 A:2A X:55 Y:E2 P:25 SP:F9 PPU: 50, 74 CYC:8435 $FA01:70 05 BVS $FA08 A:2A X:55 Y:E2 P:25 SP:F9 PPU: 56, 74 CYC:8437 $FA03:C9 2A CMP #$2A A:2A X:55 Y:E2 P:25 SP:F9 PPU: 62, 74 CYC:8439 $FA05:D0 01 BNE $FA08 A:2A X:55 Y:E2 P:nvUbdIZC SP:F9 PPU: 68, 74 CYC:8441 $FA07:60 RTS A:2A X:55 Y:E2 P:nvUbdIZC SP:F9 PPU: 74, 74 CYC:8443 $D85C:C8 INY A:2A X:55 Y:E2 P:nvUbdIZC SP:FB PPU: 92, 74 CYC:8449 $D85D:20 0A FA JSR $FA0A A:2A X:55 Y:E3 P:A5 SP:FB PPU: 98, 74 CYC:8451 $FA0A:24 01 BIT $01 = #$FF A:2A X:55 Y:E3 P:A5 SP:F9 PPU:116, 74 CYC:8457 $FA0C:38 SEC A:2A X:55 Y:E3 P:E5 SP:F9 PPU:125, 74 CYC:8460 $FA0D:A9 80 LDA #$80 A:2A X:55 Y:E3 P:E5 SP:F9 PPU:131, 74 CYC:8462 $FA0F:60 RTS A:80 X:55 Y:E3 P:E5 SP:F9 PPU:137, 74 CYC:8464 $D860:8D 78 06 STA $0678 = #$2A A:80 X:55 Y:E3 P:E5 SP:FB PPU:155, 74 CYC:8470 $D863:2E 78 06 ROL $0678 = #$80 A:80 X:55 Y:E3 P:E5 SP:FB PPU:167, 74 CYC:8474 $D866:AD 78 06 LDA $0678 = #$01 A:80 X:55 Y:E3 P:65 SP:FB PPU:185, 74 CYC:8480 $D869:20 10 FA JSR $FA10 A:01 X:55 Y:E3 P:65 SP:FB PPU:197, 74 CYC:8484 $FA10:90 1C BCC $FA2E A:01 X:55 Y:E3 P:65 SP:F9 PPU:215, 74 CYC:8490 $FA12:F0 1A BEQ $FA2E A:01 X:55 Y:E3 P:65 SP:F9 PPU:221, 74 CYC:8492 $FA14:30 18 BMI $FA2E A:01 X:55 Y:E3 P:65 SP:F9 PPU:227, 74 CYC:8494 $FA16:50 16 BVC $FA2E A:01 X:55 Y:E3 P:65 SP:F9 PPU:233, 74 CYC:8496 $FA18:C9 01 CMP #$01 A:01 X:55 Y:E3 P:65 SP:F9 PPU:239, 74 CYC:8498 $FA1A:D0 12 BNE $FA2E A:01 X:55 Y:E3 P:67 SP:F9 PPU:245, 74 CYC:8500 $FA1C:B8 CLV A:01 X:55 Y:E3 P:67 SP:F9 PPU:251, 74 CYC:8502 $FA1D:18 CLC A:01 X:55 Y:E3 P:nvUbdIZC SP:F9 PPU:257, 74 CYC:8504 $FA1E:A9 55 LDA #$55 A:01 X:55 Y:E3 P:nvUbdIZc SP:F9 PPU:263, 74 CYC:8506 $FA20:60 RTS A:55 X:55 Y:E3 P:nvUbdIzc SP:F9 PPU:269, 74 CYC:8508 $D86C:C8 INY A:55 X:55 Y:E3 P:nvUbdIzc SP:FB PPU:287, 74 CYC:8514 $D86D:8D 78 06 STA $0678 = #$01 A:55 X:55 Y:E4 P:NvUbdIzc SP:FB PPU:293, 74 CYC:8516 $D870:2E 78 06 ROL $0678 = #$55 A:55 X:55 Y:E4 P:NvUbdIzc SP:FB PPU:305, 74 CYC:8520 $D873:AD 78 06 LDA $0678 = #$AA A:55 X:55 Y:E4 P:NvUbdIzc SP:FB PPU:323, 74 CYC:8526 $D876:20 21 FA JSR $FA21 A:AA X:55 Y:E4 P:NvUbdIzc SP:FB PPU:335, 74 CYC:8530 $FA21:B0 0B BCS $FA2E A:AA X:55 Y:E4 P:NvUbdIzc SP:F9 PPU: 12, 75 CYC:8536 $FA23:F0 09 BEQ $FA2E A:AA X:55 Y:E4 P:NvUbdIzc SP:F9 PPU: 18, 75 CYC:8538 $FA25:10 07 BPL $FA2E A:AA X:55 Y:E4 P:NvUbdIzc SP:F9 PPU: 24, 75 CYC:8540 $FA27:70 05 BVS $FA2E A:AA X:55 Y:E4 P:NvUbdIzc SP:F9 PPU: 30, 75 CYC:8542 $FA29:C9 AA CMP #$AA A:AA X:55 Y:E4 P:NvUbdIzc SP:F9 PPU: 36, 75 CYC:8544 $FA2B:D0 01 BNE $FA2E A:AA X:55 Y:E4 P:nvUbdIZC SP:F9 PPU: 42, 75 CYC:8546 $FA2D:60 RTS A:AA X:55 Y:E4 P:nvUbdIZC SP:F9 PPU: 48, 75 CYC:8548 $D879:A9 FF LDA #$FF A:AA X:55 Y:E4 P:nvUbdIZC SP:FB PPU: 66, 75 CYC:8554 $D87B:8D 78 06 STA $0678 = #$AA A:FF X:55 Y:E4 P:A5 SP:FB PPU: 72, 75 CYC:8556 $D87E:85 01 STA $01 = #$FF A:FF X:55 Y:E4 P:A5 SP:FB PPU: 84, 75 CYC:8560 $D880:24 01 BIT $01 = #$FF A:FF X:55 Y:E4 P:A5 SP:FB PPU: 93, 75 CYC:8563 $D882:38 SEC A:FF X:55 Y:E4 P:E5 SP:FB PPU:102, 75 CYC:8566 $D883:EE 78 06 INC $0678 = #$FF A:FF X:55 Y:E4 P:E5 SP:FB PPU:108, 75 CYC:8568 $D886:D0 0D BNE $D895 A:FF X:55 Y:E4 P:67 SP:FB PPU:126, 75 CYC:8574 $D888:30 0B BMI $D895 A:FF X:55 Y:E4 P:67 SP:FB PPU:132, 75 CYC:8576 $D88A:50 09 BVC $D895 A:FF X:55 Y:E4 P:67 SP:FB PPU:138, 75 CYC:8578 $D88C:90 07 BCC $D895 A:FF X:55 Y:E4 P:67 SP:FB PPU:144, 75 CYC:8580 $D88E:AD 78 06 LDA $0678 = #$00 A:FF X:55 Y:E4 P:67 SP:FB PPU:150, 75 CYC:8582 $D891:C9 00 CMP #$00 A:00 X:55 Y:E4 P:67 SP:FB PPU:162, 75 CYC:8586 $D893:F0 04 BEQ $D899 A:00 X:55 Y:E4 P:67 SP:FB PPU:168, 75 CYC:8588 $D899:A9 7F LDA #$7F A:00 X:55 Y:E4 P:67 SP:FB PPU:177, 75 CYC:8591 $D89B:8D 78 06 STA $0678 = #$00 A:7F X:55 Y:E4 P:65 SP:FB PPU:183, 75 CYC:8593 $D89E:B8 CLV A:7F X:55 Y:E4 P:65 SP:FB PPU:195, 75 CYC:8597 $D89F:18 CLC A:7F X:55 Y:E4 P:25 SP:FB PPU:201, 75 CYC:8599 $D8A0:EE 78 06 INC $0678 = #$7F A:7F X:55 Y:E4 P:nvUbdIzc SP:FB PPU:207, 75 CYC:8601 $D8A3:F0 0D BEQ $D8B2 A:7F X:55 Y:E4 P:NvUbdIzc SP:FB PPU:225, 75 CYC:8607 $D8A5:10 0B BPL $D8B2 A:7F X:55 Y:E4 P:NvUbdIzc SP:FB PPU:231, 75 CYC:8609 $D8A7:70 09 BVS $D8B2 A:7F X:55 Y:E4 P:NvUbdIzc SP:FB PPU:237, 75 CYC:8611 $D8A9:B0 07 BCS $D8B2 A:7F X:55 Y:E4 P:NvUbdIzc SP:FB PPU:243, 75 CYC:8613 $D8AB:AD 78 06 LDA $0678 = #$80 A:7F X:55 Y:E4 P:NvUbdIzc SP:FB PPU:249, 75 CYC:8615 $D8AE:C9 80 CMP #$80 A:80 X:55 Y:E4 P:NvUbdIzc SP:FB PPU:261, 75 CYC:8619 $D8B0:F0 04 BEQ $D8B6 A:80 X:55 Y:E4 P:nvUbdIZC SP:FB PPU:267, 75 CYC:8621 $D8B6:A9 00 LDA #$00 A:80 X:55 Y:E4 P:nvUbdIZC SP:FB PPU:276, 75 CYC:8624 $D8B8:8D 78 06 STA $0678 = #$80 A:00 X:55 Y:E4 P:nvUbdIZC SP:FB PPU:282, 75 CYC:8626 $D8BB:24 01 BIT $01 = #$FF A:00 X:55 Y:E4 P:nvUbdIZC SP:FB PPU:294, 75 CYC:8630 $D8BD:38 SEC A:00 X:55 Y:E4 P:E7 SP:FB PPU:303, 75 CYC:8633 $D8BE:CE 78 06 DEC $0678 = #$00 A:00 X:55 Y:E4 P:E7 SP:FB PPU:309, 75 CYC:8635 $D8C1:F0 0D BEQ $D8D0 A:00 X:55 Y:E4 P:E5 SP:FB PPU:327, 75 CYC:8641 $D8C3:10 0B BPL $D8D0 A:00 X:55 Y:E4 P:E5 SP:FB PPU:333, 75 CYC:8643 $D8C5:50 09 BVC $D8D0 A:00 X:55 Y:E4 P:E5 SP:FB PPU:339, 75 CYC:8645 $D8C7:90 07 BCC $D8D0 A:00 X:55 Y:E4 P:E5 SP:FB PPU: 4, 76 CYC:8647 $D8C9:AD 78 06 LDA $0678 = #$FF A:00 X:55 Y:E4 P:E5 SP:FB PPU: 10, 76 CYC:8649 $D8CC:C9 FF CMP #$FF A:FF X:55 Y:E4 P:E5 SP:FB PPU: 22, 76 CYC:8653 $D8CE:F0 04 BEQ $D8D4 A:FF X:55 Y:E4 P:67 SP:FB PPU: 28, 76 CYC:8655 $D8D4:A9 80 LDA #$80 A:FF X:55 Y:E4 P:67 SP:FB PPU: 37, 76 CYC:8658 $D8D6:8D 78 06 STA $0678 = #$FF A:80 X:55 Y:E4 P:E5 SP:FB PPU: 43, 76 CYC:8660 $D8D9:B8 CLV A:80 X:55 Y:E4 P:E5 SP:FB PPU: 55, 76 CYC:8664 $D8DA:18 CLC A:80 X:55 Y:E4 P:A5 SP:FB PPU: 61, 76 CYC:8666 $D8DB:CE 78 06 DEC $0678 = #$80 A:80 X:55 Y:E4 P:NvUbdIzc SP:FB PPU: 67, 76 CYC:8668 $D8DE:F0 0D BEQ $D8ED A:80 X:55 Y:E4 P:nvUbdIzc SP:FB PPU: 85, 76 CYC:8674 $D8E0:30 0B BMI $D8ED A:80 X:55 Y:E4 P:nvUbdIzc SP:FB PPU: 91, 76 CYC:8676 $D8E2:70 09 BVS $D8ED A:80 X:55 Y:E4 P:nvUbdIzc SP:FB PPU: 97, 76 CYC:8678 $D8E4:B0 07 BCS $D8ED A:80 X:55 Y:E4 P:nvUbdIzc SP:FB PPU:103, 76 CYC:8680 $D8E6:AD 78 06 LDA $0678 = #$7F A:80 X:55 Y:E4 P:nvUbdIzc SP:FB PPU:109, 76 CYC:8682 $D8E9:C9 7F CMP #$7F A:7F X:55 Y:E4 P:nvUbdIzc SP:FB PPU:121, 76 CYC:8686 $D8EB:F0 04 BEQ $D8F1 A:7F X:55 Y:E4 P:nvUbdIZC SP:FB PPU:127, 76 CYC:8688 $D8F1:A9 01 LDA #$01 A:7F X:55 Y:E4 P:nvUbdIZC SP:FB PPU:136, 76 CYC:8691 $D8F3:8D 78 06 STA $0678 = #$7F A:01 X:55 Y:E4 P:25 SP:FB PPU:142, 76 CYC:8693 $D8F6:CE 78 06 DEC $0678 = #$01 A:01 X:55 Y:E4 P:25 SP:FB PPU:154, 76 CYC:8697 $D8F9:F0 04 BEQ $D8FF A:01 X:55 Y:E4 P:nvUbdIZC SP:FB PPU:172, 76 CYC:8703 $D8FF:60 RTS A:01 X:55 Y:E4 P:nvUbdIZC SP:FB PPU:181, 76 CYC:8706 $C618:20 00 D9 JSR $D900 A:01 X:55 Y:E4 P:nvUbdIZC SP:FD PPU:199, 76 CYC:8712 $D900:A9 A3 LDA #$A3 A:01 X:55 Y:E4 P:nvUbdIZC SP:FB PPU:217, 76 CYC:8718 $D902:85 33 STA $33 = #$00 A:A3 X:55 Y:E4 P:A5 SP:FB PPU:223, 76 CYC:8720 $D904:A9 89 LDA #$89 A:A3 X:55 Y:E4 P:A5 SP:FB PPU:232, 76 CYC:8723 $D906:8D 00 03 STA $0300 = #$70 A:89 X:55 Y:E4 P:A5 SP:FB PPU:238, 76 CYC:8725 $D909:A9 12 LDA #$12 A:89 X:55 Y:E4 P:A5 SP:FB PPU:250, 76 CYC:8729 $D90B:8D 45 02 STA $0245 = #$00 A:12 X:55 Y:E4 P:25 SP:FB PPU:256, 76 CYC:8731 $D90E:A9 FF LDA #$FF A:12 X:55 Y:E4 P:25 SP:FB PPU:268, 76 CYC:8735 $D910:85 01 STA $01 = #$FF A:FF X:55 Y:E4 P:A5 SP:FB PPU:274, 76 CYC:8737 $D912:A2 65 LDX #$65 A:FF X:55 Y:E4 P:A5 SP:FB PPU:283, 76 CYC:8740 $D914:A9 00 LDA #$00 A:FF X:65 Y:E4 P:25 SP:FB PPU:289, 76 CYC:8742 $D916:85 89 STA $89 = #$00 A:00 X:65 Y:E4 P:nvUbdIZC SP:FB PPU:295, 76 CYC:8744 $D918:A9 03 LDA #$03 A:00 X:65 Y:E4 P:nvUbdIZC SP:FB PPU:304, 76 CYC:8747 $D91A:85 8A STA $8A = #$00 A:03 X:65 Y:E4 P:25 SP:FB PPU:310, 76 CYC:8749 $D91C:A0 00 LDY #$00 A:03 X:65 Y:E4 P:25 SP:FB PPU:319, 76 CYC:8752 $D91E:38 SEC A:03 X:65 Y:00 P:nvUbdIZC SP:FB PPU:325, 76 CYC:8754 $D91F:A9 00 LDA #$00 A:03 X:65 Y:00 P:nvUbdIZC SP:FB PPU:331, 76 CYC:8756 $D921:B8 CLV A:00 X:65 Y:00 P:nvUbdIZC SP:FB PPU:337, 76 CYC:8758 $D922:B1 89 LDA ($89),Y = #$0300 @ 0300 = #$89 A:00 X:65 Y:00 P:nvUbdIZC SP:FB PPU: 2, 77 CYC:8760 $D924:F0 0C BEQ $D932 A:89 X:65 Y:00 P:A5 SP:FB PPU: 17, 77 CYC:8765 $D926:90 0A BCC $D932 A:89 X:65 Y:00 P:A5 SP:FB PPU: 23, 77 CYC:8767 $D928:70 08 BVS $D932 A:89 X:65 Y:00 P:A5 SP:FB PPU: 29, 77 CYC:8769 $D92A:C9 89 CMP #$89 A:89 X:65 Y:00 P:A5 SP:FB PPU: 35, 77 CYC:8771 $D92C:D0 04 BNE $D932 A:89 X:65 Y:00 P:nvUbdIZC SP:FB PPU: 41, 77 CYC:8773 $D92E:E0 65 CPX #$65 A:89 X:65 Y:00 P:nvUbdIZC SP:FB PPU: 47, 77 CYC:8775 $D930:F0 04 BEQ $D936 A:89 X:65 Y:00 P:nvUbdIZC SP:FB PPU: 53, 77 CYC:8777 $D936:A9 FF LDA #$FF A:89 X:65 Y:00 P:nvUbdIZC SP:FB PPU: 62, 77 CYC:8780 $D938:85 97 STA $97 = #$00 A:FF X:65 Y:00 P:A5 SP:FB PPU: 68, 77 CYC:8782 $D93A:85 98 STA $98 = #$00 A:FF X:65 Y:00 P:A5 SP:FB PPU: 77, 77 CYC:8785 $D93C:24 98 BIT $98 = #$FF A:FF X:65 Y:00 P:A5 SP:FB PPU: 86, 77 CYC:8788 $D93E:A0 34 LDY #$34 A:FF X:65 Y:00 P:E5 SP:FB PPU: 95, 77 CYC:8791 $D940:B1 97 LDA ($97),Y = #$FFFF @ 0033 = #$A3 A:FF X:65 Y:34 P:65 SP:FB PPU:101, 77 CYC:8793 $D942:C9 A3 CMP #$A3 A:A3 X:65 Y:34 P:E5 SP:FB PPU:119, 77 CYC:8799 $D944:D0 02 BNE $D948 A:A3 X:65 Y:34 P:67 SP:FB PPU:125, 77 CYC:8801 $D946:B0 04 BCS $D94C A:A3 X:65 Y:34 P:67 SP:FB PPU:131, 77 CYC:8803 $D94C:A5 00 LDA $00 = #$00 A:A3 X:65 Y:34 P:67 SP:FB PPU:140, 77 CYC:8806 $D94E:48 PHA A:00 X:65 Y:34 P:67 SP:FB PPU:149, 77 CYC:8809 $D94F:A9 46 LDA #$46 A:00 X:65 Y:34 P:67 SP:FA PPU:158, 77 CYC:8812 $D951:85 FF STA $FF = #$00 A:46 X:65 Y:34 P:65 SP:FA PPU:164, 77 CYC:8814 $D953:A9 01 LDA #$01 A:46 X:65 Y:34 P:65 SP:FA PPU:173, 77 CYC:8817 $D955:85 00 STA $00 = #$00 A:01 X:65 Y:34 P:65 SP:FA PPU:179, 77 CYC:8819 $D957:A0 FF LDY #$FF A:01 X:65 Y:34 P:65 SP:FA PPU:188, 77 CYC:8822 $D959:B1 FF LDA ($FF),Y = #$0146 @ 0245 = #$12 A:01 X:65 Y:FF P:E5 SP:FA PPU:194, 77 CYC:8824 $D95B:C9 12 CMP #$12 A:12 X:65 Y:FF P:65 SP:FA PPU:212, 77 CYC:8830 $D95D:F0 04 BEQ $D963 A:12 X:65 Y:FF P:67 SP:FA PPU:218, 77 CYC:8832 $D963:68 PLA A:12 X:65 Y:FF P:67 SP:FA PPU:227, 77 CYC:8835 $D964:85 00 STA $00 = #$01 A:00 X:65 Y:FF P:67 SP:FB PPU:239, 77 CYC:8839 $D966:A2 ED LDX #$ED A:00 X:65 Y:FF P:67 SP:FB PPU:248, 77 CYC:8842 $D968:A9 00 LDA #$00 A:00 X:ED Y:FF P:E5 SP:FB PPU:254, 77 CYC:8844 $D96A:85 33 STA $33 = #$A3 A:00 X:ED Y:FF P:67 SP:FB PPU:260, 77 CYC:8846 $D96C:A9 04 LDA #$04 A:00 X:ED Y:FF P:67 SP:FB PPU:269, 77 CYC:8849 $D96E:85 34 STA $34 = #$00 A:04 X:ED Y:FF P:65 SP:FB PPU:275, 77 CYC:8851 $D970:A0 00 LDY #$00 A:04 X:ED Y:FF P:65 SP:FB PPU:284, 77 CYC:8854 $D972:18 CLC A:04 X:ED Y:00 P:67 SP:FB PPU:290, 77 CYC:8856 $D973:A9 FF LDA #$FF A:04 X:ED Y:00 P:nVUbdIZc SP:FB PPU:296, 77 CYC:8858 $D975:85 01 STA $01 = #$FF A:FF X:ED Y:00 P:NVUbdIzc SP:FB PPU:302, 77 CYC:8860 $D977:24 01 BIT $01 = #$FF A:FF X:ED Y:00 P:NVUbdIzc SP:FB PPU:311, 77 CYC:8863 $D979:A9 AA LDA #$AA A:FF X:ED Y:00 P:NVUbdIzc SP:FB PPU:320, 77 CYC:8866 $D97B:8D 00 04 STA $0400 = #$AD A:AA X:ED Y:00 P:NVUbdIzc SP:FB PPU:326, 77 CYC:8868 $D97E:A9 55 LDA #$55 A:AA X:ED Y:00 P:NVUbdIzc SP:FB PPU:338, 77 CYC:8872 $D980:11 33 ORA ($33),Y = #$0400 @ 0400 = #$AA A:55 X:ED Y:00 P:64 SP:FB PPU: 3, 78 CYC:8874 $D982:B0 08 BCS $D98C A:FF X:ED Y:00 P:NVUbdIzc SP:FB PPU: 18, 78 CYC:8879 $D984:10 06 BPL $D98C A:FF X:ED Y:00 P:NVUbdIzc SP:FB PPU: 24, 78 CYC:8881 $D986:C9 FF CMP #$FF A:FF X:ED Y:00 P:NVUbdIzc SP:FB PPU: 30, 78 CYC:8883 $D988:D0 02 BNE $D98C A:FF X:ED Y:00 P:67 SP:FB PPU: 36, 78 CYC:8885 $D98A:70 02 BVS $D98E A:FF X:ED Y:00 P:67 SP:FB PPU: 42, 78 CYC:8887 $D98E:E8 INX A:FF X:ED Y:00 P:67 SP:FB PPU: 51, 78 CYC:8890 $D98F:38 SEC A:FF X:EE Y:00 P:E5 SP:FB PPU: 57, 78 CYC:8892 $D990:B8 CLV A:FF X:EE Y:00 P:E5 SP:FB PPU: 63, 78 CYC:8894 $D991:A9 00 LDA #$00 A:FF X:EE Y:00 P:A5 SP:FB PPU: 69, 78 CYC:8896 $D993:11 33 ORA ($33),Y = #$0400 @ 0400 = #$AA A:00 X:EE Y:00 P:nvUbdIZC SP:FB PPU: 75, 78 CYC:8898 $D995:F0 06 BEQ $D99D A:AA X:EE Y:00 P:A5 SP:FB PPU: 90, 78 CYC:8903 $D997:70 04 BVS $D99D A:AA X:EE Y:00 P:A5 SP:FB PPU: 96, 78 CYC:8905 $D999:90 02 BCC $D99D A:AA X:EE Y:00 P:A5 SP:FB PPU:102, 78 CYC:8907 $D99B:30 02 BMI $D99F A:AA X:EE Y:00 P:A5 SP:FB PPU:108, 78 CYC:8909 $D99F:E8 INX A:AA X:EE Y:00 P:A5 SP:FB PPU:117, 78 CYC:8912 $D9A0:18 CLC A:AA X:EF Y:00 P:A5 SP:FB PPU:123, 78 CYC:8914 $D9A1:24 01 BIT $01 = #$FF A:AA X:EF Y:00 P:NvUbdIzc SP:FB PPU:129, 78 CYC:8916 $D9A3:A9 55 LDA #$55 A:AA X:EF Y:00 P:NVUbdIzc SP:FB PPU:138, 78 CYC:8919 $D9A5:31 33 AND ($33),Y = #$0400 @ 0400 = #$AA A:55 X:EF Y:00 P:64 SP:FB PPU:144, 78 CYC:8921 $D9A7:D0 06 BNE $D9AF A:00 X:EF Y:00 P:nVUbdIZc SP:FB PPU:159, 78 CYC:8926 $D9A9:50 04 BVC $D9AF A:00 X:EF Y:00 P:nVUbdIZc SP:FB PPU:165, 78 CYC:8928 $D9AB:B0 02 BCS $D9AF A:00 X:EF Y:00 P:nVUbdIZc SP:FB PPU:171, 78 CYC:8930 $D9AD:10 02 BPL $D9B1 A:00 X:EF Y:00 P:nVUbdIZc SP:FB PPU:177, 78 CYC:8932 $D9B1:E8 INX A:00 X:EF Y:00 P:nVUbdIZc SP:FB PPU:186, 78 CYC:8935 $D9B2:38 SEC A:00 X:F0 Y:00 P:NVUbdIzc SP:FB PPU:192, 78 CYC:8937 $D9B3:B8 CLV A:00 X:F0 Y:00 P:E5 SP:FB PPU:198, 78 CYC:8939 $D9B4:A9 EF LDA #$EF A:00 X:F0 Y:00 P:A5 SP:FB PPU:204, 78 CYC:8941 $D9B6:8D 00 04 STA $0400 = #$AA A:EF X:F0 Y:00 P:A5 SP:FB PPU:210, 78 CYC:8943 $D9B9:A9 F8 LDA #$F8 A:EF X:F0 Y:00 P:A5 SP:FB PPU:222, 78 CYC:8947 $D9BB:31 33 AND ($33),Y = #$0400 @ 0400 = #$EF A:F8 X:F0 Y:00 P:A5 SP:FB PPU:228, 78 CYC:8949 $D9BD:90 08 BCC $D9C7 A:E8 X:F0 Y:00 P:A5 SP:FB PPU:243, 78 CYC:8954 $D9BF:10 06 BPL $D9C7 A:E8 X:F0 Y:00 P:A5 SP:FB PPU:249, 78 CYC:8956 $D9C1:C9 E8 CMP #$E8 A:E8 X:F0 Y:00 P:A5 SP:FB PPU:255, 78 CYC:8958 $D9C3:D0 02 BNE $D9C7 A:E8 X:F0 Y:00 P:nvUbdIZC SP:FB PPU:261, 78 CYC:8960 $D9C5:50 02 BVC $D9C9 A:E8 X:F0 Y:00 P:nvUbdIZC SP:FB PPU:267, 78 CYC:8962 $D9C9:E8 INX A:E8 X:F0 Y:00 P:nvUbdIZC SP:FB PPU:276, 78 CYC:8965 $D9CA:18 CLC A:E8 X:F1 Y:00 P:A5 SP:FB PPU:282, 78 CYC:8967 $D9CB:24 01 BIT $01 = #$FF A:E8 X:F1 Y:00 P:NvUbdIzc SP:FB PPU:288, 78 CYC:8969 $D9CD:A9 AA LDA #$AA A:E8 X:F1 Y:00 P:NVUbdIzc SP:FB PPU:297, 78 CYC:8972 $D9CF:8D 00 04 STA $0400 = #$EF A:AA X:F1 Y:00 P:NVUbdIzc SP:FB PPU:303, 78 CYC:8974 $D9D2:A9 5F LDA #$5F A:AA X:F1 Y:00 P:NVUbdIzc SP:FB PPU:315, 78 CYC:8978 $D9D4:51 33 EOR ($33),Y = #$0400 @ 0400 = #$AA A:5F X:F1 Y:00 P:64 SP:FB PPU:321, 78 CYC:8980 $D9D6:B0 08 BCS $D9E0 A:F5 X:F1 Y:00 P:NVUbdIzc SP:FB PPU:336, 78 CYC:8985 $D9D8:10 06 BPL $D9E0 A:F5 X:F1 Y:00 P:NVUbdIzc SP:FB PPU: 1, 79 CYC:8987 $D9DA:C9 F5 CMP #$F5 A:F5 X:F1 Y:00 P:NVUbdIzc SP:FB PPU: 7, 79 CYC:8989 $D9DC:D0 02 BNE $D9E0 A:F5 X:F1 Y:00 P:67 SP:FB PPU: 13, 79 CYC:8991 $D9DE:70 02 BVS $D9E2 A:F5 X:F1 Y:00 P:67 SP:FB PPU: 19, 79 CYC:8993 $D9E2:E8 INX A:F5 X:F1 Y:00 P:67 SP:FB PPU: 28, 79 CYC:8996 $D9E3:38 SEC A:F5 X:F2 Y:00 P:E5 SP:FB PPU: 34, 79 CYC:8998 $D9E4:B8 CLV A:F5 X:F2 Y:00 P:E5 SP:FB PPU: 40, 79 CYC:9000 $D9E5:A9 70 LDA #$70 A:F5 X:F2 Y:00 P:A5 SP:FB PPU: 46, 79 CYC:9002 $D9E7:8D 00 04 STA $0400 = #$AA A:70 X:F2 Y:00 P:25 SP:FB PPU: 52, 79 CYC:9004 $D9EA:51 33 EOR ($33),Y = #$0400 @ 0400 = #$70 A:70 X:F2 Y:00 P:25 SP:FB PPU: 64, 79 CYC:9008 $D9EC:D0 06 BNE $D9F4 A:00 X:F2 Y:00 P:nvUbdIZC SP:FB PPU: 79, 79 CYC:9013 $D9EE:70 04 BVS $D9F4 A:00 X:F2 Y:00 P:nvUbdIZC SP:FB PPU: 85, 79 CYC:9015 $D9F0:90 02 BCC $D9F4 A:00 X:F2 Y:00 P:nvUbdIZC SP:FB PPU: 91, 79 CYC:9017 $D9F2:10 02 BPL $D9F6 A:00 X:F2 Y:00 P:nvUbdIZC SP:FB PPU: 97, 79 CYC:9019 $D9F6:E8 INX A:00 X:F2 Y:00 P:nvUbdIZC SP:FB PPU:106, 79 CYC:9022 $D9F7:18 CLC A:00 X:F3 Y:00 P:A5 SP:FB PPU:112, 79 CYC:9024 $D9F8:24 01 BIT $01 = #$FF A:00 X:F3 Y:00 P:NvUbdIzc SP:FB PPU:118, 79 CYC:9026 $D9FA:A9 69 LDA #$69 A:00 X:F3 Y:00 P:E6 SP:FB PPU:127, 79 CYC:9029 $D9FC:8D 00 04 STA $0400 = #$70 A:69 X:F3 Y:00 P:64 SP:FB PPU:133, 79 CYC:9031 $D9FF:A9 00 LDA #$00 A:69 X:F3 Y:00 P:64 SP:FB PPU:145, 79 CYC:9035 $DA01:71 33 ADC ($33),Y = #$0400 @ 0400 = #$69 A:00 X:F3 Y:00 P:nVUbdIZc SP:FB PPU:151, 79 CYC:9037 $DA03:30 08 BMI $DA0D A:69 X:F3 Y:00 P:nvUbdIzc SP:FB PPU:166, 79 CYC:9042 $DA05:B0 06 BCS $DA0D A:69 X:F3 Y:00 P:nvUbdIzc SP:FB PPU:172, 79 CYC:9044 $DA07:C9 69 CMP #$69 A:69 X:F3 Y:00 P:nvUbdIzc SP:FB PPU:178, 79 CYC:9046 $DA09:D0 02 BNE $DA0D A:69 X:F3 Y:00 P:nvUbdIZC SP:FB PPU:184, 79 CYC:9048 $DA0B:50 02 BVC $DA0F A:69 X:F3 Y:00 P:nvUbdIZC SP:FB PPU:190, 79 CYC:9050 $DA0F:E8 INX A:69 X:F3 Y:00 P:nvUbdIZC SP:FB PPU:199, 79 CYC:9053 $DA10:38 SEC A:69 X:F4 Y:00 P:A5 SP:FB PPU:205, 79 CYC:9055 $DA11:24 01 BIT $01 = #$FF A:69 X:F4 Y:00 P:A5 SP:FB PPU:211, 79 CYC:9057 $DA13:A9 00 LDA #$00 A:69 X:F4 Y:00 P:E5 SP:FB PPU:220, 79 CYC:9060 $DA15:71 33 ADC ($33),Y = #$0400 @ 0400 = #$69 A:00 X:F4 Y:00 P:67 SP:FB PPU:226, 79 CYC:9062 $DA17:30 08 BMI $DA21 A:6A X:F4 Y:00 P:nvUbdIzc SP:FB PPU:241, 79 CYC:9067 $DA19:B0 06 BCS $DA21 A:6A X:F4 Y:00 P:nvUbdIzc SP:FB PPU:247, 79 CYC:9069 $DA1B:C9 6A CMP #$6A A:6A X:F4 Y:00 P:nvUbdIzc SP:FB PPU:253, 79 CYC:9071 $DA1D:D0 02 BNE $DA21 A:6A X:F4 Y:00 P:nvUbdIZC SP:FB PPU:259, 79 CYC:9073 $DA1F:50 02 BVC $DA23 A:6A X:F4 Y:00 P:nvUbdIZC SP:FB PPU:265, 79 CYC:9075 $DA23:E8 INX A:6A X:F4 Y:00 P:nvUbdIZC SP:FB PPU:274, 79 CYC:9078 $DA24:38 SEC A:6A X:F5 Y:00 P:A5 SP:FB PPU:280, 79 CYC:9080 $DA25:B8 CLV A:6A X:F5 Y:00 P:A5 SP:FB PPU:286, 79 CYC:9082 $DA26:A9 7F LDA #$7F A:6A X:F5 Y:00 P:A5 SP:FB PPU:292, 79 CYC:9084 $DA28:8D 00 04 STA $0400 = #$69 A:7F X:F5 Y:00 P:25 SP:FB PPU:298, 79 CYC:9086 $DA2B:71 33 ADC ($33),Y = #$0400 @ 0400 = #$7F A:7F X:F5 Y:00 P:25 SP:FB PPU:310, 79 CYC:9090 $DA2D:10 08 BPL $DA37 A:FF X:F5 Y:00 P:NVUbdIzc SP:FB PPU:325, 79 CYC:9095 $DA2F:B0 06 BCS $DA37 A:FF X:F5 Y:00 P:NVUbdIzc SP:FB PPU:331, 79 CYC:9097 $DA31:C9 FF CMP #$FF A:FF X:F5 Y:00 P:NVUbdIzc SP:FB PPU:337, 79 CYC:9099 $DA33:D0 02 BNE $DA37 A:FF X:F5 Y:00 P:67 SP:FB PPU: 2, 80 CYC:9101 $DA35:70 02 BVS $DA39 A:FF X:F5 Y:00 P:67 SP:FB PPU: 8, 80 CYC:9103 $DA39:E8 INX A:FF X:F5 Y:00 P:67 SP:FB PPU: 17, 80 CYC:9106 $DA3A:18 CLC A:FF X:F6 Y:00 P:E5 SP:FB PPU: 23, 80 CYC:9108 $DA3B:24 01 BIT $01 = #$FF A:FF X:F6 Y:00 P:NVUbdIzc SP:FB PPU: 29, 80 CYC:9110 $DA3D:A9 80 LDA #$80 A:FF X:F6 Y:00 P:NVUbdIzc SP:FB PPU: 38, 80 CYC:9113 $DA3F:8D 00 04 STA $0400 = #$7F A:80 X:F6 Y:00 P:NVUbdIzc SP:FB PPU: 44, 80 CYC:9115 $DA42:A9 7F LDA #$7F A:80 X:F6 Y:00 P:NVUbdIzc SP:FB PPU: 56, 80 CYC:9119 $DA44:71 33 ADC ($33),Y = #$0400 @ 0400 = #$80 A:7F X:F6 Y:00 P:64 SP:FB PPU: 62, 80 CYC:9121 $DA46:10 08 BPL $DA50 A:FF X:F6 Y:00 P:NvUbdIzc SP:FB PPU: 77, 80 CYC:9126 $DA48:B0 06 BCS $DA50 A:FF X:F6 Y:00 P:NvUbdIzc SP:FB PPU: 83, 80 CYC:9128 $DA4A:C9 FF CMP #$FF A:FF X:F6 Y:00 P:NvUbdIzc SP:FB PPU: 89, 80 CYC:9130 $DA4C:D0 02 BNE $DA50 A:FF X:F6 Y:00 P:nvUbdIZC SP:FB PPU: 95, 80 CYC:9132 $DA4E:50 02 BVC $DA52 A:FF X:F6 Y:00 P:nvUbdIZC SP:FB PPU:101, 80 CYC:9134 $DA52:E8 INX A:FF X:F6 Y:00 P:nvUbdIZC SP:FB PPU:110, 80 CYC:9137 $DA53:38 SEC A:FF X:F7 Y:00 P:A5 SP:FB PPU:116, 80 CYC:9139 $DA54:B8 CLV A:FF X:F7 Y:00 P:A5 SP:FB PPU:122, 80 CYC:9141 $DA55:A9 80 LDA #$80 A:FF X:F7 Y:00 P:A5 SP:FB PPU:128, 80 CYC:9143 $DA57:8D 00 04 STA $0400 = #$80 A:80 X:F7 Y:00 P:A5 SP:FB PPU:134, 80 CYC:9145 $DA5A:A9 7F LDA #$7F A:80 X:F7 Y:00 P:A5 SP:FB PPU:146, 80 CYC:9149 $DA5C:71 33 ADC ($33),Y = #$0400 @ 0400 = #$80 A:7F X:F7 Y:00 P:25 SP:FB PPU:152, 80 CYC:9151 $DA5E:D0 06 BNE $DA66 A:00 X:F7 Y:00 P:nvUbdIZC SP:FB PPU:167, 80 CYC:9156 $DA60:30 04 BMI $DA66 A:00 X:F7 Y:00 P:nvUbdIZC SP:FB PPU:173, 80 CYC:9158 $DA62:70 02 BVS $DA66 A:00 X:F7 Y:00 P:nvUbdIZC SP:FB PPU:179, 80 CYC:9160 $DA64:B0 02 BCS $DA68 A:00 X:F7 Y:00 P:nvUbdIZC SP:FB PPU:185, 80 CYC:9162 $DA68:E8 INX A:00 X:F7 Y:00 P:nvUbdIZC SP:FB PPU:194, 80 CYC:9165 $DA69:24 01 BIT $01 = #$FF A:00 X:F8 Y:00 P:A5 SP:FB PPU:200, 80 CYC:9167 $DA6B:A9 40 LDA #$40 A:00 X:F8 Y:00 P:E7 SP:FB PPU:209, 80 CYC:9170 $DA6D:8D 00 04 STA $0400 = #$80 A:40 X:F8 Y:00 P:65 SP:FB PPU:215, 80 CYC:9172 $DA70:D1 33 CMP ($33),Y = #$0400 @ 0400 = #$40 A:40 X:F8 Y:00 P:65 SP:FB PPU:227, 80 CYC:9176 $DA72:30 06 BMI $DA7A A:40 X:F8 Y:00 P:67 SP:FB PPU:242, 80 CYC:9181 $DA74:90 04 BCC $DA7A A:40 X:F8 Y:00 P:67 SP:FB PPU:248, 80 CYC:9183 $DA76:D0 02 BNE $DA7A A:40 X:F8 Y:00 P:67 SP:FB PPU:254, 80 CYC:9185 $DA78:70 02 BVS $DA7C A:40 X:F8 Y:00 P:67 SP:FB PPU:260, 80 CYC:9187 $DA7C:E8 INX A:40 X:F8 Y:00 P:67 SP:FB PPU:269, 80 CYC:9190 $DA7D:B8 CLV A:40 X:F9 Y:00 P:E5 SP:FB PPU:275, 80 CYC:9192 $DA7E:CE 00 04 DEC $0400 = #$40 A:40 X:F9 Y:00 P:A5 SP:FB PPU:281, 80 CYC:9194 $DA81:D1 33 CMP ($33),Y = #$0400 @ 0400 = #$3F A:40 X:F9 Y:00 P:25 SP:FB PPU:299, 80 CYC:9200 $DA83:F0 06 BEQ $DA8B A:40 X:F9 Y:00 P:25 SP:FB PPU:314, 80 CYC:9205 $DA85:30 04 BMI $DA8B A:40 X:F9 Y:00 P:25 SP:FB PPU:320, 80 CYC:9207 $DA87:90 02 BCC $DA8B A:40 X:F9 Y:00 P:25 SP:FB PPU:326, 80 CYC:9209 $DA89:50 02 BVC $DA8D A:40 X:F9 Y:00 P:25 SP:FB PPU:332, 80 CYC:9211 $DA8D:E8 INX A:40 X:F9 Y:00 P:25 SP:FB PPU: 0, 81 CYC:9214 $DA8E:EE 00 04 INC $0400 = #$3F A:40 X:FA Y:00 P:A5 SP:FB PPU: 6, 81 CYC:9216 $DA91:EE 00 04 INC $0400 = #$40 A:40 X:FA Y:00 P:25 SP:FB PPU: 24, 81 CYC:9222 $DA94:D1 33 CMP ($33),Y = #$0400 @ 0400 = #$41 A:40 X:FA Y:00 P:25 SP:FB PPU: 42, 81 CYC:9228 $DA96:F0 02 BEQ $DA9A A:40 X:FA Y:00 P:NvUbdIzc SP:FB PPU: 57, 81 CYC:9233 $DA98:30 02 BMI $DA9C A:40 X:FA Y:00 P:NvUbdIzc SP:FB PPU: 63, 81 CYC:9235 $DA9C:E8 INX A:40 X:FA Y:00 P:NvUbdIzc SP:FB PPU: 72, 81 CYC:9238 $DA9D:A9 00 LDA #$00 A:40 X:FB Y:00 P:NvUbdIzc SP:FB PPU: 78, 81 CYC:9240 $DA9F:8D 00 04 STA $0400 = #$41 A:00 X:FB Y:00 P:nvUbdIZc SP:FB PPU: 84, 81 CYC:9242 $DAA2:A9 80 LDA #$80 A:00 X:FB Y:00 P:nvUbdIZc SP:FB PPU: 96, 81 CYC:9246 $DAA4:D1 33 CMP ($33),Y = #$0400 @ 0400 = #$00 A:80 X:FB Y:00 P:NvUbdIzc SP:FB PPU:102, 81 CYC:9248 $DAA6:F0 04 BEQ $DAAC A:80 X:FB Y:00 P:A5 SP:FB PPU:117, 81 CYC:9253 $DAA8:10 02 BPL $DAAC A:80 X:FB Y:00 P:A5 SP:FB PPU:123, 81 CYC:9255 $DAAA:B0 02 BCS $DAAE A:80 X:FB Y:00 P:A5 SP:FB PPU:129, 81 CYC:9257 $DAAE:E8 INX A:80 X:FB Y:00 P:A5 SP:FB PPU:138, 81 CYC:9260 $DAAF:A0 80 LDY #$80 A:80 X:FC Y:00 P:A5 SP:FB PPU:144, 81 CYC:9262 $DAB1:8C 00 04 STY $0400 = #$00 A:80 X:FC Y:80 P:A5 SP:FB PPU:150, 81 CYC:9264 $DAB4:A0 00 LDY #$00 A:80 X:FC Y:80 P:A5 SP:FB PPU:162, 81 CYC:9268 $DAB6:D1 33 CMP ($33),Y = #$0400 @ 0400 = #$80 A:80 X:FC Y:00 P:nvUbdIZC SP:FB PPU:168, 81 CYC:9270 $DAB8:D0 04 BNE $DABE A:80 X:FC Y:00 P:nvUbdIZC SP:FB PPU:183, 81 CYC:9275 $DABA:30 02 BMI $DABE A:80 X:FC Y:00 P:nvUbdIZC SP:FB PPU:189, 81 CYC:9277 $DABC:B0 02 BCS $DAC0 A:80 X:FC Y:00 P:nvUbdIZC SP:FB PPU:195, 81 CYC:9279 $DAC0:E8 INX A:80 X:FC Y:00 P:nvUbdIZC SP:FB PPU:204, 81 CYC:9282 $DAC1:EE 00 04 INC $0400 = #$80 A:80 X:FD Y:00 P:A5 SP:FB PPU:210, 81 CYC:9284 $DAC4:D1 33 CMP ($33),Y = #$0400 @ 0400 = #$81 A:80 X:FD Y:00 P:A5 SP:FB PPU:228, 81 CYC:9290 $DAC6:B0 04 BCS $DACC A:80 X:FD Y:00 P:NvUbdIzc SP:FB PPU:243, 81 CYC:9295 $DAC8:F0 02 BEQ $DACC A:80 X:FD Y:00 P:NvUbdIzc SP:FB PPU:249, 81 CYC:9297 $DACA:30 02 BMI $DACE A:80 X:FD Y:00 P:NvUbdIzc SP:FB PPU:255, 81 CYC:9299 $DACE:E8 INX A:80 X:FD Y:00 P:NvUbdIzc SP:FB PPU:264, 81 CYC:9302 $DACF:CE 00 04 DEC $0400 = #$81 A:80 X:FE Y:00 P:NvUbdIzc SP:FB PPU:270, 81 CYC:9304 $DAD2:CE 00 04 DEC $0400 = #$80 A:80 X:FE Y:00 P:NvUbdIzc SP:FB PPU:288, 81 CYC:9310 $DAD5:D1 33 CMP ($33),Y = #$0400 @ 0400 = #$7F A:80 X:FE Y:00 P:nvUbdIzc SP:FB PPU:306, 81 CYC:9316 $DAD7:90 04 BCC $DADD A:80 X:FE Y:00 P:25 SP:FB PPU:321, 81 CYC:9321 $DAD9:F0 02 BEQ $DADD A:80 X:FE Y:00 P:25 SP:FB PPU:327, 81 CYC:9323 $DADB:10 02 BPL $DADF A:80 X:FE Y:00 P:25 SP:FB PPU:333, 81 CYC:9325 $DADF:60 RTS A:80 X:FE Y:00 P:25 SP:FB PPU: 1, 82 CYC:9328 $C61B:A5 00 LDA $00 = #$00 A:80 X:FE Y:00 P:25 SP:FD PPU: 19, 82 CYC:9334 $C61D:85 10 STA $10 = #$00 A:00 X:FE Y:00 P:nvUbdIZC SP:FD PPU: 28, 82 CYC:9337 $C61F:A9 00 LDA #$00 A:00 X:FE Y:00 P:nvUbdIZC SP:FD PPU: 37, 82 CYC:9340 $C621:85 00 STA $00 = #$00 A:00 X:FE Y:00 P:nvUbdIZC SP:FD PPU: 43, 82 CYC:9342 $C623:20 E0 DA JSR $DAE0 A:00 X:FE Y:00 P:nvUbdIZC SP:FD PPU: 52, 82 CYC:9345 $DAE0:A9 00 LDA #$00 A:00 X:FE Y:00 P:nvUbdIZC SP:FB PPU: 70, 82 CYC:9351 $DAE2:85 33 STA $33 = #$00 A:00 X:FE Y:00 P:nvUbdIZC SP:FB PPU: 76, 82 CYC:9353 $DAE4:A9 04 LDA #$04 A:00 X:FE Y:00 P:nvUbdIZC SP:FB PPU: 85, 82 CYC:9356 $DAE6:85 34 STA $34 = #$04 A:04 X:FE Y:00 P:25 SP:FB PPU: 91, 82 CYC:9358 $DAE8:A0 00 LDY #$00 A:04 X:FE Y:00 P:25 SP:FB PPU:100, 82 CYC:9361 $DAEA:A2 01 LDX #$01 A:04 X:FE Y:00 P:nvUbdIZC SP:FB PPU:106, 82 CYC:9363 $DAEC:24 01 BIT $01 = #$FF A:04 X:01 Y:00 P:25 SP:FB PPU:112, 82 CYC:9365 $DAEE:A9 40 LDA #$40 A:04 X:01 Y:00 P:E5 SP:FB PPU:121, 82 CYC:9368 $DAF0:8D 00 04 STA $0400 = #$7F A:40 X:01 Y:00 P:65 SP:FB PPU:127, 82 CYC:9370 $DAF3:38 SEC A:40 X:01 Y:00 P:65 SP:FB PPU:139, 82 CYC:9374 $DAF4:F1 33 SBC ($33),Y = #$0400 @ 0400 = #$40 A:40 X:01 Y:00 P:65 SP:FB PPU:145, 82 CYC:9376 $DAF6:30 0A BMI $DB02 A:00 X:01 Y:00 P:nvUbdIZC SP:FB PPU:160, 82 CYC:9381 $DAF8:90 08 BCC $DB02 A:00 X:01 Y:00 P:nvUbdIZC SP:FB PPU:166, 82 CYC:9383 $DAFA:D0 06 BNE $DB02 A:00 X:01 Y:00 P:nvUbdIZC SP:FB PPU:172, 82 CYC:9385 $DAFC:70 04 BVS $DB02 A:00 X:01 Y:00 P:nvUbdIZC SP:FB PPU:178, 82 CYC:9387 $DAFE:C9 00 CMP #$00 A:00 X:01 Y:00 P:nvUbdIZC SP:FB PPU:184, 82 CYC:9389 $DB00:F0 02 BEQ $DB04 A:00 X:01 Y:00 P:nvUbdIZC SP:FB PPU:190, 82 CYC:9391 $DB04:E8 INX A:00 X:01 Y:00 P:nvUbdIZC SP:FB PPU:199, 82 CYC:9394 $DB05:B8 CLV A:00 X:02 Y:00 P:25 SP:FB PPU:205, 82 CYC:9396 $DB06:38 SEC A:00 X:02 Y:00 P:25 SP:FB PPU:211, 82 CYC:9398 $DB07:A9 40 LDA #$40 A:00 X:02 Y:00 P:25 SP:FB PPU:217, 82 CYC:9400 $DB09:CE 00 04 DEC $0400 = #$40 A:40 X:02 Y:00 P:25 SP:FB PPU:223, 82 CYC:9402 $DB0C:F1 33 SBC ($33),Y = #$0400 @ 0400 = #$3F A:40 X:02 Y:00 P:25 SP:FB PPU:241, 82 CYC:9408 $DB0E:F0 0A BEQ $DB1A A:01 X:02 Y:00 P:25 SP:FB PPU:256, 82 CYC:9413 $DB10:30 08 BMI $DB1A A:01 X:02 Y:00 P:25 SP:FB PPU:262, 82 CYC:9415 $DB12:90 06 BCC $DB1A A:01 X:02 Y:00 P:25 SP:FB PPU:268, 82 CYC:9417 $DB14:70 04 BVS $DB1A A:01 X:02 Y:00 P:25 SP:FB PPU:274, 82 CYC:9419 $DB16:C9 01 CMP #$01 A:01 X:02 Y:00 P:25 SP:FB PPU:280, 82 CYC:9421 $DB18:F0 02 BEQ $DB1C A:01 X:02 Y:00 P:nvUbdIZC SP:FB PPU:286, 82 CYC:9423 $DB1C:E8 INX A:01 X:02 Y:00 P:nvUbdIZC SP:FB PPU:295, 82 CYC:9426 $DB1D:A9 40 LDA #$40 A:01 X:03 Y:00 P:25 SP:FB PPU:301, 82 CYC:9428 $DB1F:38 SEC A:40 X:03 Y:00 P:25 SP:FB PPU:307, 82 CYC:9430 $DB20:24 01 BIT $01 = #$FF A:40 X:03 Y:00 P:25 SP:FB PPU:313, 82 CYC:9432 $DB22:EE 00 04 INC $0400 = #$3F A:40 X:03 Y:00 P:E5 SP:FB PPU:322, 82 CYC:9435 $DB25:EE 00 04 INC $0400 = #$40 A:40 X:03 Y:00 P:65 SP:FB PPU:340, 82 CYC:9441 $DB28:F1 33 SBC ($33),Y = #$0400 @ 0400 = #$41 A:40 X:03 Y:00 P:65 SP:FB PPU: 17, 83 CYC:9447 $DB2A:B0 0A BCS $DB36 A:FF X:03 Y:00 P:NvUbdIzc SP:FB PPU: 32, 83 CYC:9452 $DB2C:F0 08 BEQ $DB36 A:FF X:03 Y:00 P:NvUbdIzc SP:FB PPU: 38, 83 CYC:9454 $DB2E:10 06 BPL $DB36 A:FF X:03 Y:00 P:NvUbdIzc SP:FB PPU: 44, 83 CYC:9456 $DB30:70 04 BVS $DB36 A:FF X:03 Y:00 P:NvUbdIzc SP:FB PPU: 50, 83 CYC:9458 $DB32:C9 FF CMP #$FF A:FF X:03 Y:00 P:NvUbdIzc SP:FB PPU: 56, 83 CYC:9460 $DB34:F0 02 BEQ $DB38 A:FF X:03 Y:00 P:nvUbdIZC SP:FB PPU: 62, 83 CYC:9462 $DB38:E8 INX A:FF X:03 Y:00 P:nvUbdIZC SP:FB PPU: 71, 83 CYC:9465 $DB39:18 CLC A:FF X:04 Y:00 P:25 SP:FB PPU: 77, 83 CYC:9467 $DB3A:A9 00 LDA #$00 A:FF X:04 Y:00 P:nvUbdIzc SP:FB PPU: 83, 83 CYC:9469 $DB3C:8D 00 04 STA $0400 = #$41 A:00 X:04 Y:00 P:nvUbdIZc SP:FB PPU: 89, 83 CYC:9471 $DB3F:A9 80 LDA #$80 A:00 X:04 Y:00 P:nvUbdIZc SP:FB PPU:101, 83 CYC:9475 $DB41:F1 33 SBC ($33),Y = #$0400 @ 0400 = #$00 A:80 X:04 Y:00 P:NvUbdIzc SP:FB PPU:107, 83 CYC:9477 $DB43:90 04 BCC $DB49 A:7F X:04 Y:00 P:65 SP:FB PPU:122, 83 CYC:9482 $DB45:C9 7F CMP #$7F A:7F X:04 Y:00 P:65 SP:FB PPU:128, 83 CYC:9484 $DB47:F0 02 BEQ $DB4B A:7F X:04 Y:00 P:67 SP:FB PPU:134, 83 CYC:9486 $DB4B:E8 INX A:7F X:04 Y:00 P:67 SP:FB PPU:143, 83 CYC:9489 $DB4C:38 SEC A:7F X:05 Y:00 P:65 SP:FB PPU:149, 83 CYC:9491 $DB4D:A9 7F LDA #$7F A:7F X:05 Y:00 P:65 SP:FB PPU:155, 83 CYC:9493 $DB4F:8D 00 04 STA $0400 = #$00 A:7F X:05 Y:00 P:65 SP:FB PPU:161, 83 CYC:9495 $DB52:A9 81 LDA #$81 A:7F X:05 Y:00 P:65 SP:FB PPU:173, 83 CYC:9499 $DB54:F1 33 SBC ($33),Y = #$0400 @ 0400 = #$7F A:81 X:05 Y:00 P:E5 SP:FB PPU:179, 83 CYC:9501 $DB56:50 06 BVC $DB5E A:02 X:05 Y:00 P:65 SP:FB PPU:194, 83 CYC:9506 $DB58:90 04 BCC $DB5E A:02 X:05 Y:00 P:65 SP:FB PPU:200, 83 CYC:9508 $DB5A:C9 02 CMP #$02 A:02 X:05 Y:00 P:65 SP:FB PPU:206, 83 CYC:9510 $DB5C:F0 02 BEQ $DB60 A:02 X:05 Y:00 P:67 SP:FB PPU:212, 83 CYC:9512 $DB60:E8 INX A:02 X:05 Y:00 P:67 SP:FB PPU:221, 83 CYC:9515 $DB61:A9 00 LDA #$00 A:02 X:06 Y:00 P:65 SP:FB PPU:227, 83 CYC:9517 $DB63:A9 87 LDA #$87 A:00 X:06 Y:00 P:67 SP:FB PPU:233, 83 CYC:9519 $DB65:91 33 STA ($33),Y = #$0400 @ 0400 = #$7F A:87 X:06 Y:00 P:E5 SP:FB PPU:239, 83 CYC:9521 $DB67:AD 00 04 LDA $0400 = #$87 A:87 X:06 Y:00 P:E5 SP:FB PPU:257, 83 CYC:9527 $DB6A:C9 87 CMP #$87 A:87 X:06 Y:00 P:E5 SP:FB PPU:269, 83 CYC:9531 $DB6C:F0 02 BEQ $DB70 A:87 X:06 Y:00 P:67 SP:FB PPU:275, 83 CYC:9533 $DB70:E8 INX A:87 X:06 Y:00 P:67 SP:FB PPU:284, 83 CYC:9536 $DB71:A9 7E LDA #$7E A:87 X:07 Y:00 P:65 SP:FB PPU:290, 83 CYC:9538 $DB73:8D 00 02 STA $0200 = #$7F A:7E X:07 Y:00 P:65 SP:FB PPU:296, 83 CYC:9540 $DB76:A9 DB LDA #$DB A:7E X:07 Y:00 P:65 SP:FB PPU:308, 83 CYC:9544 $DB78:8D 01 02 STA $0201 = #$00 A:DB X:07 Y:00 P:E5 SP:FB PPU:314, 83 CYC:9546 $DB7B:6C 00 02 JMP ($0200) = #$DB7E A:DB X:07 Y:00 P:E5 SP:FB PPU:326, 83 CYC:9550 $DB7E:A9 00 LDA #$00 A:DB X:07 Y:00 P:E5 SP:FB PPU: 0, 84 CYC:9555 $DB80:8D FF 02 STA $02FF = #$00 A:00 X:07 Y:00 P:67 SP:FB PPU: 6, 84 CYC:9557 $DB83:A9 01 LDA #$01 A:00 X:07 Y:00 P:67 SP:FB PPU: 18, 84 CYC:9561 $DB85:8D 00 03 STA $0300 = #$89 A:01 X:07 Y:00 P:65 SP:FB PPU: 24, 84 CYC:9563 $DB88:A9 03 LDA #$03 A:01 X:07 Y:00 P:65 SP:FB PPU: 36, 84 CYC:9567 $DB8A:8D 00 02 STA $0200 = #$7E A:03 X:07 Y:00 P:65 SP:FB PPU: 42, 84 CYC:9569 $DB8D:A9 A9 LDA #$A9 A:03 X:07 Y:00 P:65 SP:FB PPU: 54, 84 CYC:9573 $DB8F:8D 00 01 STA $0100 = #$00 A:A9 X:07 Y:00 P:E5 SP:FB PPU: 60, 84 CYC:9575 $DB92:A9 55 LDA #$55 A:A9 X:07 Y:00 P:E5 SP:FB PPU: 72, 84 CYC:9579 $DB94:8D 01 01 STA $0101 = #$00 A:55 X:07 Y:00 P:65 SP:FB PPU: 78, 84 CYC:9581 $DB97:A9 60 LDA #$60 A:55 X:07 Y:00 P:65 SP:FB PPU: 90, 84 CYC:9585 $DB99:8D 02 01 STA $0102 = #$00 A:60 X:07 Y:00 P:65 SP:FB PPU: 96, 84 CYC:9587 $DB9C:A9 A9 LDA #$A9 A:60 X:07 Y:00 P:65 SP:FB PPU:108, 84 CYC:9591 $DB9E:8D 00 03 STA $0300 = #$01 A:A9 X:07 Y:00 P:E5 SP:FB PPU:114, 84 CYC:9593 $DBA1:A9 AA LDA #$AA A:A9 X:07 Y:00 P:E5 SP:FB PPU:126, 84 CYC:9597 $DBA3:8D 01 03 STA $0301 = #$00 A:AA X:07 Y:00 P:E5 SP:FB PPU:132, 84 CYC:9599 $DBA6:A9 60 LDA #$60 A:AA X:07 Y:00 P:E5 SP:FB PPU:144, 84 CYC:9603 $DBA8:8D 02 03 STA $0302 = #$00 A:60 X:07 Y:00 P:65 SP:FB PPU:150, 84 CYC:9605 $DBAB:20 B5 DB JSR $DBB5 A:60 X:07 Y:00 P:65 SP:FB PPU:162, 84 CYC:9609 $DBB5:6C FF 02 JMP ($02FF) = #$0300 A:60 X:07 Y:00 P:65 SP:F9 PPU:180, 84 CYC:9615 $0300:A9 AA LDA #$AA A:60 X:07 Y:00 P:65 SP:F9 PPU:195, 84 CYC:9620 $0302:60 RTS A:AA X:07 Y:00 P:E5 SP:F9 PPU:201, 84 CYC:9622 $DBAE:C9 AA CMP #$AA A:AA X:07 Y:00 P:E5 SP:FB PPU:219, 84 CYC:9628 $DBB0:F0 02 BEQ $DBB4 A:AA X:07 Y:00 P:67 SP:FB PPU:225, 84 CYC:9630 $DBB4:60 RTS A:AA X:07 Y:00 P:67 SP:FB PPU:234, 84 CYC:9633 $C626:20 4A DF JSR $DF4A A:AA X:07 Y:00 P:67 SP:FD PPU:252, 84 CYC:9639 $DF4A:A9 89 LDA #$89 A:AA X:07 Y:00 P:67 SP:FB PPU:270, 84 CYC:9645 $DF4C:8D 00 03 STA $0300 = #$A9 A:89 X:07 Y:00 P:E5 SP:FB PPU:276, 84 CYC:9647 $DF4F:A9 A3 LDA #$A3 A:89 X:07 Y:00 P:E5 SP:FB PPU:288, 84 CYC:9651 $DF51:85 33 STA $33 = #$00 A:A3 X:07 Y:00 P:E5 SP:FB PPU:294, 84 CYC:9653 $DF53:A9 12 LDA #$12 A:A3 X:07 Y:00 P:E5 SP:FB PPU:303, 84 CYC:9656 $DF55:8D 45 02 STA $0245 = #$12 A:12 X:07 Y:00 P:65 SP:FB PPU:309, 84 CYC:9658 $DF58:A2 65 LDX #$65 A:12 X:07 Y:00 P:65 SP:FB PPU:321, 84 CYC:9662 $DF5A:A0 00 LDY #$00 A:12 X:65 Y:00 P:65 SP:FB PPU:327, 84 CYC:9664 $DF5C:38 SEC A:12 X:65 Y:00 P:67 SP:FB PPU:333, 84 CYC:9666 $DF5D:A9 00 LDA #$00 A:12 X:65 Y:00 P:67 SP:FB PPU:339, 84 CYC:9668 $DF5F:B8 CLV A:00 X:65 Y:00 P:67 SP:FB PPU: 4, 85 CYC:9670 $DF60:B9 00 03 LDA $0300,Y @ 0300 = #$89 A:00 X:65 Y:00 P:nvUbdIZC SP:FB PPU: 10, 85 CYC:9672 $DF63:F0 0C BEQ $DF71 A:89 X:65 Y:00 P:A5 SP:FB PPU: 22, 85 CYC:9676 $DF65:90 0A BCC $DF71 A:89 X:65 Y:00 P:A5 SP:FB PPU: 28, 85 CYC:9678 $DF67:70 08 BVS $DF71 A:89 X:65 Y:00 P:A5 SP:FB PPU: 34, 85 CYC:9680 $DF69:C9 89 CMP #$89 A:89 X:65 Y:00 P:A5 SP:FB PPU: 40, 85 CYC:9682 $DF6B:D0 04 BNE $DF71 A:89 X:65 Y:00 P:nvUbdIZC SP:FB PPU: 46, 85 CYC:9684 $DF6D:E0 65 CPX #$65 A:89 X:65 Y:00 P:nvUbdIZC SP:FB PPU: 52, 85 CYC:9686 $DF6F:F0 04 BEQ $DF75 A:89 X:65 Y:00 P:nvUbdIZC SP:FB PPU: 58, 85 CYC:9688 $DF75:A9 FF LDA #$FF A:89 X:65 Y:00 P:nvUbdIZC SP:FB PPU: 67, 85 CYC:9691 $DF77:85 01 STA $01 = #$FF A:FF X:65 Y:00 P:A5 SP:FB PPU: 73, 85 CYC:9693 $DF79:24 01 BIT $01 = #$FF A:FF X:65 Y:00 P:A5 SP:FB PPU: 82, 85 CYC:9696 $DF7B:A0 34 LDY #$34 A:FF X:65 Y:00 P:E5 SP:FB PPU: 91, 85 CYC:9699 $DF7D:B9 FF FF LDA $FFFF,Y @ 0033 = #$A3 A:FF X:65 Y:34 P:65 SP:FB PPU: 97, 85 CYC:9701 $DF80:C9 A3 CMP #$A3 A:A3 X:65 Y:34 P:E5 SP:FB PPU:112, 85 CYC:9706 $DF82:D0 02 BNE $DF86 A:A3 X:65 Y:34 P:67 SP:FB PPU:118, 85 CYC:9708 $DF84:B0 04 BCS $DF8A A:A3 X:65 Y:34 P:67 SP:FB PPU:124, 85 CYC:9710 $DF8A:A9 46 LDA #$46 A:A3 X:65 Y:34 P:67 SP:FB PPU:133, 85 CYC:9713 $DF8C:85 FF STA $FF = #$46 A:46 X:65 Y:34 P:65 SP:FB PPU:139, 85 CYC:9715 $DF8E:A0 FF LDY #$FF A:46 X:65 Y:34 P:65 SP:FB PPU:148, 85 CYC:9718 $DF90:B9 46 01 LDA $0146,Y @ 0245 = #$12 A:46 X:65 Y:FF P:E5 SP:FB PPU:154, 85 CYC:9720 $DF93:C9 12 CMP #$12 A:12 X:65 Y:FF P:65 SP:FB PPU:169, 85 CYC:9725 $DF95:F0 04 BEQ $DF9B A:12 X:65 Y:FF P:67 SP:FB PPU:175, 85 CYC:9727 $DF9B:A2 39 LDX #$39 A:12 X:65 Y:FF P:67 SP:FB PPU:184, 85 CYC:9730 $DF9D:18 CLC A:12 X:39 Y:FF P:65 SP:FB PPU:190, 85 CYC:9732 $DF9E:A9 FF LDA #$FF A:12 X:39 Y:FF P:64 SP:FB PPU:196, 85 CYC:9734 $DFA0:85 01 STA $01 = #$FF A:FF X:39 Y:FF P:NVUbdIzc SP:FB PPU:202, 85 CYC:9736 $DFA2:24 01 BIT $01 = #$FF A:FF X:39 Y:FF P:NVUbdIzc SP:FB PPU:211, 85 CYC:9739 $DFA4:A9 AA LDA #$AA A:FF X:39 Y:FF P:NVUbdIzc SP:FB PPU:220, 85 CYC:9742 $DFA6:8D 00 04 STA $0400 = #$87 A:AA X:39 Y:FF P:NVUbdIzc SP:FB PPU:226, 85 CYC:9744 $DFA9:A9 55 LDA #$55 A:AA X:39 Y:FF P:NVUbdIzc SP:FB PPU:238, 85 CYC:9748 $DFAB:A0 00 LDY #$00 A:55 X:39 Y:FF P:64 SP:FB PPU:244, 85 CYC:9750 $DFAD:19 00 04 ORA $0400,Y @ 0400 = #$AA A:55 X:39 Y:00 P:nVUbdIZc SP:FB PPU:250, 85 CYC:9752 $DFB0:B0 08 BCS $DFBA A:FF X:39 Y:00 P:NVUbdIzc SP:FB PPU:262, 85 CYC:9756 $DFB2:10 06 BPL $DFBA A:FF X:39 Y:00 P:NVUbdIzc SP:FB PPU:268, 85 CYC:9758 $DFB4:C9 FF CMP #$FF A:FF X:39 Y:00 P:NVUbdIzc SP:FB PPU:274, 85 CYC:9760 $DFB6:D0 02 BNE $DFBA A:FF X:39 Y:00 P:67 SP:FB PPU:280, 85 CYC:9762 $DFB8:70 02 BVS $DFBC A:FF X:39 Y:00 P:67 SP:FB PPU:286, 85 CYC:9764 $DFBC:E8 INX A:FF X:39 Y:00 P:67 SP:FB PPU:295, 85 CYC:9767 $DFBD:38 SEC A:FF X:3A Y:00 P:65 SP:FB PPU:301, 85 CYC:9769 $DFBE:B8 CLV A:FF X:3A Y:00 P:65 SP:FB PPU:307, 85 CYC:9771 $DFBF:A9 00 LDA #$00 A:FF X:3A Y:00 P:25 SP:FB PPU:313, 85 CYC:9773 $DFC1:19 00 04 ORA $0400,Y @ 0400 = #$AA A:00 X:3A Y:00 P:nvUbdIZC SP:FB PPU:319, 85 CYC:9775 $DFC4:F0 06 BEQ $DFCC A:AA X:3A Y:00 P:A5 SP:FB PPU:331, 85 CYC:9779 $DFC6:70 04 BVS $DFCC A:AA X:3A Y:00 P:A5 SP:FB PPU:337, 85 CYC:9781 $DFC8:90 02 BCC $DFCC A:AA X:3A Y:00 P:A5 SP:FB PPU: 2, 86 CYC:9783 $DFCA:30 02 BMI $DFCE A:AA X:3A Y:00 P:A5 SP:FB PPU: 8, 86 CYC:9785 $DFCE:E8 INX A:AA X:3A Y:00 P:A5 SP:FB PPU: 17, 86 CYC:9788 $DFCF:18 CLC A:AA X:3B Y:00 P:25 SP:FB PPU: 23, 86 CYC:9790 $DFD0:24 01 BIT $01 = #$FF A:AA X:3B Y:00 P:nvUbdIzc SP:FB PPU: 29, 86 CYC:9792 $DFD2:A9 55 LDA #$55 A:AA X:3B Y:00 P:NVUbdIzc SP:FB PPU: 38, 86 CYC:9795 $DFD4:39 00 04 AND $0400,Y @ 0400 = #$AA A:55 X:3B Y:00 P:64 SP:FB PPU: 44, 86 CYC:9797 $DFD7:D0 06 BNE $DFDF A:00 X:3B Y:00 P:nVUbdIZc SP:FB PPU: 56, 86 CYC:9801 $DFD9:50 04 BVC $DFDF A:00 X:3B Y:00 P:nVUbdIZc SP:FB PPU: 62, 86 CYC:9803 $DFDB:B0 02 BCS $DFDF A:00 X:3B Y:00 P:nVUbdIZc SP:FB PPU: 68, 86 CYC:9805 $DFDD:10 02 BPL $DFE1 A:00 X:3B Y:00 P:nVUbdIZc SP:FB PPU: 74, 86 CYC:9807 $DFE1:E8 INX A:00 X:3B Y:00 P:nVUbdIZc SP:FB PPU: 83, 86 CYC:9810 $DFE2:38 SEC A:00 X:3C Y:00 P:64 SP:FB PPU: 89, 86 CYC:9812 $DFE3:B8 CLV A:00 X:3C Y:00 P:65 SP:FB PPU: 95, 86 CYC:9814 $DFE4:A9 EF LDA #$EF A:00 X:3C Y:00 P:25 SP:FB PPU:101, 86 CYC:9816 $DFE6:8D 00 04 STA $0400 = #$AA A:EF X:3C Y:00 P:A5 SP:FB PPU:107, 86 CYC:9818 $DFE9:A9 F8 LDA #$F8 A:EF X:3C Y:00 P:A5 SP:FB PPU:119, 86 CYC:9822 $DFEB:39 00 04 AND $0400,Y @ 0400 = #$EF A:F8 X:3C Y:00 P:A5 SP:FB PPU:125, 86 CYC:9824 $DFEE:90 08 BCC $DFF8 A:E8 X:3C Y:00 P:A5 SP:FB PPU:137, 86 CYC:9828 $DFF0:10 06 BPL $DFF8 A:E8 X:3C Y:00 P:A5 SP:FB PPU:143, 86 CYC:9830 $DFF2:C9 E8 CMP #$E8 A:E8 X:3C Y:00 P:A5 SP:FB PPU:149, 86 CYC:9832 $DFF4:D0 02 BNE $DFF8 A:E8 X:3C Y:00 P:nvUbdIZC SP:FB PPU:155, 86 CYC:9834 $DFF6:50 02 BVC $DFFA A:E8 X:3C Y:00 P:nvUbdIZC SP:FB PPU:161, 86 CYC:9836 $DFFA:E8 INX A:E8 X:3C Y:00 P:nvUbdIZC SP:FB PPU:170, 86 CYC:9839 $DFFB:18 CLC A:E8 X:3D Y:00 P:25 SP:FB PPU:176, 86 CYC:9841 $DFFC:24 01 BIT $01 = #$FF A:E8 X:3D Y:00 P:nvUbdIzc SP:FB PPU:182, 86 CYC:9843 $DFFE:A9 AA LDA #$AA A:E8 X:3D Y:00 P:NVUbdIzc SP:FB PPU:191, 86 CYC:9846 $E000:8D 00 04 STA $0400 = #$EF A:AA X:3D Y:00 P:NVUbdIzc SP:FB PPU:197, 86 CYC:9848 $E003:A9 5F LDA #$5F A:AA X:3D Y:00 P:NVUbdIzc SP:FB PPU:209, 86 CYC:9852 $E005:59 00 04 EOR $0400,Y @ 0400 = #$AA A:5F X:3D Y:00 P:64 SP:FB PPU:215, 86 CYC:9854 $E008:B0 08 BCS $E012 A:F5 X:3D Y:00 P:NVUbdIzc SP:FB PPU:227, 86 CYC:9858 $E00A:10 06 BPL $E012 A:F5 X:3D Y:00 P:NVUbdIzc SP:FB PPU:233, 86 CYC:9860 $E00C:C9 F5 CMP #$F5 A:F5 X:3D Y:00 P:NVUbdIzc SP:FB PPU:239, 86 CYC:9862 $E00E:D0 02 BNE $E012 A:F5 X:3D Y:00 P:67 SP:FB PPU:245, 86 CYC:9864 $E010:70 02 BVS $E014 A:F5 X:3D Y:00 P:67 SP:FB PPU:251, 86 CYC:9866 $E014:E8 INX A:F5 X:3D Y:00 P:67 SP:FB PPU:260, 86 CYC:9869 $E015:38 SEC A:F5 X:3E Y:00 P:65 SP:FB PPU:266, 86 CYC:9871 $E016:B8 CLV A:F5 X:3E Y:00 P:65 SP:FB PPU:272, 86 CYC:9873 $E017:A9 70 LDA #$70 A:F5 X:3E Y:00 P:25 SP:FB PPU:278, 86 CYC:9875 $E019:8D 00 04 STA $0400 = #$AA A:70 X:3E Y:00 P:25 SP:FB PPU:284, 86 CYC:9877 $E01C:59 00 04 EOR $0400,Y @ 0400 = #$70 A:70 X:3E Y:00 P:25 SP:FB PPU:296, 86 CYC:9881 $E01F:D0 06 BNE $E027 A:00 X:3E Y:00 P:nvUbdIZC SP:FB PPU:308, 86 CYC:9885 $E021:70 04 BVS $E027 A:00 X:3E Y:00 P:nvUbdIZC SP:FB PPU:314, 86 CYC:9887 $E023:90 02 BCC $E027 A:00 X:3E Y:00 P:nvUbdIZC SP:FB PPU:320, 86 CYC:9889 $E025:10 02 BPL $E029 A:00 X:3E Y:00 P:nvUbdIZC SP:FB PPU:326, 86 CYC:9891 $E029:E8 INX A:00 X:3E Y:00 P:nvUbdIZC SP:FB PPU:335, 86 CYC:9894 $E02A:18 CLC A:00 X:3F Y:00 P:25 SP:FB PPU: 0, 87 CYC:9896 $E02B:24 01 BIT $01 = #$FF A:00 X:3F Y:00 P:nvUbdIzc SP:FB PPU: 6, 87 CYC:9898 $E02D:A9 69 LDA #$69 A:00 X:3F Y:00 P:E6 SP:FB PPU: 15, 87 CYC:9901 $E02F:8D 00 04 STA $0400 = #$70 A:69 X:3F Y:00 P:64 SP:FB PPU: 21, 87 CYC:9903 $E032:A9 00 LDA #$00 A:69 X:3F Y:00 P:64 SP:FB PPU: 33, 87 CYC:9907 $E034:79 00 04 ADC $0400,Y @ 0400 = #$69 A:00 X:3F Y:00 P:nVUbdIZc SP:FB PPU: 39, 87 CYC:9909 $E037:30 08 BMI $E041 A:69 X:3F Y:00 P:nvUbdIzc SP:FB PPU: 51, 87 CYC:9913 $E039:B0 06 BCS $E041 A:69 X:3F Y:00 P:nvUbdIzc SP:FB PPU: 57, 87 CYC:9915 $E03B:C9 69 CMP #$69 A:69 X:3F Y:00 P:nvUbdIzc SP:FB PPU: 63, 87 CYC:9917 $E03D:D0 02 BNE $E041 A:69 X:3F Y:00 P:nvUbdIZC SP:FB PPU: 69, 87 CYC:9919 $E03F:50 02 BVC $E043 A:69 X:3F Y:00 P:nvUbdIZC SP:FB PPU: 75, 87 CYC:9921 $E043:E8 INX A:69 X:3F Y:00 P:nvUbdIZC SP:FB PPU: 84, 87 CYC:9924 $E044:38 SEC A:69 X:40 Y:00 P:25 SP:FB PPU: 90, 87 CYC:9926 $E045:24 01 BIT $01 = #$FF A:69 X:40 Y:00 P:25 SP:FB PPU: 96, 87 CYC:9928 $E047:A9 00 LDA #$00 A:69 X:40 Y:00 P:E5 SP:FB PPU:105, 87 CYC:9931 $E049:79 00 04 ADC $0400,Y @ 0400 = #$69 A:00 X:40 Y:00 P:67 SP:FB PPU:111, 87 CYC:9933 $E04C:30 08 BMI $E056 A:6A X:40 Y:00 P:nvUbdIzc SP:FB PPU:123, 87 CYC:9937 $E04E:B0 06 BCS $E056 A:6A X:40 Y:00 P:nvUbdIzc SP:FB PPU:129, 87 CYC:9939 $E050:C9 6A CMP #$6A A:6A X:40 Y:00 P:nvUbdIzc SP:FB PPU:135, 87 CYC:9941 $E052:D0 02 BNE $E056 A:6A X:40 Y:00 P:nvUbdIZC SP:FB PPU:141, 87 CYC:9943 $E054:50 02 BVC $E058 A:6A X:40 Y:00 P:nvUbdIZC SP:FB PPU:147, 87 CYC:9945 $E058:E8 INX A:6A X:40 Y:00 P:nvUbdIZC SP:FB PPU:156, 87 CYC:9948 $E059:38 SEC A:6A X:41 Y:00 P:25 SP:FB PPU:162, 87 CYC:9950 $E05A:B8 CLV A:6A X:41 Y:00 P:25 SP:FB PPU:168, 87 CYC:9952 $E05B:A9 7F LDA #$7F A:6A X:41 Y:00 P:25 SP:FB PPU:174, 87 CYC:9954 $E05D:8D 00 04 STA $0400 = #$69 A:7F X:41 Y:00 P:25 SP:FB PPU:180, 87 CYC:9956 $E060:79 00 04 ADC $0400,Y @ 0400 = #$7F A:7F X:41 Y:00 P:25 SP:FB PPU:192, 87 CYC:9960 $E063:10 08 BPL $E06D A:FF X:41 Y:00 P:NVUbdIzc SP:FB PPU:204, 87 CYC:9964 $E065:B0 06 BCS $E06D A:FF X:41 Y:00 P:NVUbdIzc SP:FB PPU:210, 87 CYC:9966 $E067:C9 FF CMP #$FF A:FF X:41 Y:00 P:NVUbdIzc SP:FB PPU:216, 87 CYC:9968 $E069:D0 02 BNE $E06D A:FF X:41 Y:00 P:67 SP:FB PPU:222, 87 CYC:9970 $E06B:70 02 BVS $E06F A:FF X:41 Y:00 P:67 SP:FB PPU:228, 87 CYC:9972 $E06F:E8 INX A:FF X:41 Y:00 P:67 SP:FB PPU:237, 87 CYC:9975 $E070:18 CLC A:FF X:42 Y:00 P:65 SP:FB PPU:243, 87 CYC:9977 $E071:24 01 BIT $01 = #$FF A:FF X:42 Y:00 P:64 SP:FB PPU:249, 87 CYC:9979 $E073:A9 80 LDA #$80 A:FF X:42 Y:00 P:NVUbdIzc SP:FB PPU:258, 87 CYC:9982 $E075:8D 00 04 STA $0400 = #$7F A:80 X:42 Y:00 P:NVUbdIzc SP:FB PPU:264, 87 CYC:9984 $E078:A9 7F LDA #$7F A:80 X:42 Y:00 P:NVUbdIzc SP:FB PPU:276, 87 CYC:9988 $E07A:79 00 04 ADC $0400,Y @ 0400 = #$80 A:7F X:42 Y:00 P:64 SP:FB PPU:282, 87 CYC:9990 $E07D:10 08 BPL $E087 A:FF X:42 Y:00 P:NvUbdIzc SP:FB PPU:294, 87 CYC:9994 $E07F:B0 06 BCS $E087 A:FF X:42 Y:00 P:NvUbdIzc SP:FB PPU:300, 87 CYC:9996 $E081:C9 FF CMP #$FF A:FF X:42 Y:00 P:NvUbdIzc SP:FB PPU:306, 87 CYC:9998 $E083:D0 02 BNE $E087 A:FF X:42 Y:00 P:nvUbdIZC SP:FB PPU:312, 87 CYC:10000 $E085:50 02 BVC $E089 A:FF X:42 Y:00 P:nvUbdIZC SP:FB PPU:318, 87 CYC:10002 $E089:E8 INX A:FF X:42 Y:00 P:nvUbdIZC SP:FB PPU:327, 87 CYC:10005 $E08A:38 SEC A:FF X:43 Y:00 P:25 SP:FB PPU:333, 87 CYC:10007 $E08B:B8 CLV A:FF X:43 Y:00 P:25 SP:FB PPU:339, 87 CYC:10009 $E08C:A9 80 LDA #$80 A:FF X:43 Y:00 P:25 SP:FB PPU: 4, 88 CYC:10011 $E08E:8D 00 04 STA $0400 = #$80 A:80 X:43 Y:00 P:A5 SP:FB PPU: 10, 88 CYC:10013 $E091:A9 7F LDA #$7F A:80 X:43 Y:00 P:A5 SP:FB PPU: 22, 88 CYC:10017 $E093:79 00 04 ADC $0400,Y @ 0400 = #$80 A:7F X:43 Y:00 P:25 SP:FB PPU: 28, 88 CYC:10019 $E096:D0 06 BNE $E09E A:00 X:43 Y:00 P:nvUbdIZC SP:FB PPU: 40, 88 CYC:10023 $E098:30 04 BMI $E09E A:00 X:43 Y:00 P:nvUbdIZC SP:FB PPU: 46, 88 CYC:10025 $E09A:70 02 BVS $E09E A:00 X:43 Y:00 P:nvUbdIZC SP:FB PPU: 52, 88 CYC:10027 $E09C:B0 02 BCS $E0A0 A:00 X:43 Y:00 P:nvUbdIZC SP:FB PPU: 58, 88 CYC:10029 $E0A0:E8 INX A:00 X:43 Y:00 P:nvUbdIZC SP:FB PPU: 67, 88 CYC:10032 $E0A1:24 01 BIT $01 = #$FF A:00 X:44 Y:00 P:25 SP:FB PPU: 73, 88 CYC:10034 $E0A3:A9 40 LDA #$40 A:00 X:44 Y:00 P:E7 SP:FB PPU: 82, 88 CYC:10037 $E0A5:8D 00 04 STA $0400 = #$80 A:40 X:44 Y:00 P:65 SP:FB PPU: 88, 88 CYC:10039 $E0A8:D9 00 04 CMP $0400,Y @ 0400 = #$40 A:40 X:44 Y:00 P:65 SP:FB PPU:100, 88 CYC:10043 $E0AB:30 06 BMI $E0B3 A:40 X:44 Y:00 P:67 SP:FB PPU:112, 88 CYC:10047 $E0AD:90 04 BCC $E0B3 A:40 X:44 Y:00 P:67 SP:FB PPU:118, 88 CYC:10049 $E0AF:D0 02 BNE $E0B3 A:40 X:44 Y:00 P:67 SP:FB PPU:124, 88 CYC:10051 $E0B1:70 02 BVS $E0B5 A:40 X:44 Y:00 P:67 SP:FB PPU:130, 88 CYC:10053 $E0B5:E8 INX A:40 X:44 Y:00 P:67 SP:FB PPU:139, 88 CYC:10056 $E0B6:B8 CLV A:40 X:45 Y:00 P:65 SP:FB PPU:145, 88 CYC:10058 $E0B7:CE 00 04 DEC $0400 = #$40 A:40 X:45 Y:00 P:25 SP:FB PPU:151, 88 CYC:10060 $E0BA:D9 00 04 CMP $0400,Y @ 0400 = #$3F A:40 X:45 Y:00 P:25 SP:FB PPU:169, 88 CYC:10066 $E0BD:F0 06 BEQ $E0C5 A:40 X:45 Y:00 P:25 SP:FB PPU:181, 88 CYC:10070 $E0BF:30 04 BMI $E0C5 A:40 X:45 Y:00 P:25 SP:FB PPU:187, 88 CYC:10072 $E0C1:90 02 BCC $E0C5 A:40 X:45 Y:00 P:25 SP:FB PPU:193, 88 CYC:10074 $E0C3:50 02 BVC $E0C7 A:40 X:45 Y:00 P:25 SP:FB PPU:199, 88 CYC:10076 $E0C7:E8 INX A:40 X:45 Y:00 P:25 SP:FB PPU:208, 88 CYC:10079 $E0C8:EE 00 04 INC $0400 = #$3F A:40 X:46 Y:00 P:25 SP:FB PPU:214, 88 CYC:10081 $E0CB:EE 00 04 INC $0400 = #$40 A:40 X:46 Y:00 P:25 SP:FB PPU:232, 88 CYC:10087 $E0CE:D9 00 04 CMP $0400,Y @ 0400 = #$41 A:40 X:46 Y:00 P:25 SP:FB PPU:250, 88 CYC:10093 $E0D1:F0 02 BEQ $E0D5 A:40 X:46 Y:00 P:NvUbdIzc SP:FB PPU:262, 88 CYC:10097 $E0D3:30 02 BMI $E0D7 A:40 X:46 Y:00 P:NvUbdIzc SP:FB PPU:268, 88 CYC:10099 $E0D7:E8 INX A:40 X:46 Y:00 P:NvUbdIzc SP:FB PPU:277, 88 CYC:10102 $E0D8:A9 00 LDA #$00 A:40 X:47 Y:00 P:nvUbdIzc SP:FB PPU:283, 88 CYC:10104 $E0DA:8D 00 04 STA $0400 = #$41 A:00 X:47 Y:00 P:nvUbdIZc SP:FB PPU:289, 88 CYC:10106 $E0DD:A9 80 LDA #$80 A:00 X:47 Y:00 P:nvUbdIZc SP:FB PPU:301, 88 CYC:10110 $E0DF:D9 00 04 CMP $0400,Y @ 0400 = #$00 A:80 X:47 Y:00 P:NvUbdIzc SP:FB PPU:307, 88 CYC:10112 $E0E2:F0 04 BEQ $E0E8 A:80 X:47 Y:00 P:A5 SP:FB PPU:319, 88 CYC:10116 $E0E4:10 02 BPL $E0E8 A:80 X:47 Y:00 P:A5 SP:FB PPU:325, 88 CYC:10118 $E0E6:B0 02 BCS $E0EA A:80 X:47 Y:00 P:A5 SP:FB PPU:331, 88 CYC:10120 $E0EA:E8 INX A:80 X:47 Y:00 P:A5 SP:FB PPU:340, 88 CYC:10123 $E0EB:A0 80 LDY #$80 A:80 X:48 Y:00 P:25 SP:FB PPU: 5, 89 CYC:10125 $E0ED:8C 00 04 STY $0400 = #$00 A:80 X:48 Y:80 P:A5 SP:FB PPU: 11, 89 CYC:10127 $E0F0:A0 00 LDY #$00 A:80 X:48 Y:80 P:A5 SP:FB PPU: 23, 89 CYC:10131 $E0F2:D9 00 04 CMP $0400,Y @ 0400 = #$80 A:80 X:48 Y:00 P:nvUbdIZC SP:FB PPU: 29, 89 CYC:10133 $E0F5:D0 04 BNE $E0FB A:80 X:48 Y:00 P:nvUbdIZC SP:FB PPU: 41, 89 CYC:10137 $E0F7:30 02 BMI $E0FB A:80 X:48 Y:00 P:nvUbdIZC SP:FB PPU: 47, 89 CYC:10139 $E0F9:B0 02 BCS $E0FD A:80 X:48 Y:00 P:nvUbdIZC SP:FB PPU: 53, 89 CYC:10141 $E0FD:E8 INX A:80 X:48 Y:00 P:nvUbdIZC SP:FB PPU: 62, 89 CYC:10144 $E0FE:EE 00 04 INC $0400 = #$80 A:80 X:49 Y:00 P:25 SP:FB PPU: 68, 89 CYC:10146 $E101:D9 00 04 CMP $0400,Y @ 0400 = #$81 A:80 X:49 Y:00 P:A5 SP:FB PPU: 86, 89 CYC:10152 $E104:B0 04 BCS $E10A A:80 X:49 Y:00 P:NvUbdIzc SP:FB PPU: 98, 89 CYC:10156 $E106:F0 02 BEQ $E10A A:80 X:49 Y:00 P:NvUbdIzc SP:FB PPU:104, 89 CYC:10158 $E108:30 02 BMI $E10C A:80 X:49 Y:00 P:NvUbdIzc SP:FB PPU:110, 89 CYC:10160 $E10C:E8 INX A:80 X:49 Y:00 P:NvUbdIzc SP:FB PPU:119, 89 CYC:10163 $E10D:CE 00 04 DEC $0400 = #$81 A:80 X:4A Y:00 P:nvUbdIzc SP:FB PPU:125, 89 CYC:10165 $E110:CE 00 04 DEC $0400 = #$80 A:80 X:4A Y:00 P:NvUbdIzc SP:FB PPU:143, 89 CYC:10171 $E113:D9 00 04 CMP $0400,Y @ 0400 = #$7F A:80 X:4A Y:00 P:nvUbdIzc SP:FB PPU:161, 89 CYC:10177 $E116:90 04 BCC $E11C A:80 X:4A Y:00 P:25 SP:FB PPU:173, 89 CYC:10181 $E118:F0 02 BEQ $E11C A:80 X:4A Y:00 P:25 SP:FB PPU:179, 89 CYC:10183 $E11A:10 02 BPL $E11E A:80 X:4A Y:00 P:25 SP:FB PPU:185, 89 CYC:10185 $E11E:E8 INX A:80 X:4A Y:00 P:25 SP:FB PPU:194, 89 CYC:10188 $E11F:24 01 BIT $01 = #$FF A:80 X:4B Y:00 P:25 SP:FB PPU:200, 89 CYC:10190 $E121:A9 40 LDA #$40 A:80 X:4B Y:00 P:E5 SP:FB PPU:209, 89 CYC:10193 $E123:8D 00 04 STA $0400 = #$7F A:40 X:4B Y:00 P:65 SP:FB PPU:215, 89 CYC:10195 $E126:38 SEC A:40 X:4B Y:00 P:65 SP:FB PPU:227, 89 CYC:10199 $E127:F9 00 04 SBC $0400,Y @ 0400 = #$40 A:40 X:4B Y:00 P:65 SP:FB PPU:233, 89 CYC:10201 $E12A:30 0A BMI $E136 A:00 X:4B Y:00 P:nvUbdIZC SP:FB PPU:245, 89 CYC:10205 $E12C:90 08 BCC $E136 A:00 X:4B Y:00 P:nvUbdIZC SP:FB PPU:251, 89 CYC:10207 $E12E:D0 06 BNE $E136 A:00 X:4B Y:00 P:nvUbdIZC SP:FB PPU:257, 89 CYC:10209 $E130:70 04 BVS $E136 A:00 X:4B Y:00 P:nvUbdIZC SP:FB PPU:263, 89 CYC:10211 $E132:C9 00 CMP #$00 A:00 X:4B Y:00 P:nvUbdIZC SP:FB PPU:269, 89 CYC:10213 $E134:F0 02 BEQ $E138 A:00 X:4B Y:00 P:nvUbdIZC SP:FB PPU:275, 89 CYC:10215 $E138:E8 INX A:00 X:4B Y:00 P:nvUbdIZC SP:FB PPU:284, 89 CYC:10218 $E139:B8 CLV A:00 X:4C Y:00 P:25 SP:FB PPU:290, 89 CYC:10220 $E13A:38 SEC A:00 X:4C Y:00 P:25 SP:FB PPU:296, 89 CYC:10222 $E13B:A9 40 LDA #$40 A:00 X:4C Y:00 P:25 SP:FB PPU:302, 89 CYC:10224 $E13D:CE 00 04 DEC $0400 = #$40 A:40 X:4C Y:00 P:25 SP:FB PPU:308, 89 CYC:10226 $E140:F9 00 04 SBC $0400,Y @ 0400 = #$3F A:40 X:4C Y:00 P:25 SP:FB PPU:326, 89 CYC:10232 $E143:F0 0A BEQ $E14F A:01 X:4C Y:00 P:25 SP:FB PPU:338, 89 CYC:10236 $E145:30 08 BMI $E14F A:01 X:4C Y:00 P:25 SP:FB PPU: 3, 90 CYC:10238 $E147:90 06 BCC $E14F A:01 X:4C Y:00 P:25 SP:FB PPU: 9, 90 CYC:10240 $E149:70 04 BVS $E14F A:01 X:4C Y:00 P:25 SP:FB PPU: 15, 90 CYC:10242 $E14B:C9 01 CMP #$01 A:01 X:4C Y:00 P:25 SP:FB PPU: 21, 90 CYC:10244 $E14D:F0 02 BEQ $E151 A:01 X:4C Y:00 P:nvUbdIZC SP:FB PPU: 27, 90 CYC:10246 $E151:E8 INX A:01 X:4C Y:00 P:nvUbdIZC SP:FB PPU: 36, 90 CYC:10249 $E152:A9 40 LDA #$40 A:01 X:4D Y:00 P:25 SP:FB PPU: 42, 90 CYC:10251 $E154:38 SEC A:40 X:4D Y:00 P:25 SP:FB PPU: 48, 90 CYC:10253 $E155:24 01 BIT $01 = #$FF A:40 X:4D Y:00 P:25 SP:FB PPU: 54, 90 CYC:10255 $E157:EE 00 04 INC $0400 = #$3F A:40 X:4D Y:00 P:E5 SP:FB PPU: 63, 90 CYC:10258 $E15A:EE 00 04 INC $0400 = #$40 A:40 X:4D Y:00 P:65 SP:FB PPU: 81, 90 CYC:10264 $E15D:F9 00 04 SBC $0400,Y @ 0400 = #$41 A:40 X:4D Y:00 P:65 SP:FB PPU: 99, 90 CYC:10270 $E160:B0 0A BCS $E16C A:FF X:4D Y:00 P:NvUbdIzc SP:FB PPU:111, 90 CYC:10274 $E162:F0 08 BEQ $E16C A:FF X:4D Y:00 P:NvUbdIzc SP:FB PPU:117, 90 CYC:10276 $E164:10 06 BPL $E16C A:FF X:4D Y:00 P:NvUbdIzc SP:FB PPU:123, 90 CYC:10278 $E166:70 04 BVS $E16C A:FF X:4D Y:00 P:NvUbdIzc SP:FB PPU:129, 90 CYC:10280 $E168:C9 FF CMP #$FF A:FF X:4D Y:00 P:NvUbdIzc SP:FB PPU:135, 90 CYC:10282 $E16A:F0 02 BEQ $E16E A:FF X:4D Y:00 P:nvUbdIZC SP:FB PPU:141, 90 CYC:10284 $E16E:E8 INX A:FF X:4D Y:00 P:nvUbdIZC SP:FB PPU:150, 90 CYC:10287 $E16F:18 CLC A:FF X:4E Y:00 P:25 SP:FB PPU:156, 90 CYC:10289 $E170:A9 00 LDA #$00 A:FF X:4E Y:00 P:nvUbdIzc SP:FB PPU:162, 90 CYC:10291 $E172:8D 00 04 STA $0400 = #$41 A:00 X:4E Y:00 P:nvUbdIZc SP:FB PPU:168, 90 CYC:10293 $E175:A9 80 LDA #$80 A:00 X:4E Y:00 P:nvUbdIZc SP:FB PPU:180, 90 CYC:10297 $E177:F9 00 04 SBC $0400,Y @ 0400 = #$00 A:80 X:4E Y:00 P:NvUbdIzc SP:FB PPU:186, 90 CYC:10299 $E17A:90 04 BCC $E180 A:7F X:4E Y:00 P:65 SP:FB PPU:198, 90 CYC:10303 $E17C:C9 7F CMP #$7F A:7F X:4E Y:00 P:65 SP:FB PPU:204, 90 CYC:10305 $E17E:F0 02 BEQ $E182 A:7F X:4E Y:00 P:67 SP:FB PPU:210, 90 CYC:10307 $E182:E8 INX A:7F X:4E Y:00 P:67 SP:FB PPU:219, 90 CYC:10310 $E183:38 SEC A:7F X:4F Y:00 P:65 SP:FB PPU:225, 90 CYC:10312 $E184:A9 7F LDA #$7F A:7F X:4F Y:00 P:65 SP:FB PPU:231, 90 CYC:10314 $E186:8D 00 04 STA $0400 = #$00 A:7F X:4F Y:00 P:65 SP:FB PPU:237, 90 CYC:10316 $E189:A9 81 LDA #$81 A:7F X:4F Y:00 P:65 SP:FB PPU:249, 90 CYC:10320 $E18B:F9 00 04 SBC $0400,Y @ 0400 = #$7F A:81 X:4F Y:00 P:E5 SP:FB PPU:255, 90 CYC:10322 $E18E:50 06 BVC $E196 A:02 X:4F Y:00 P:65 SP:FB PPU:267, 90 CYC:10326 $E190:90 04 BCC $E196 A:02 X:4F Y:00 P:65 SP:FB PPU:273, 90 CYC:10328 $E192:C9 02 CMP #$02 A:02 X:4F Y:00 P:65 SP:FB PPU:279, 90 CYC:10330 $E194:F0 02 BEQ $E198 A:02 X:4F Y:00 P:67 SP:FB PPU:285, 90 CYC:10332 $E198:E8 INX A:02 X:4F Y:00 P:67 SP:FB PPU:294, 90 CYC:10335 $E199:A9 00 LDA #$00 A:02 X:50 Y:00 P:65 SP:FB PPU:300, 90 CYC:10337 $E19B:A9 87 LDA #$87 A:00 X:50 Y:00 P:67 SP:FB PPU:306, 90 CYC:10339 $E19D:99 00 04 STA $0400,Y @ 0400 = #$7F A:87 X:50 Y:00 P:E5 SP:FB PPU:312, 90 CYC:10341 $E1A0:AD 00 04 LDA $0400 = #$87 A:87 X:50 Y:00 P:E5 SP:FB PPU:327, 90 CYC:10346 $E1A3:C9 87 CMP #$87 A:87 X:50 Y:00 P:E5 SP:FB PPU:339, 90 CYC:10350 $E1A5:F0 02 BEQ $E1A9 A:87 X:50 Y:00 P:67 SP:FB PPU: 4, 91 CYC:10352 $E1A9:60 RTS A:87 X:50 Y:00 P:67 SP:FB PPU: 13, 91 CYC:10355 $C629:20 B8 DB JSR $DBB8 A:87 X:50 Y:00 P:67 SP:FD PPU: 31, 91 CYC:10361 $DBB8:A9 FF LDA #$FF A:87 X:50 Y:00 P:67 SP:FB PPU: 49, 91 CYC:10367 $DBBA:85 01 STA $01 = #$FF A:FF X:50 Y:00 P:E5 SP:FB PPU: 55, 91 CYC:10369 $DBBC:A9 AA LDA #$AA A:FF X:50 Y:00 P:E5 SP:FB PPU: 64, 91 CYC:10372 $DBBE:85 33 STA $33 = #$A3 A:AA X:50 Y:00 P:E5 SP:FB PPU: 70, 91 CYC:10374 $DBC0:A9 BB LDA #$BB A:AA X:50 Y:00 P:E5 SP:FB PPU: 79, 91 CYC:10377 $DBC2:85 89 STA $89 = #$00 A:BB X:50 Y:00 P:E5 SP:FB PPU: 85, 91 CYC:10379 $DBC4:A2 00 LDX #$00 A:BB X:50 Y:00 P:E5 SP:FB PPU: 94, 91 CYC:10382 $DBC6:A9 66 LDA #$66 A:BB X:00 Y:00 P:67 SP:FB PPU:100, 91 CYC:10384 $DBC8:24 01 BIT $01 = #$FF A:66 X:00 Y:00 P:65 SP:FB PPU:106, 91 CYC:10386 $DBCA:38 SEC A:66 X:00 Y:00 P:E5 SP:FB PPU:115, 91 CYC:10389 $DBCB:A0 00 LDY #$00 A:66 X:00 Y:00 P:E5 SP:FB PPU:121, 91 CYC:10391 $DBCD:B4 33 LDY $33,X @ 33 = #$AA A:66 X:00 Y:00 P:67 SP:FB PPU:127, 91 CYC:10393 $DBCF:10 12 BPL $DBE3 A:66 X:00 Y:AA P:E5 SP:FB PPU:139, 91 CYC:10397 $DBD1:F0 10 BEQ $DBE3 A:66 X:00 Y:AA P:E5 SP:FB PPU:145, 91 CYC:10399 $DBD3:50 0E BVC $DBE3 A:66 X:00 Y:AA P:E5 SP:FB PPU:151, 91 CYC:10401 $DBD5:90 0C BCC $DBE3 A:66 X:00 Y:AA P:E5 SP:FB PPU:157, 91 CYC:10403 $DBD7:C9 66 CMP #$66 A:66 X:00 Y:AA P:E5 SP:FB PPU:163, 91 CYC:10405 $DBD9:D0 08 BNE $DBE3 A:66 X:00 Y:AA P:67 SP:FB PPU:169, 91 CYC:10407 $DBDB:E0 00 CPX #$00 A:66 X:00 Y:AA P:67 SP:FB PPU:175, 91 CYC:10409 $DBDD:D0 04 BNE $DBE3 A:66 X:00 Y:AA P:67 SP:FB PPU:181, 91 CYC:10411 $DBDF:C0 AA CPY #$AA A:66 X:00 Y:AA P:67 SP:FB PPU:187, 91 CYC:10413 $DBE1:F0 04 BEQ $DBE7 A:66 X:00 Y:AA P:67 SP:FB PPU:193, 91 CYC:10415 $DBE7:A2 8A LDX #$8A A:66 X:00 Y:AA P:67 SP:FB PPU:202, 91 CYC:10418 $DBE9:A9 66 LDA #$66 A:66 X:8A Y:AA P:E5 SP:FB PPU:208, 91 CYC:10420 $DBEB:B8 CLV A:66 X:8A Y:AA P:65 SP:FB PPU:214, 91 CYC:10422 $DBEC:18 CLC A:66 X:8A Y:AA P:25 SP:FB PPU:220, 91 CYC:10424 $DBED:A0 00 LDY #$00 A:66 X:8A Y:AA P:nvUbdIzc SP:FB PPU:226, 91 CYC:10426 $DBEF:B4 FF LDY $FF,X @ 89 = #$BB A:66 X:8A Y:00 P:nvUbdIZc SP:FB PPU:232, 91 CYC:10428 $DBF1:10 12 BPL $DC05 A:66 X:8A Y:BB P:NvUbdIzc SP:FB PPU:244, 91 CYC:10432 $DBF3:F0 10 BEQ $DC05 A:66 X:8A Y:BB P:NvUbdIzc SP:FB PPU:250, 91 CYC:10434 $DBF5:70 0E BVS $DC05 A:66 X:8A Y:BB P:NvUbdIzc SP:FB PPU:256, 91 CYC:10436 $DBF7:B0 0C BCS $DC05 A:66 X:8A Y:BB P:NvUbdIzc SP:FB PPU:262, 91 CYC:10438 $DBF9:C0 BB CPY #$BB A:66 X:8A Y:BB P:NvUbdIzc SP:FB PPU:268, 91 CYC:10440 $DBFB:D0 08 BNE $DC05 A:66 X:8A Y:BB P:nvUbdIZC SP:FB PPU:274, 91 CYC:10442 $DBFD:C9 66 CMP #$66 A:66 X:8A Y:BB P:nvUbdIZC SP:FB PPU:280, 91 CYC:10444 $DBFF:D0 04 BNE $DC05 A:66 X:8A Y:BB P:nvUbdIZC SP:FB PPU:286, 91 CYC:10446 $DC01:E0 8A CPX #$8A A:66 X:8A Y:BB P:nvUbdIZC SP:FB PPU:292, 91 CYC:10448 $DC03:F0 04 BEQ $DC09 A:66 X:8A Y:BB P:nvUbdIZC SP:FB PPU:298, 91 CYC:10450 $DC09:24 01 BIT $01 = #$FF A:66 X:8A Y:BB P:nvUbdIZC SP:FB PPU:307, 91 CYC:10453 $DC0B:38 SEC A:66 X:8A Y:BB P:E5 SP:FB PPU:316, 91 CYC:10456 $DC0C:A0 44 LDY #$44 A:66 X:8A Y:BB P:E5 SP:FB PPU:322, 91 CYC:10458 $DC0E:A2 00 LDX #$00 A:66 X:8A Y:44 P:65 SP:FB PPU:328, 91 CYC:10460 $DC10:94 33 STY $33,X @ 33 = #$AA A:66 X:00 Y:44 P:67 SP:FB PPU:334, 91 CYC:10462 $DC12:A5 33 LDA $33 = #$44 A:66 X:00 Y:44 P:67 SP:FB PPU: 5, 92 CYC:10466 $DC14:90 18 BCC $DC2E A:44 X:00 Y:44 P:65 SP:FB PPU: 14, 92 CYC:10469 $DC16:C9 44 CMP #$44 A:44 X:00 Y:44 P:65 SP:FB PPU: 20, 92 CYC:10471 $DC18:D0 14 BNE $DC2E A:44 X:00 Y:44 P:67 SP:FB PPU: 26, 92 CYC:10473 $DC1A:50 12 BVC $DC2E A:44 X:00 Y:44 P:67 SP:FB PPU: 32, 92 CYC:10475 $DC1C:18 CLC A:44 X:00 Y:44 P:67 SP:FB PPU: 38, 92 CYC:10477 $DC1D:B8 CLV A:44 X:00 Y:44 P:nVUbdIZc SP:FB PPU: 44, 92 CYC:10479 $DC1E:A0 99 LDY #$99 A:44 X:00 Y:44 P:nvUbdIZc SP:FB PPU: 50, 92 CYC:10481 $DC20:A2 80 LDX #$80 A:44 X:00 Y:99 P:NvUbdIzc SP:FB PPU: 56, 92 CYC:10483 $DC22:94 85 STY $85,X @ 05 = #$00 A:44 X:80 Y:99 P:NvUbdIzc SP:FB PPU: 62, 92 CYC:10485 $DC24:A5 05 LDA $05 = #$99 A:44 X:80 Y:99 P:NvUbdIzc SP:FB PPU: 74, 92 CYC:10489 $DC26:B0 06 BCS $DC2E A:99 X:80 Y:99 P:NvUbdIzc SP:FB PPU: 83, 92 CYC:10492 $DC28:C9 99 CMP #$99 A:99 X:80 Y:99 P:NvUbdIzc SP:FB PPU: 89, 92 CYC:10494 $DC2A:D0 02 BNE $DC2E A:99 X:80 Y:99 P:nvUbdIZC SP:FB PPU: 95, 92 CYC:10496 $DC2C:50 04 BVC $DC32 A:99 X:80 Y:99 P:nvUbdIZC SP:FB PPU:101, 92 CYC:10498 $DC32:A0 0B LDY #$0B A:99 X:80 Y:99 P:nvUbdIZC SP:FB PPU:110, 92 CYC:10501 $DC34:A9 AA LDA #$AA A:99 X:80 Y:0B P:25 SP:FB PPU:116, 92 CYC:10503 $DC36:A2 78 LDX #$78 A:AA X:80 Y:0B P:A5 SP:FB PPU:122, 92 CYC:10505 $DC38:85 78 STA $78 = #$00 A:AA X:78 Y:0B P:25 SP:FB PPU:128, 92 CYC:10507 $DC3A:20 B6 F7 JSR $F7B6 A:AA X:78 Y:0B P:25 SP:FB PPU:137, 92 CYC:10510 $F7B6:18 CLC A:AA X:78 Y:0B P:25 SP:F9 PPU:155, 92 CYC:10516 $F7B7:A9 FF LDA #$FF A:AA X:78 Y:0B P:nvUbdIzc SP:F9 PPU:161, 92 CYC:10518 $F7B9:85 01 STA $01 = #$FF A:FF X:78 Y:0B P:NvUbdIzc SP:F9 PPU:167, 92 CYC:10520 $F7BB:24 01 BIT $01 = #$FF A:FF X:78 Y:0B P:NvUbdIzc SP:F9 PPU:176, 92 CYC:10523 $F7BD:A9 55 LDA #$55 A:FF X:78 Y:0B P:NVUbdIzc SP:F9 PPU:185, 92 CYC:10526 $F7BF:60 RTS A:55 X:78 Y:0B P:64 SP:F9 PPU:191, 92 CYC:10528 $DC3D:15 00 ORA $00,X @ 78 = #$AA A:55 X:78 Y:0B P:64 SP:FB PPU:209, 92 CYC:10534 $DC3F:20 C0 F7 JSR $F7C0 A:FF X:78 Y:0B P:NVUbdIzc SP:FB PPU:221, 92 CYC:10538 $F7C0:B0 09 BCS $F7CB A:FF X:78 Y:0B P:NVUbdIzc SP:F9 PPU:239, 92 CYC:10544 $F7C2:10 07 BPL $F7CB A:FF X:78 Y:0B P:NVUbdIzc SP:F9 PPU:245, 92 CYC:10546 $F7C4:C9 FF CMP #$FF A:FF X:78 Y:0B P:NVUbdIzc SP:F9 PPU:251, 92 CYC:10548 $F7C6:D0 03 BNE $F7CB A:FF X:78 Y:0B P:67 SP:F9 PPU:257, 92 CYC:10550 $F7C8:50 01 BVC $F7CB A:FF X:78 Y:0B P:67 SP:F9 PPU:263, 92 CYC:10552 $F7CA:60 RTS A:FF X:78 Y:0B P:67 SP:F9 PPU:269, 92 CYC:10554 $DC42:C8 INY A:FF X:78 Y:0B P:67 SP:FB PPU:287, 92 CYC:10560 $DC43:A9 00 LDA #$00 A:FF X:78 Y:0C P:65 SP:FB PPU:293, 92 CYC:10562 $DC45:85 78 STA $78 = #$AA A:00 X:78 Y:0C P:67 SP:FB PPU:299, 92 CYC:10564 $DC47:20 CE F7 JSR $F7CE A:00 X:78 Y:0C P:67 SP:FB PPU:308, 92 CYC:10567 $F7CE:38 SEC A:00 X:78 Y:0C P:67 SP:F9 PPU:326, 92 CYC:10573 $F7CF:B8 CLV A:00 X:78 Y:0C P:67 SP:F9 PPU:332, 92 CYC:10575 $F7D0:A9 00 LDA #$00 A:00 X:78 Y:0C P:nvUbdIZC SP:F9 PPU:338, 92 CYC:10577 $F7D2:60 RTS A:00 X:78 Y:0C P:nvUbdIZC SP:F9 PPU: 3, 93 CYC:10579 $DC4A:15 00 ORA $00,X @ 78 = #$00 A:00 X:78 Y:0C P:nvUbdIZC SP:FB PPU: 21, 93 CYC:10585 $DC4C:20 D3 F7 JSR $F7D3 A:00 X:78 Y:0C P:nvUbdIZC SP:FB PPU: 33, 93 CYC:10589 $F7D3:D0 07 BNE $F7DC A:00 X:78 Y:0C P:nvUbdIZC SP:F9 PPU: 51, 93 CYC:10595 $F7D5:70 05 BVS $F7DC A:00 X:78 Y:0C P:nvUbdIZC SP:F9 PPU: 57, 93 CYC:10597 $F7D7:90 03 BCC $F7DC A:00 X:78 Y:0C P:nvUbdIZC SP:F9 PPU: 63, 93 CYC:10599 $F7D9:30 01 BMI $F7DC A:00 X:78 Y:0C P:nvUbdIZC SP:F9 PPU: 69, 93 CYC:10601 $F7DB:60 RTS A:00 X:78 Y:0C P:nvUbdIZC SP:F9 PPU: 75, 93 CYC:10603 $DC4F:C8 INY A:00 X:78 Y:0C P:nvUbdIZC SP:FB PPU: 93, 93 CYC:10609 $DC50:A9 AA LDA #$AA A:00 X:78 Y:0D P:25 SP:FB PPU: 99, 93 CYC:10611 $DC52:85 78 STA $78 = #$00 A:AA X:78 Y:0D P:A5 SP:FB PPU:105, 93 CYC:10613 $DC54:20 DF F7 JSR $F7DF A:AA X:78 Y:0D P:A5 SP:FB PPU:114, 93 CYC:10616 $F7DF:18 CLC A:AA X:78 Y:0D P:A5 SP:F9 PPU:132, 93 CYC:10622 $F7E0:24 01 BIT $01 = #$FF A:AA X:78 Y:0D P:NvUbdIzc SP:F9 PPU:138, 93 CYC:10624 $F7E2:A9 55 LDA #$55 A:AA X:78 Y:0D P:NVUbdIzc SP:F9 PPU:147, 93 CYC:10627 $F7E4:60 RTS A:55 X:78 Y:0D P:64 SP:F9 PPU:153, 93 CYC:10629 $DC57:35 00 AND $00,X @ 78 = #$AA A:55 X:78 Y:0D P:64 SP:FB PPU:171, 93 CYC:10635 $DC59:20 E5 F7 JSR $F7E5 A:00 X:78 Y:0D P:nVUbdIZc SP:FB PPU:183, 93 CYC:10639 $F7E5:D0 07 BNE $F7EE A:00 X:78 Y:0D P:nVUbdIZc SP:F9 PPU:201, 93 CYC:10645 $F7E7:50 05 BVC $F7EE A:00 X:78 Y:0D P:nVUbdIZc SP:F9 PPU:207, 93 CYC:10647 $F7E9:B0 03 BCS $F7EE A:00 X:78 Y:0D P:nVUbdIZc SP:F9 PPU:213, 93 CYC:10649 $F7EB:30 01 BMI $F7EE A:00 X:78 Y:0D P:nVUbdIZc SP:F9 PPU:219, 93 CYC:10651 $F7ED:60 RTS A:00 X:78 Y:0D P:nVUbdIZc SP:F9 PPU:225, 93 CYC:10653 $DC5C:C8 INY A:00 X:78 Y:0D P:nVUbdIZc SP:FB PPU:243, 93 CYC:10659 $DC5D:A9 EF LDA #$EF A:00 X:78 Y:0E P:64 SP:FB PPU:249, 93 CYC:10661 $DC5F:85 78 STA $78 = #$AA A:EF X:78 Y:0E P:NVUbdIzc SP:FB PPU:255, 93 CYC:10663 $DC61:20 F1 F7 JSR $F7F1 A:EF X:78 Y:0E P:NVUbdIzc SP:FB PPU:264, 93 CYC:10666 $F7F1:38 SEC A:EF X:78 Y:0E P:NVUbdIzc SP:F9 PPU:282, 93 CYC:10672 $F7F2:B8 CLV A:EF X:78 Y:0E P:E5 SP:F9 PPU:288, 93 CYC:10674 $F7F3:A9 F8 LDA #$F8 A:EF X:78 Y:0E P:A5 SP:F9 PPU:294, 93 CYC:10676 $F7F5:60 RTS A:F8 X:78 Y:0E P:A5 SP:F9 PPU:300, 93 CYC:10678 $DC64:35 00 AND $00,X @ 78 = #$EF A:F8 X:78 Y:0E P:A5 SP:FB PPU:318, 93 CYC:10684 $DC66:20 F6 F7 JSR $F7F6 A:E8 X:78 Y:0E P:A5 SP:FB PPU:330, 93 CYC:10688 $F7F6:90 09 BCC $F801 A:E8 X:78 Y:0E P:A5 SP:F9 PPU: 7, 94 CYC:10694 $F7F8:10 07 BPL $F801 A:E8 X:78 Y:0E P:A5 SP:F9 PPU: 13, 94 CYC:10696 $F7FA:C9 E8 CMP #$E8 A:E8 X:78 Y:0E P:A5 SP:F9 PPU: 19, 94 CYC:10698 $F7FC:D0 03 BNE $F801 A:E8 X:78 Y:0E P:nvUbdIZC SP:F9 PPU: 25, 94 CYC:10700 $F7FE:70 01 BVS $F801 A:E8 X:78 Y:0E P:nvUbdIZC SP:F9 PPU: 31, 94 CYC:10702 $F800:60 RTS A:E8 X:78 Y:0E P:nvUbdIZC SP:F9 PPU: 37, 94 CYC:10704 $DC69:C8 INY A:E8 X:78 Y:0E P:nvUbdIZC SP:FB PPU: 55, 94 CYC:10710 $DC6A:A9 AA LDA #$AA A:E8 X:78 Y:0F P:25 SP:FB PPU: 61, 94 CYC:10712 $DC6C:85 78 STA $78 = #$EF A:AA X:78 Y:0F P:A5 SP:FB PPU: 67, 94 CYC:10714 $DC6E:20 04 F8 JSR $F804 A:AA X:78 Y:0F P:A5 SP:FB PPU: 76, 94 CYC:10717 $F804:18 CLC A:AA X:78 Y:0F P:A5 SP:F9 PPU: 94, 94 CYC:10723 $F805:24 01 BIT $01 = #$FF A:AA X:78 Y:0F P:NvUbdIzc SP:F9 PPU:100, 94 CYC:10725 $F807:A9 5F LDA #$5F A:AA X:78 Y:0F P:NVUbdIzc SP:F9 PPU:109, 94 CYC:10728 $F809:60 RTS A:5F X:78 Y:0F P:64 SP:F9 PPU:115, 94 CYC:10730 $DC71:55 00 EOR $00,X @ 78 = #$AA A:5F X:78 Y:0F P:64 SP:FB PPU:133, 94 CYC:10736 $DC73:20 0A F8 JSR $F80A A:F5 X:78 Y:0F P:NVUbdIzc SP:FB PPU:145, 94 CYC:10740 $F80A:B0 09 BCS $F815 A:F5 X:78 Y:0F P:NVUbdIzc SP:F9 PPU:163, 94 CYC:10746 $F80C:10 07 BPL $F815 A:F5 X:78 Y:0F P:NVUbdIzc SP:F9 PPU:169, 94 CYC:10748 $F80E:C9 F5 CMP #$F5 A:F5 X:78 Y:0F P:NVUbdIzc SP:F9 PPU:175, 94 CYC:10750 $F810:D0 03 BNE $F815 A:F5 X:78 Y:0F P:67 SP:F9 PPU:181, 94 CYC:10752 $F812:50 01 BVC $F815 A:F5 X:78 Y:0F P:67 SP:F9 PPU:187, 94 CYC:10754 $F814:60 RTS A:F5 X:78 Y:0F P:67 SP:F9 PPU:193, 94 CYC:10756 $DC76:C8 INY A:F5 X:78 Y:0F P:67 SP:FB PPU:211, 94 CYC:10762 $DC77:A9 70 LDA #$70 A:F5 X:78 Y:10 P:65 SP:FB PPU:217, 94 CYC:10764 $DC79:85 78 STA $78 = #$AA A:70 X:78 Y:10 P:65 SP:FB PPU:223, 94 CYC:10766 $DC7B:20 18 F8 JSR $F818 A:70 X:78 Y:10 P:65 SP:FB PPU:232, 94 CYC:10769 $F818:38 SEC A:70 X:78 Y:10 P:65 SP:F9 PPU:250, 94 CYC:10775 $F819:B8 CLV A:70 X:78 Y:10 P:65 SP:F9 PPU:256, 94 CYC:10777 $F81A:A9 70 LDA #$70 A:70 X:78 Y:10 P:25 SP:F9 PPU:262, 94 CYC:10779 $F81C:60 RTS A:70 X:78 Y:10 P:25 SP:F9 PPU:268, 94 CYC:10781 $DC7E:55 00 EOR $00,X @ 78 = #$70 A:70 X:78 Y:10 P:25 SP:FB PPU:286, 94 CYC:10787 $DC80:20 1D F8 JSR $F81D A:00 X:78 Y:10 P:nvUbdIZC SP:FB PPU:298, 94 CYC:10791 $F81D:D0 07 BNE $F826 A:00 X:78 Y:10 P:nvUbdIZC SP:F9 PPU:316, 94 CYC:10797 $F81F:70 05 BVS $F826 A:00 X:78 Y:10 P:nvUbdIZC SP:F9 PPU:322, 94 CYC:10799 $F821:90 03 BCC $F826 A:00 X:78 Y:10 P:nvUbdIZC SP:F9 PPU:328, 94 CYC:10801 $F823:30 01 BMI $F826 A:00 X:78 Y:10 P:nvUbdIZC SP:F9 PPU:334, 94 CYC:10803 $F825:60 RTS A:00 X:78 Y:10 P:nvUbdIZC SP:F9 PPU:340, 94 CYC:10805 $DC83:C8 INY A:00 X:78 Y:10 P:nvUbdIZC SP:FB PPU: 17, 95 CYC:10811 $DC84:A9 69 LDA #$69 A:00 X:78 Y:11 P:25 SP:FB PPU: 23, 95 CYC:10813 $DC86:85 78 STA $78 = #$70 A:69 X:78 Y:11 P:25 SP:FB PPU: 29, 95 CYC:10815 $DC88:20 29 F8 JSR $F829 A:69 X:78 Y:11 P:25 SP:FB PPU: 38, 95 CYC:10818 $F829:18 CLC A:69 X:78 Y:11 P:25 SP:F9 PPU: 56, 95 CYC:10824 $F82A:24 01 BIT $01 = #$FF A:69 X:78 Y:11 P:nvUbdIzc SP:F9 PPU: 62, 95 CYC:10826 $F82C:A9 00 LDA #$00 A:69 X:78 Y:11 P:NVUbdIzc SP:F9 PPU: 71, 95 CYC:10829 $F82E:60 RTS A:00 X:78 Y:11 P:nVUbdIZc SP:F9 PPU: 77, 95 CYC:10831 $DC8B:75 00 ADC $00,X @ 78 = #$69 A:00 X:78 Y:11 P:nVUbdIZc SP:FB PPU: 95, 95 CYC:10837 $DC8D:20 2F F8 JSR $F82F A:69 X:78 Y:11 P:nvUbdIzc SP:FB PPU:107, 95 CYC:10841 $F82F:30 09 BMI $F83A A:69 X:78 Y:11 P:nvUbdIzc SP:F9 PPU:125, 95 CYC:10847 $F831:B0 07 BCS $F83A A:69 X:78 Y:11 P:nvUbdIzc SP:F9 PPU:131, 95 CYC:10849 $F833:C9 69 CMP #$69 A:69 X:78 Y:11 P:nvUbdIzc SP:F9 PPU:137, 95 CYC:10851 $F835:D0 03 BNE $F83A A:69 X:78 Y:11 P:nvUbdIZC SP:F9 PPU:143, 95 CYC:10853 $F837:70 01 BVS $F83A A:69 X:78 Y:11 P:nvUbdIZC SP:F9 PPU:149, 95 CYC:10855 $F839:60 RTS A:69 X:78 Y:11 P:nvUbdIZC SP:F9 PPU:155, 95 CYC:10857 $DC90:C8 INY A:69 X:78 Y:11 P:nvUbdIZC SP:FB PPU:173, 95 CYC:10863 $DC91:20 3D F8 JSR $F83D A:69 X:78 Y:12 P:25 SP:FB PPU:179, 95 CYC:10865 $F83D:38 SEC A:69 X:78 Y:12 P:25 SP:F9 PPU:197, 95 CYC:10871 $F83E:24 01 BIT $01 = #$FF A:69 X:78 Y:12 P:25 SP:F9 PPU:203, 95 CYC:10873 $F840:A9 00 LDA #$00 A:69 X:78 Y:12 P:E5 SP:F9 PPU:212, 95 CYC:10876 $F842:60 RTS A:00 X:78 Y:12 P:67 SP:F9 PPU:218, 95 CYC:10878 $DC94:75 00 ADC $00,X @ 78 = #$69 A:00 X:78 Y:12 P:67 SP:FB PPU:236, 95 CYC:10884 $DC96:20 43 F8 JSR $F843 A:6A X:78 Y:12 P:nvUbdIzc SP:FB PPU:248, 95 CYC:10888 $F843:30 09 BMI $F84E A:6A X:78 Y:12 P:nvUbdIzc SP:F9 PPU:266, 95 CYC:10894 $F845:B0 07 BCS $F84E A:6A X:78 Y:12 P:nvUbdIzc SP:F9 PPU:272, 95 CYC:10896 $F847:C9 6A CMP #$6A A:6A X:78 Y:12 P:nvUbdIzc SP:F9 PPU:278, 95 CYC:10898 $F849:D0 03 BNE $F84E A:6A X:78 Y:12 P:nvUbdIZC SP:F9 PPU:284, 95 CYC:10900 $F84B:70 01 BVS $F84E A:6A X:78 Y:12 P:nvUbdIZC SP:F9 PPU:290, 95 CYC:10902 $F84D:60 RTS A:6A X:78 Y:12 P:nvUbdIZC SP:F9 PPU:296, 95 CYC:10904 $DC99:C8 INY A:6A X:78 Y:12 P:nvUbdIZC SP:FB PPU:314, 95 CYC:10910 $DC9A:A9 7F LDA #$7F A:6A X:78 Y:13 P:25 SP:FB PPU:320, 95 CYC:10912 $DC9C:85 78 STA $78 = #$69 A:7F X:78 Y:13 P:25 SP:FB PPU:326, 95 CYC:10914 $DC9E:20 51 F8 JSR $F851 A:7F X:78 Y:13 P:25 SP:FB PPU:335, 95 CYC:10917 $F851:38 SEC A:7F X:78 Y:13 P:25 SP:F9 PPU: 12, 96 CYC:10923 $F852:B8 CLV A:7F X:78 Y:13 P:25 SP:F9 PPU: 18, 96 CYC:10925 $F853:A9 7F LDA #$7F A:7F X:78 Y:13 P:25 SP:F9 PPU: 24, 96 CYC:10927 $F855:60 RTS A:7F X:78 Y:13 P:25 SP:F9 PPU: 30, 96 CYC:10929 $DCA1:75 00 ADC $00,X @ 78 = #$7F A:7F X:78 Y:13 P:25 SP:FB PPU: 48, 96 CYC:10935 $DCA3:20 56 F8 JSR $F856 A:FF X:78 Y:13 P:NVUbdIzc SP:FB PPU: 60, 96 CYC:10939 $F856:10 09 BPL $F861 A:FF X:78 Y:13 P:NVUbdIzc SP:F9 PPU: 78, 96 CYC:10945 $F858:B0 07 BCS $F861 A:FF X:78 Y:13 P:NVUbdIzc SP:F9 PPU: 84, 96 CYC:10947 $F85A:C9 FF CMP #$FF A:FF X:78 Y:13 P:NVUbdIzc SP:F9 PPU: 90, 96 CYC:10949 $F85C:D0 03 BNE $F861 A:FF X:78 Y:13 P:67 SP:F9 PPU: 96, 96 CYC:10951 $F85E:50 01 BVC $F861 A:FF X:78 Y:13 P:67 SP:F9 PPU:102, 96 CYC:10953 $F860:60 RTS A:FF X:78 Y:13 P:67 SP:F9 PPU:108, 96 CYC:10955 $DCA6:C8 INY A:FF X:78 Y:13 P:67 SP:FB PPU:126, 96 CYC:10961 $DCA7:A9 80 LDA #$80 A:FF X:78 Y:14 P:65 SP:FB PPU:132, 96 CYC:10963 $DCA9:85 78 STA $78 = #$7F A:80 X:78 Y:14 P:E5 SP:FB PPU:138, 96 CYC:10965 $DCAB:20 64 F8 JSR $F864 A:80 X:78 Y:14 P:E5 SP:FB PPU:147, 96 CYC:10968 $F864:18 CLC A:80 X:78 Y:14 P:E5 SP:F9 PPU:165, 96 CYC:10974 $F865:24 01 BIT $01 = #$FF A:80 X:78 Y:14 P:NVUbdIzc SP:F9 PPU:171, 96 CYC:10976 $F867:A9 7F LDA #$7F A:80 X:78 Y:14 P:NVUbdIzc SP:F9 PPU:180, 96 CYC:10979 $F869:60 RTS A:7F X:78 Y:14 P:64 SP:F9 PPU:186, 96 CYC:10981 $DCAE:75 00 ADC $00,X @ 78 = #$80 A:7F X:78 Y:14 P:64 SP:FB PPU:204, 96 CYC:10987 $DCB0:20 6A F8 JSR $F86A A:FF X:78 Y:14 P:NvUbdIzc SP:FB PPU:216, 96 CYC:10991 $F86A:10 09 BPL $F875 A:FF X:78 Y:14 P:NvUbdIzc SP:F9 PPU:234, 96 CYC:10997 $F86C:B0 07 BCS $F875 A:FF X:78 Y:14 P:NvUbdIzc SP:F9 PPU:240, 96 CYC:10999 $F86E:C9 FF CMP #$FF A:FF X:78 Y:14 P:NvUbdIzc SP:F9 PPU:246, 96 CYC:11001 $F870:D0 03 BNE $F875 A:FF X:78 Y:14 P:nvUbdIZC SP:F9 PPU:252, 96 CYC:11003 $F872:70 01 BVS $F875 A:FF X:78 Y:14 P:nvUbdIZC SP:F9 PPU:258, 96 CYC:11005 $F874:60 RTS A:FF X:78 Y:14 P:nvUbdIZC SP:F9 PPU:264, 96 CYC:11007 $DCB3:C8 INY A:FF X:78 Y:14 P:nvUbdIZC SP:FB PPU:282, 96 CYC:11013 $DCB4:20 78 F8 JSR $F878 A:FF X:78 Y:15 P:25 SP:FB PPU:288, 96 CYC:11015 $F878:38 SEC A:FF X:78 Y:15 P:25 SP:F9 PPU:306, 96 CYC:11021 $F879:B8 CLV A:FF X:78 Y:15 P:25 SP:F9 PPU:312, 96 CYC:11023 $F87A:A9 7F LDA #$7F A:FF X:78 Y:15 P:25 SP:F9 PPU:318, 96 CYC:11025 $F87C:60 RTS A:7F X:78 Y:15 P:25 SP:F9 PPU:324, 96 CYC:11027 $DCB7:75 00 ADC $00,X @ 78 = #$80 A:7F X:78 Y:15 P:25 SP:FB PPU: 1, 97 CYC:11033 $DCB9:20 7D F8 JSR $F87D A:00 X:78 Y:15 P:nvUbdIZC SP:FB PPU: 13, 97 CYC:11037 $F87D:D0 07 BNE $F886 A:00 X:78 Y:15 P:nvUbdIZC SP:F9 PPU: 31, 97 CYC:11043 $F87F:30 05 BMI $F886 A:00 X:78 Y:15 P:nvUbdIZC SP:F9 PPU: 37, 97 CYC:11045 $F881:70 03 BVS $F886 A:00 X:78 Y:15 P:nvUbdIZC SP:F9 PPU: 43, 97 CYC:11047 $F883:90 01 BCC $F886 A:00 X:78 Y:15 P:nvUbdIZC SP:F9 PPU: 49, 97 CYC:11049 $F885:60 RTS A:00 X:78 Y:15 P:nvUbdIZC SP:F9 PPU: 55, 97 CYC:11051 $DCBC:C8 INY A:00 X:78 Y:15 P:nvUbdIZC SP:FB PPU: 73, 97 CYC:11057 $DCBD:A9 40 LDA #$40 A:00 X:78 Y:16 P:25 SP:FB PPU: 79, 97 CYC:11059 $DCBF:85 78 STA $78 = #$80 A:40 X:78 Y:16 P:25 SP:FB PPU: 85, 97 CYC:11061 $DCC1:20 89 F8 JSR $F889 A:40 X:78 Y:16 P:25 SP:FB PPU: 94, 97 CYC:11064 $F889:24 01 BIT $01 = #$FF A:40 X:78 Y:16 P:25 SP:F9 PPU:112, 97 CYC:11070 $F88B:A9 40 LDA #$40 A:40 X:78 Y:16 P:E5 SP:F9 PPU:121, 97 CYC:11073 $F88D:60 RTS A:40 X:78 Y:16 P:65 SP:F9 PPU:127, 97 CYC:11075 $DCC4:D5 00 CMP $00,X @ 78 = #$40 A:40 X:78 Y:16 P:65 SP:FB PPU:145, 97 CYC:11081 $DCC6:20 8E F8 JSR $F88E A:40 X:78 Y:16 P:67 SP:FB PPU:157, 97 CYC:11085 $F88E:30 07 BMI $F897 A:40 X:78 Y:16 P:67 SP:F9 PPU:175, 97 CYC:11091 $F890:90 05 BCC $F897 A:40 X:78 Y:16 P:67 SP:F9 PPU:181, 97 CYC:11093 $F892:D0 03 BNE $F897 A:40 X:78 Y:16 P:67 SP:F9 PPU:187, 97 CYC:11095 $F894:50 01 BVC $F897 A:40 X:78 Y:16 P:67 SP:F9 PPU:193, 97 CYC:11097 $F896:60 RTS A:40 X:78 Y:16 P:67 SP:F9 PPU:199, 97 CYC:11099 $DCC9:C8 INY A:40 X:78 Y:16 P:67 SP:FB PPU:217, 97 CYC:11105 $DCCA:48 PHA A:40 X:78 Y:17 P:65 SP:FB PPU:223, 97 CYC:11107 $DCCB:A9 3F LDA #$3F A:40 X:78 Y:17 P:65 SP:FA PPU:232, 97 CYC:11110 $DCCD:85 78 STA $78 = #$40 A:3F X:78 Y:17 P:65 SP:FA PPU:238, 97 CYC:11112 $DCCF:68 PLA A:3F X:78 Y:17 P:65 SP:FA PPU:247, 97 CYC:11115 $DCD0:20 9A F8 JSR $F89A A:40 X:78 Y:17 P:65 SP:FB PPU:259, 97 CYC:11119 $F89A:B8 CLV A:40 X:78 Y:17 P:65 SP:F9 PPU:277, 97 CYC:11125 $F89B:60 RTS A:40 X:78 Y:17 P:25 SP:F9 PPU:283, 97 CYC:11127 $DCD3:D5 00 CMP $00,X @ 78 = #$3F A:40 X:78 Y:17 P:25 SP:FB PPU:301, 97 CYC:11133 $DCD5:20 9C F8 JSR $F89C A:40 X:78 Y:17 P:25 SP:FB PPU:313, 97 CYC:11137 $F89C:F0 07 BEQ $F8A5 A:40 X:78 Y:17 P:25 SP:F9 PPU:331, 97 CYC:11143 $F89E:30 05 BMI $F8A5 A:40 X:78 Y:17 P:25 SP:F9 PPU:337, 97 CYC:11145 $F8A0:90 03 BCC $F8A5 A:40 X:78 Y:17 P:25 SP:F9 PPU: 2, 98 CYC:11147 $F8A2:70 01 BVS $F8A5 A:40 X:78 Y:17 P:25 SP:F9 PPU: 8, 98 CYC:11149 $F8A4:60 RTS A:40 X:78 Y:17 P:25 SP:F9 PPU: 14, 98 CYC:11151 $DCD8:C8 INY A:40 X:78 Y:17 P:25 SP:FB PPU: 32, 98 CYC:11157 $DCD9:48 PHA A:40 X:78 Y:18 P:25 SP:FB PPU: 38, 98 CYC:11159 $DCDA:A9 41 LDA #$41 A:40 X:78 Y:18 P:25 SP:FA PPU: 47, 98 CYC:11162 $DCDC:85 78 STA $78 = #$3F A:41 X:78 Y:18 P:25 SP:FA PPU: 53, 98 CYC:11164 $DCDE:68 PLA A:41 X:78 Y:18 P:25 SP:FA PPU: 62, 98 CYC:11167 $DCDF:D5 00 CMP $00,X @ 78 = #$41 A:40 X:78 Y:18 P:25 SP:FB PPU: 74, 98 CYC:11171 $DCE1:20 A8 F8 JSR $F8A8 A:40 X:78 Y:18 P:NvUbdIzc SP:FB PPU: 86, 98 CYC:11175 $F8A8:F0 05 BEQ $F8AF A:40 X:78 Y:18 P:NvUbdIzc SP:F9 PPU:104, 98 CYC:11181 $F8AA:10 03 BPL $F8AF A:40 X:78 Y:18 P:NvUbdIzc SP:F9 PPU:110, 98 CYC:11183 $F8AC:10 01 BPL $F8AF A:40 X:78 Y:18 P:NvUbdIzc SP:F9 PPU:116, 98 CYC:11185 $F8AE:60 RTS A:40 X:78 Y:18 P:NvUbdIzc SP:F9 PPU:122, 98 CYC:11187 $DCE4:C8 INY A:40 X:78 Y:18 P:NvUbdIzc SP:FB PPU:140, 98 CYC:11193 $DCE5:48 PHA A:40 X:78 Y:19 P:nvUbdIzc SP:FB PPU:146, 98 CYC:11195 $DCE6:A9 00 LDA #$00 A:40 X:78 Y:19 P:nvUbdIzc SP:FA PPU:155, 98 CYC:11198 $DCE8:85 78 STA $78 = #$41 A:00 X:78 Y:19 P:nvUbdIZc SP:FA PPU:161, 98 CYC:11200 $DCEA:68 PLA A:00 X:78 Y:19 P:nvUbdIZc SP:FA PPU:170, 98 CYC:11203 $DCEB:20 B2 F8 JSR $F8B2 A:40 X:78 Y:19 P:nvUbdIzc SP:FB PPU:182, 98 CYC:11207 $F8B2:A9 80 LDA #$80 A:40 X:78 Y:19 P:nvUbdIzc SP:F9 PPU:200, 98 CYC:11213 $F8B4:60 RTS A:80 X:78 Y:19 P:NvUbdIzc SP:F9 PPU:206, 98 CYC:11215 $DCEE:D5 00 CMP $00,X @ 78 = #$00 A:80 X:78 Y:19 P:NvUbdIzc SP:FB PPU:224, 98 CYC:11221 $DCF0:20 B5 F8 JSR $F8B5 A:80 X:78 Y:19 P:A5 SP:FB PPU:236, 98 CYC:11225 $F8B5:F0 05 BEQ $F8BC A:80 X:78 Y:19 P:A5 SP:F9 PPU:254, 98 CYC:11231 $F8B7:10 03 BPL $F8BC A:80 X:78 Y:19 P:A5 SP:F9 PPU:260, 98 CYC:11233 $F8B9:90 01 BCC $F8BC A:80 X:78 Y:19 P:A5 SP:F9 PPU:266, 98 CYC:11235 $F8BB:60 RTS A:80 X:78 Y:19 P:A5 SP:F9 PPU:272, 98 CYC:11237 $DCF3:C8 INY A:80 X:78 Y:19 P:A5 SP:FB PPU:290, 98 CYC:11243 $DCF4:48 PHA A:80 X:78 Y:1A P:25 SP:FB PPU:296, 98 CYC:11245 $DCF5:A9 80 LDA #$80 A:80 X:78 Y:1A P:25 SP:FA PPU:305, 98 CYC:11248 $DCF7:85 78 STA $78 = #$00 A:80 X:78 Y:1A P:A5 SP:FA PPU:311, 98 CYC:11250 $DCF9:68 PLA A:80 X:78 Y:1A P:A5 SP:FA PPU:320, 98 CYC:11253 $DCFA:D5 00 CMP $00,X @ 78 = #$80 A:80 X:78 Y:1A P:A5 SP:FB PPU:332, 98 CYC:11257 $DCFC:20 BF F8 JSR $F8BF A:80 X:78 Y:1A P:nvUbdIZC SP:FB PPU: 3, 99 CYC:11261 $F8BF:D0 05 BNE $F8C6 A:80 X:78 Y:1A P:nvUbdIZC SP:F9 PPU: 21, 99 CYC:11267 $F8C1:30 03 BMI $F8C6 A:80 X:78 Y:1A P:nvUbdIZC SP:F9 PPU: 27, 99 CYC:11269 $F8C3:90 01 BCC $F8C6 A:80 X:78 Y:1A P:nvUbdIZC SP:F9 PPU: 33, 99 CYC:11271 $F8C5:60 RTS A:80 X:78 Y:1A P:nvUbdIZC SP:F9 PPU: 39, 99 CYC:11273 $DCFF:C8 INY A:80 X:78 Y:1A P:nvUbdIZC SP:FB PPU: 57, 99 CYC:11279 $DD00:48 PHA A:80 X:78 Y:1B P:25 SP:FB PPU: 63, 99 CYC:11281 $DD01:A9 81 LDA #$81 A:80 X:78 Y:1B P:25 SP:FA PPU: 72, 99 CYC:11284 $DD03:85 78 STA $78 = #$80 A:81 X:78 Y:1B P:A5 SP:FA PPU: 78, 99 CYC:11286 $DD05:68 PLA A:81 X:78 Y:1B P:A5 SP:FA PPU: 87, 99 CYC:11289 $DD06:D5 00 CMP $00,X @ 78 = #$81 A:80 X:78 Y:1B P:A5 SP:FB PPU: 99, 99 CYC:11293 $DD08:20 C9 F8 JSR $F8C9 A:80 X:78 Y:1B P:NvUbdIzc SP:FB PPU:111, 99 CYC:11297 $F8C9:B0 05 BCS $F8D0 A:80 X:78 Y:1B P:NvUbdIzc SP:F9 PPU:129, 99 CYC:11303 $F8CB:F0 03 BEQ $F8D0 A:80 X:78 Y:1B P:NvUbdIzc SP:F9 PPU:135, 99 CYC:11305 $F8CD:10 01 BPL $F8D0 A:80 X:78 Y:1B P:NvUbdIzc SP:F9 PPU:141, 99 CYC:11307 $F8CF:60 RTS A:80 X:78 Y:1B P:NvUbdIzc SP:F9 PPU:147, 99 CYC:11309 $DD0B:C8 INY A:80 X:78 Y:1B P:NvUbdIzc SP:FB PPU:165, 99 CYC:11315 $DD0C:48 PHA A:80 X:78 Y:1C P:nvUbdIzc SP:FB PPU:171, 99 CYC:11317 $DD0D:A9 7F LDA #$7F A:80 X:78 Y:1C P:nvUbdIzc SP:FA PPU:180, 99 CYC:11320 $DD0F:85 78 STA $78 = #$81 A:7F X:78 Y:1C P:nvUbdIzc SP:FA PPU:186, 99 CYC:11322 $DD11:68 PLA A:7F X:78 Y:1C P:nvUbdIzc SP:FA PPU:195, 99 CYC:11325 $DD12:D5 00 CMP $00,X @ 78 = #$7F A:80 X:78 Y:1C P:NvUbdIzc SP:FB PPU:207, 99 CYC:11329 $DD14:20 D3 F8 JSR $F8D3 A:80 X:78 Y:1C P:25 SP:FB PPU:219, 99 CYC:11333 $F8D3:90 05 BCC $F8DA A:80 X:78 Y:1C P:25 SP:F9 PPU:237, 99 CYC:11339 $F8D5:F0 03 BEQ $F8DA A:80 X:78 Y:1C P:25 SP:F9 PPU:243, 99 CYC:11341 $F8D7:30 01 BMI $F8DA A:80 X:78 Y:1C P:25 SP:F9 PPU:249, 99 CYC:11343 $F8D9:60 RTS A:80 X:78 Y:1C P:25 SP:F9 PPU:255, 99 CYC:11345 $DD17:C8 INY A:80 X:78 Y:1C P:25 SP:FB PPU:273, 99 CYC:11351 $DD18:A9 40 LDA #$40 A:80 X:78 Y:1D P:25 SP:FB PPU:279, 99 CYC:11353 $DD1A:85 78 STA $78 = #$7F A:40 X:78 Y:1D P:25 SP:FB PPU:285, 99 CYC:11355 $DD1C:20 31 F9 JSR $F931 A:40 X:78 Y:1D P:25 SP:FB PPU:294, 99 CYC:11358 $F931:24 01 BIT $01 = #$FF A:40 X:78 Y:1D P:25 SP:F9 PPU:312, 99 CYC:11364 $F933:A9 40 LDA #$40 A:40 X:78 Y:1D P:E5 SP:F9 PPU:321, 99 CYC:11367 $F935:38 SEC A:40 X:78 Y:1D P:65 SP:F9 PPU:327, 99 CYC:11369 $F936:60 RTS A:40 X:78 Y:1D P:65 SP:F9 PPU:333, 99 CYC:11371 $DD1F:F5 00 SBC $00,X @ 78 = #$40 A:40 X:78 Y:1D P:65 SP:FB PPU: 10,100 CYC:11377 $DD21:20 37 F9 JSR $F937 A:00 X:78 Y:1D P:nvUbdIZC SP:FB PPU: 22,100 CYC:11381 $F937:30 0B BMI $F944 A:00 X:78 Y:1D P:nvUbdIZC SP:F9 PPU: 40,100 CYC:11387 $F939:90 09 BCC $F944 A:00 X:78 Y:1D P:nvUbdIZC SP:F9 PPU: 46,100 CYC:11389 $F93B:D0 07 BNE $F944 A:00 X:78 Y:1D P:nvUbdIZC SP:F9 PPU: 52,100 CYC:11391 $F93D:70 05 BVS $F944 A:00 X:78 Y:1D P:nvUbdIZC SP:F9 PPU: 58,100 CYC:11393 $F93F:C9 00 CMP #$00 A:00 X:78 Y:1D P:nvUbdIZC SP:F9 PPU: 64,100 CYC:11395 $F941:D0 01 BNE $F944 A:00 X:78 Y:1D P:nvUbdIZC SP:F9 PPU: 70,100 CYC:11397 $F943:60 RTS A:00 X:78 Y:1D P:nvUbdIZC SP:F9 PPU: 76,100 CYC:11399 $DD24:C8 INY A:00 X:78 Y:1D P:nvUbdIZC SP:FB PPU: 94,100 CYC:11405 $DD25:A9 3F LDA #$3F A:00 X:78 Y:1E P:25 SP:FB PPU:100,100 CYC:11407 $DD27:85 78 STA $78 = #$40 A:3F X:78 Y:1E P:25 SP:FB PPU:106,100 CYC:11409 $DD29:20 47 F9 JSR $F947 A:3F X:78 Y:1E P:25 SP:FB PPU:115,100 CYC:11412 $F947:B8 CLV A:3F X:78 Y:1E P:25 SP:F9 PPU:133,100 CYC:11418 $F948:38 SEC A:3F X:78 Y:1E P:25 SP:F9 PPU:139,100 CYC:11420 $F949:A9 40 LDA #$40 A:3F X:78 Y:1E P:25 SP:F9 PPU:145,100 CYC:11422 $F94B:60 RTS A:40 X:78 Y:1E P:25 SP:F9 PPU:151,100 CYC:11424 $DD2C:F5 00 SBC $00,X @ 78 = #$3F A:40 X:78 Y:1E P:25 SP:FB PPU:169,100 CYC:11430 $DD2E:20 4C F9 JSR $F94C A:01 X:78 Y:1E P:25 SP:FB PPU:181,100 CYC:11434 $F94C:F0 0B BEQ $F959 A:01 X:78 Y:1E P:25 SP:F9 PPU:199,100 CYC:11440 $F94E:30 09 BMI $F959 A:01 X:78 Y:1E P:25 SP:F9 PPU:205,100 CYC:11442 $F950:90 07 BCC $F959 A:01 X:78 Y:1E P:25 SP:F9 PPU:211,100 CYC:11444 $F952:70 05 BVS $F959 A:01 X:78 Y:1E P:25 SP:F9 PPU:217,100 CYC:11446 $F954:C9 01 CMP #$01 A:01 X:78 Y:1E P:25 SP:F9 PPU:223,100 CYC:11448 $F956:D0 01 BNE $F959 A:01 X:78 Y:1E P:nvUbdIZC SP:F9 PPU:229,100 CYC:11450 $F958:60 RTS A:01 X:78 Y:1E P:nvUbdIZC SP:F9 PPU:235,100 CYC:11452 $DD31:C8 INY A:01 X:78 Y:1E P:nvUbdIZC SP:FB PPU:253,100 CYC:11458 $DD32:A9 41 LDA #$41 A:01 X:78 Y:1F P:25 SP:FB PPU:259,100 CYC:11460 $DD34:85 78 STA $78 = #$3F A:41 X:78 Y:1F P:25 SP:FB PPU:265,100 CYC:11462 $DD36:20 5C F9 JSR $F95C A:41 X:78 Y:1F P:25 SP:FB PPU:274,100 CYC:11465 $F95C:A9 40 LDA #$40 A:41 X:78 Y:1F P:25 SP:F9 PPU:292,100 CYC:11471 $F95E:38 SEC A:40 X:78 Y:1F P:25 SP:F9 PPU:298,100 CYC:11473 $F95F:24 01 BIT $01 = #$FF A:40 X:78 Y:1F P:25 SP:F9 PPU:304,100 CYC:11475 $F961:60 RTS A:40 X:78 Y:1F P:E5 SP:F9 PPU:313,100 CYC:11478 $DD39:F5 00 SBC $00,X @ 78 = #$41 A:40 X:78 Y:1F P:E5 SP:FB PPU:331,100 CYC:11484 $DD3B:20 62 F9 JSR $F962 A:FF X:78 Y:1F P:NvUbdIzc SP:FB PPU: 2,101 CYC:11488 $F962:B0 0B BCS $F96F A:FF X:78 Y:1F P:NvUbdIzc SP:F9 PPU: 20,101 CYC:11494 $F964:F0 09 BEQ $F96F A:FF X:78 Y:1F P:NvUbdIzc SP:F9 PPU: 26,101 CYC:11496 $F966:10 07 BPL $F96F A:FF X:78 Y:1F P:NvUbdIzc SP:F9 PPU: 32,101 CYC:11498 $F968:70 05 BVS $F96F A:FF X:78 Y:1F P:NvUbdIzc SP:F9 PPU: 38,101 CYC:11500 $F96A:C9 FF CMP #$FF A:FF X:78 Y:1F P:NvUbdIzc SP:F9 PPU: 44,101 CYC:11502 $F96C:D0 01 BNE $F96F A:FF X:78 Y:1F P:nvUbdIZC SP:F9 PPU: 50,101 CYC:11504 $F96E:60 RTS A:FF X:78 Y:1F P:nvUbdIZC SP:F9 PPU: 56,101 CYC:11506 $DD3E:C8 INY A:FF X:78 Y:1F P:nvUbdIZC SP:FB PPU: 74,101 CYC:11512 $DD3F:A9 00 LDA #$00 A:FF X:78 Y:20 P:25 SP:FB PPU: 80,101 CYC:11514 $DD41:85 78 STA $78 = #$41 A:00 X:78 Y:20 P:nvUbdIZC SP:FB PPU: 86,101 CYC:11516 $DD43:20 72 F9 JSR $F972 A:00 X:78 Y:20 P:nvUbdIZC SP:FB PPU: 95,101 CYC:11519 $F972:18 CLC A:00 X:78 Y:20 P:nvUbdIZC SP:F9 PPU:113,101 CYC:11525 $F973:A9 80 LDA #$80 A:00 X:78 Y:20 P:nvUbdIZc SP:F9 PPU:119,101 CYC:11527 $F975:60 RTS A:80 X:78 Y:20 P:NvUbdIzc SP:F9 PPU:125,101 CYC:11529 $DD46:F5 00 SBC $00,X @ 78 = #$00 A:80 X:78 Y:20 P:NvUbdIzc SP:FB PPU:143,101 CYC:11535 $DD48:20 76 F9 JSR $F976 A:7F X:78 Y:20 P:65 SP:FB PPU:155,101 CYC:11539 $F976:90 05 BCC $F97D A:7F X:78 Y:20 P:65 SP:F9 PPU:173,101 CYC:11545 $F978:C9 7F CMP #$7F A:7F X:78 Y:20 P:65 SP:F9 PPU:179,101 CYC:11547 $F97A:D0 01 BNE $F97D A:7F X:78 Y:20 P:67 SP:F9 PPU:185,101 CYC:11549 $F97C:60 RTS A:7F X:78 Y:20 P:67 SP:F9 PPU:191,101 CYC:11551 $DD4B:C8 INY A:7F X:78 Y:20 P:67 SP:FB PPU:209,101 CYC:11557 $DD4C:A9 7F LDA #$7F A:7F X:78 Y:21 P:65 SP:FB PPU:215,101 CYC:11559 $DD4E:85 78 STA $78 = #$00 A:7F X:78 Y:21 P:65 SP:FB PPU:221,101 CYC:11561 $DD50:20 80 F9 JSR $F980 A:7F X:78 Y:21 P:65 SP:FB PPU:230,101 CYC:11564 $F980:38 SEC A:7F X:78 Y:21 P:65 SP:F9 PPU:248,101 CYC:11570 $F981:A9 81 LDA #$81 A:7F X:78 Y:21 P:65 SP:F9 PPU:254,101 CYC:11572 $F983:60 RTS A:81 X:78 Y:21 P:E5 SP:F9 PPU:260,101 CYC:11574 $DD53:F5 00 SBC $00,X @ 78 = #$7F A:81 X:78 Y:21 P:E5 SP:FB PPU:278,101 CYC:11580 $DD55:20 84 F9 JSR $F984 A:02 X:78 Y:21 P:65 SP:FB PPU:290,101 CYC:11584 $F984:50 07 BVC $F98D A:02 X:78 Y:21 P:65 SP:F9 PPU:308,101 CYC:11590 $F986:90 05 BCC $F98D A:02 X:78 Y:21 P:65 SP:F9 PPU:314,101 CYC:11592 $F988:C9 02 CMP #$02 A:02 X:78 Y:21 P:65 SP:F9 PPU:320,101 CYC:11594 $F98A:D0 01 BNE $F98D A:02 X:78 Y:21 P:67 SP:F9 PPU:326,101 CYC:11596 $F98C:60 RTS A:02 X:78 Y:21 P:67 SP:F9 PPU:332,101 CYC:11598 $DD58:A9 AA LDA #$AA A:02 X:78 Y:21 P:67 SP:FB PPU: 9,102 CYC:11604 $DD5A:85 33 STA $33 = #$44 A:AA X:78 Y:21 P:E5 SP:FB PPU: 15,102 CYC:11606 $DD5C:A9 BB LDA #$BB A:AA X:78 Y:21 P:E5 SP:FB PPU: 24,102 CYC:11609 $DD5E:85 89 STA $89 = #$BB A:BB X:78 Y:21 P:E5 SP:FB PPU: 30,102 CYC:11611 $DD60:A2 00 LDX #$00 A:BB X:78 Y:21 P:E5 SP:FB PPU: 39,102 CYC:11614 $DD62:A0 66 LDY #$66 A:BB X:00 Y:21 P:67 SP:FB PPU: 45,102 CYC:11616 $DD64:24 01 BIT $01 = #$FF A:BB X:00 Y:66 P:65 SP:FB PPU: 51,102 CYC:11618 $DD66:38 SEC A:BB X:00 Y:66 P:E5 SP:FB PPU: 60,102 CYC:11621 $DD67:A9 00 LDA #$00 A:BB X:00 Y:66 P:E5 SP:FB PPU: 66,102 CYC:11623 $DD69:B5 33 LDA $33,X @ 33 = #$AA A:00 X:00 Y:66 P:67 SP:FB PPU: 72,102 CYC:11625 $DD6B:10 12 BPL $DD7F A:AA X:00 Y:66 P:E5 SP:FB PPU: 84,102 CYC:11629 $DD6D:F0 10 BEQ $DD7F A:AA X:00 Y:66 P:E5 SP:FB PPU: 90,102 CYC:11631 $DD6F:50 0E BVC $DD7F A:AA X:00 Y:66 P:E5 SP:FB PPU: 96,102 CYC:11633 $DD71:90 0C BCC $DD7F A:AA X:00 Y:66 P:E5 SP:FB PPU:102,102 CYC:11635 $DD73:C0 66 CPY #$66 A:AA X:00 Y:66 P:E5 SP:FB PPU:108,102 CYC:11637 $DD75:D0 08 BNE $DD7F A:AA X:00 Y:66 P:67 SP:FB PPU:114,102 CYC:11639 $DD77:E0 00 CPX #$00 A:AA X:00 Y:66 P:67 SP:FB PPU:120,102 CYC:11641 $DD79:D0 04 BNE $DD7F A:AA X:00 Y:66 P:67 SP:FB PPU:126,102 CYC:11643 $DD7B:C9 AA CMP #$AA A:AA X:00 Y:66 P:67 SP:FB PPU:132,102 CYC:11645 $DD7D:F0 04 BEQ $DD83 A:AA X:00 Y:66 P:67 SP:FB PPU:138,102 CYC:11647 $DD83:A2 8A LDX #$8A A:AA X:00 Y:66 P:67 SP:FB PPU:147,102 CYC:11650 $DD85:A0 66 LDY #$66 A:AA X:8A Y:66 P:E5 SP:FB PPU:153,102 CYC:11652 $DD87:B8 CLV A:AA X:8A Y:66 P:65 SP:FB PPU:159,102 CYC:11654 $DD88:18 CLC A:AA X:8A Y:66 P:25 SP:FB PPU:165,102 CYC:11656 $DD89:A9 00 LDA #$00 A:AA X:8A Y:66 P:nvUbdIzc SP:FB PPU:171,102 CYC:11658 $DD8B:B5 FF LDA $FF,X @ 89 = #$BB A:00 X:8A Y:66 P:nvUbdIZc SP:FB PPU:177,102 CYC:11660 $DD8D:10 12 BPL $DDA1 A:BB X:8A Y:66 P:NvUbdIzc SP:FB PPU:189,102 CYC:11664 $DD8F:F0 10 BEQ $DDA1 A:BB X:8A Y:66 P:NvUbdIzc SP:FB PPU:195,102 CYC:11666 $DD91:70 0E BVS $DDA1 A:BB X:8A Y:66 P:NvUbdIzc SP:FB PPU:201,102 CYC:11668 $DD93:B0 0C BCS $DDA1 A:BB X:8A Y:66 P:NvUbdIzc SP:FB PPU:207,102 CYC:11670 $DD95:C9 BB CMP #$BB A:BB X:8A Y:66 P:NvUbdIzc SP:FB PPU:213,102 CYC:11672 $DD97:D0 08 BNE $DDA1 A:BB X:8A Y:66 P:nvUbdIZC SP:FB PPU:219,102 CYC:11674 $DD99:C0 66 CPY #$66 A:BB X:8A Y:66 P:nvUbdIZC SP:FB PPU:225,102 CYC:11676 $DD9B:D0 04 BNE $DDA1 A:BB X:8A Y:66 P:nvUbdIZC SP:FB PPU:231,102 CYC:11678 $DD9D:E0 8A CPX #$8A A:BB X:8A Y:66 P:nvUbdIZC SP:FB PPU:237,102 CYC:11680 $DD9F:F0 04 BEQ $DDA5 A:BB X:8A Y:66 P:nvUbdIZC SP:FB PPU:243,102 CYC:11682 $DDA5:24 01 BIT $01 = #$FF A:BB X:8A Y:66 P:nvUbdIZC SP:FB PPU:252,102 CYC:11685 $DDA7:38 SEC A:BB X:8A Y:66 P:E5 SP:FB PPU:261,102 CYC:11688 $DDA8:A9 44 LDA #$44 A:BB X:8A Y:66 P:E5 SP:FB PPU:267,102 CYC:11690 $DDAA:A2 00 LDX #$00 A:44 X:8A Y:66 P:65 SP:FB PPU:273,102 CYC:11692 $DDAC:95 33 STA $33,X @ 33 = #$AA A:44 X:00 Y:66 P:67 SP:FB PPU:279,102 CYC:11694 $DDAE:A5 33 LDA $33 = #$44 A:44 X:00 Y:66 P:67 SP:FB PPU:291,102 CYC:11698 $DDB0:90 18 BCC $DDCA A:44 X:00 Y:66 P:65 SP:FB PPU:300,102 CYC:11701 $DDB2:C9 44 CMP #$44 A:44 X:00 Y:66 P:65 SP:FB PPU:306,102 CYC:11703 $DDB4:D0 14 BNE $DDCA A:44 X:00 Y:66 P:67 SP:FB PPU:312,102 CYC:11705 $DDB6:50 12 BVC $DDCA A:44 X:00 Y:66 P:67 SP:FB PPU:318,102 CYC:11707 $DDB8:18 CLC A:44 X:00 Y:66 P:67 SP:FB PPU:324,102 CYC:11709 $DDB9:B8 CLV A:44 X:00 Y:66 P:nVUbdIZc SP:FB PPU:330,102 CYC:11711 $DDBA:A9 99 LDA #$99 A:44 X:00 Y:66 P:nvUbdIZc SP:FB PPU:336,102 CYC:11713 $DDBC:A2 80 LDX #$80 A:99 X:00 Y:66 P:NvUbdIzc SP:FB PPU: 1,103 CYC:11715 $DDBE:95 85 STA $85,X @ 05 = #$99 A:99 X:80 Y:66 P:NvUbdIzc SP:FB PPU: 7,103 CYC:11717 $DDC0:A5 05 LDA $05 = #$99 A:99 X:80 Y:66 P:NvUbdIzc SP:FB PPU: 19,103 CYC:11721 $DDC2:B0 06 BCS $DDCA A:99 X:80 Y:66 P:NvUbdIzc SP:FB PPU: 28,103 CYC:11724 $DDC4:C9 99 CMP #$99 A:99 X:80 Y:66 P:NvUbdIzc SP:FB PPU: 34,103 CYC:11726 $DDC6:D0 02 BNE $DDCA A:99 X:80 Y:66 P:nvUbdIZC SP:FB PPU: 40,103 CYC:11728 $DDC8:50 04 BVC $DDCE A:99 X:80 Y:66 P:nvUbdIZC SP:FB PPU: 46,103 CYC:11730 $DDCE:A0 25 LDY #$25 A:99 X:80 Y:66 P:nvUbdIZC SP:FB PPU: 55,103 CYC:11733 $DDD0:A2 78 LDX #$78 A:99 X:80 Y:25 P:25 SP:FB PPU: 61,103 CYC:11735 $DDD2:20 90 F9 JSR $F990 A:99 X:78 Y:25 P:25 SP:FB PPU: 67,103 CYC:11737 $F990:A2 55 LDX #$55 A:99 X:78 Y:25 P:25 SP:F9 PPU: 85,103 CYC:11743 $F992:A9 FF LDA #$FF A:99 X:55 Y:25 P:25 SP:F9 PPU: 91,103 CYC:11745 $F994:85 01 STA $01 = #$FF A:FF X:55 Y:25 P:A5 SP:F9 PPU: 97,103 CYC:11747 $F996:EA NOP A:FF X:55 Y:25 P:A5 SP:F9 PPU:106,103 CYC:11750 $F997:24 01 BIT $01 = #$FF A:FF X:55 Y:25 P:A5 SP:F9 PPU:112,103 CYC:11752 $F999:38 SEC A:FF X:55 Y:25 P:E5 SP:F9 PPU:121,103 CYC:11755 $F99A:A9 01 LDA #$01 A:FF X:55 Y:25 P:E5 SP:F9 PPU:127,103 CYC:11757 $F99C:60 RTS A:01 X:55 Y:25 P:65 SP:F9 PPU:133,103 CYC:11759 $DDD5:95 00 STA $00,X @ 55 = #$00 A:01 X:55 Y:25 P:65 SP:FB PPU:151,103 CYC:11765 $DDD7:56 00 LSR $00,X @ 55 = #$01 A:01 X:55 Y:25 P:65 SP:FB PPU:163,103 CYC:11769 $DDD9:B5 00 LDA $00,X @ 55 = #$00 A:01 X:55 Y:25 P:67 SP:FB PPU:181,103 CYC:11775 $DDDB:20 9D F9 JSR $F99D A:00 X:55 Y:25 P:67 SP:FB PPU:193,103 CYC:11779 $F99D:90 1B BCC $F9BA A:00 X:55 Y:25 P:67 SP:F9 PPU:211,103 CYC:11785 $F99F:D0 19 BNE $F9BA A:00 X:55 Y:25 P:67 SP:F9 PPU:217,103 CYC:11787 $F9A1:30 17 BMI $F9BA A:00 X:55 Y:25 P:67 SP:F9 PPU:223,103 CYC:11789 $F9A3:50 15 BVC $F9BA A:00 X:55 Y:25 P:67 SP:F9 PPU:229,103 CYC:11791 $F9A5:C9 00 CMP #$00 A:00 X:55 Y:25 P:67 SP:F9 PPU:235,103 CYC:11793 $F9A7:D0 11 BNE $F9BA A:00 X:55 Y:25 P:67 SP:F9 PPU:241,103 CYC:11795 $F9A9:B8 CLV A:00 X:55 Y:25 P:67 SP:F9 PPU:247,103 CYC:11797 $F9AA:A9 AA LDA #$AA A:00 X:55 Y:25 P:nvUbdIZC SP:F9 PPU:253,103 CYC:11799 $F9AC:60 RTS A:AA X:55 Y:25 P:A5 SP:F9 PPU:259,103 CYC:11801 $DDDE:C8 INY A:AA X:55 Y:25 P:A5 SP:FB PPU:277,103 CYC:11807 $DDDF:95 00 STA $00,X @ 55 = #$00 A:AA X:55 Y:26 P:25 SP:FB PPU:283,103 CYC:11809 $DDE1:56 00 LSR $00,X @ 55 = #$AA A:AA X:55 Y:26 P:25 SP:FB PPU:295,103 CYC:11813 $DDE3:B5 00 LDA $00,X @ 55 = #$55 A:AA X:55 Y:26 P:nvUbdIzc SP:FB PPU:313,103 CYC:11819 $DDE5:20 AD F9 JSR $F9AD A:55 X:55 Y:26 P:nvUbdIzc SP:FB PPU:325,103 CYC:11823 $F9AD:B0 0B BCS $F9BA A:55 X:55 Y:26 P:nvUbdIzc SP:F9 PPU: 2,104 CYC:11829 $F9AF:F0 09 BEQ $F9BA A:55 X:55 Y:26 P:nvUbdIzc SP:F9 PPU: 8,104 CYC:11831 $F9B1:30 07 BMI $F9BA A:55 X:55 Y:26 P:nvUbdIzc SP:F9 PPU: 14,104 CYC:11833 $F9B3:70 05 BVS $F9BA A:55 X:55 Y:26 P:nvUbdIzc SP:F9 PPU: 20,104 CYC:11835 $F9B5:C9 55 CMP #$55 A:55 X:55 Y:26 P:nvUbdIzc SP:F9 PPU: 26,104 CYC:11837 $F9B7:D0 01 BNE $F9BA A:55 X:55 Y:26 P:nvUbdIZC SP:F9 PPU: 32,104 CYC:11839 $F9B9:60 RTS A:55 X:55 Y:26 P:nvUbdIZC SP:F9 PPU: 38,104 CYC:11841 $DDE8:C8 INY A:55 X:55 Y:26 P:nvUbdIZC SP:FB PPU: 56,104 CYC:11847 $DDE9:20 BD F9 JSR $F9BD A:55 X:55 Y:27 P:25 SP:FB PPU: 62,104 CYC:11849 $F9BD:24 01 BIT $01 = #$FF A:55 X:55 Y:27 P:25 SP:F9 PPU: 80,104 CYC:11855 $F9BF:38 SEC A:55 X:55 Y:27 P:E5 SP:F9 PPU: 89,104 CYC:11858 $F9C0:A9 80 LDA #$80 A:55 X:55 Y:27 P:E5 SP:F9 PPU: 95,104 CYC:11860 $F9C2:60 RTS A:80 X:55 Y:27 P:E5 SP:F9 PPU:101,104 CYC:11862 $DDEC:95 00 STA $00,X @ 55 = #$55 A:80 X:55 Y:27 P:E5 SP:FB PPU:119,104 CYC:11868 $DDEE:16 00 ASL $00,X @ 55 = #$80 A:80 X:55 Y:27 P:E5 SP:FB PPU:131,104 CYC:11872 $DDF0:B5 00 LDA $00,X @ 55 = #$00 A:80 X:55 Y:27 P:67 SP:FB PPU:149,104 CYC:11878 $DDF2:20 C3 F9 JSR $F9C3 A:00 X:55 Y:27 P:67 SP:FB PPU:161,104 CYC:11882 $F9C3:90 1C BCC $F9E1 A:00 X:55 Y:27 P:67 SP:F9 PPU:179,104 CYC:11888 $F9C5:D0 1A BNE $F9E1 A:00 X:55 Y:27 P:67 SP:F9 PPU:185,104 CYC:11890 $F9C7:30 18 BMI $F9E1 A:00 X:55 Y:27 P:67 SP:F9 PPU:191,104 CYC:11892 $F9C9:50 16 BVC $F9E1 A:00 X:55 Y:27 P:67 SP:F9 PPU:197,104 CYC:11894 $F9CB:C9 00 CMP #$00 A:00 X:55 Y:27 P:67 SP:F9 PPU:203,104 CYC:11896 $F9CD:D0 12 BNE $F9E1 A:00 X:55 Y:27 P:67 SP:F9 PPU:209,104 CYC:11898 $F9CF:B8 CLV A:00 X:55 Y:27 P:67 SP:F9 PPU:215,104 CYC:11900 $F9D0:A9 55 LDA #$55 A:00 X:55 Y:27 P:nvUbdIZC SP:F9 PPU:221,104 CYC:11902 $F9D2:38 SEC A:55 X:55 Y:27 P:25 SP:F9 PPU:227,104 CYC:11904 $F9D3:60 RTS A:55 X:55 Y:27 P:25 SP:F9 PPU:233,104 CYC:11906 $DDF5:C8 INY A:55 X:55 Y:27 P:25 SP:FB PPU:251,104 CYC:11912 $DDF6:95 00 STA $00,X @ 55 = #$00 A:55 X:55 Y:28 P:25 SP:FB PPU:257,104 CYC:11914 $DDF8:16 00 ASL $00,X @ 55 = #$55 A:55 X:55 Y:28 P:25 SP:FB PPU:269,104 CYC:11918 $DDFA:B5 00 LDA $00,X @ 55 = #$AA A:55 X:55 Y:28 P:NvUbdIzc SP:FB PPU:287,104 CYC:11924 $DDFC:20 D4 F9 JSR $F9D4 A:AA X:55 Y:28 P:NvUbdIzc SP:FB PPU:299,104 CYC:11928 $F9D4:B0 0B BCS $F9E1 A:AA X:55 Y:28 P:NvUbdIzc SP:F9 PPU:317,104 CYC:11934 $F9D6:F0 09 BEQ $F9E1 A:AA X:55 Y:28 P:NvUbdIzc SP:F9 PPU:323,104 CYC:11936 $F9D8:10 07 BPL $F9E1 A:AA X:55 Y:28 P:NvUbdIzc SP:F9 PPU:329,104 CYC:11938 $F9DA:70 05 BVS $F9E1 A:AA X:55 Y:28 P:NvUbdIzc SP:F9 PPU:335,104 CYC:11940 $F9DC:C9 AA CMP #$AA A:AA X:55 Y:28 P:NvUbdIzc SP:F9 PPU: 0,105 CYC:11942 $F9DE:D0 01 BNE $F9E1 A:AA X:55 Y:28 P:nvUbdIZC SP:F9 PPU: 6,105 CYC:11944 $F9E0:60 RTS A:AA X:55 Y:28 P:nvUbdIZC SP:F9 PPU: 12,105 CYC:11946 $DDFF:C8 INY A:AA X:55 Y:28 P:nvUbdIZC SP:FB PPU: 30,105 CYC:11952 $DE00:20 E4 F9 JSR $F9E4 A:AA X:55 Y:29 P:25 SP:FB PPU: 36,105 CYC:11954 $F9E4:24 01 BIT $01 = #$FF A:AA X:55 Y:29 P:25 SP:F9 PPU: 54,105 CYC:11960 $F9E6:38 SEC A:AA X:55 Y:29 P:E5 SP:F9 PPU: 63,105 CYC:11963 $F9E7:A9 01 LDA #$01 A:AA X:55 Y:29 P:E5 SP:F9 PPU: 69,105 CYC:11965 $F9E9:60 RTS A:01 X:55 Y:29 P:65 SP:F9 PPU: 75,105 CYC:11967 $DE03:95 00 STA $00,X @ 55 = #$AA A:01 X:55 Y:29 P:65 SP:FB PPU: 93,105 CYC:11973 $DE05:76 00 ROR $00,X @ 55 = #$01 A:01 X:55 Y:29 P:65 SP:FB PPU:105,105 CYC:11977 $DE07:B5 00 LDA $00,X @ 55 = #$80 A:01 X:55 Y:29 P:E5 SP:FB PPU:123,105 CYC:11983 $DE09:20 EA F9 JSR $F9EA A:80 X:55 Y:29 P:E5 SP:FB PPU:135,105 CYC:11987 $F9EA:90 1C BCC $FA08 A:80 X:55 Y:29 P:E5 SP:F9 PPU:153,105 CYC:11993 $F9EC:F0 1A BEQ $FA08 A:80 X:55 Y:29 P:E5 SP:F9 PPU:159,105 CYC:11995 $F9EE:10 18 BPL $FA08 A:80 X:55 Y:29 P:E5 SP:F9 PPU:165,105 CYC:11997 $F9F0:50 16 BVC $FA08 A:80 X:55 Y:29 P:E5 SP:F9 PPU:171,105 CYC:11999 $F9F2:C9 80 CMP #$80 A:80 X:55 Y:29 P:E5 SP:F9 PPU:177,105 CYC:12001 $F9F4:D0 12 BNE $FA08 A:80 X:55 Y:29 P:67 SP:F9 PPU:183,105 CYC:12003 $F9F6:B8 CLV A:80 X:55 Y:29 P:67 SP:F9 PPU:189,105 CYC:12005 $F9F7:18 CLC A:80 X:55 Y:29 P:nvUbdIZC SP:F9 PPU:195,105 CYC:12007 $F9F8:A9 55 LDA #$55 A:80 X:55 Y:29 P:nvUbdIZc SP:F9 PPU:201,105 CYC:12009 $F9FA:60 RTS A:55 X:55 Y:29 P:nvUbdIzc SP:F9 PPU:207,105 CYC:12011 $DE0C:C8 INY A:55 X:55 Y:29 P:nvUbdIzc SP:FB PPU:225,105 CYC:12017 $DE0D:95 00 STA $00,X @ 55 = #$80 A:55 X:55 Y:2A P:nvUbdIzc SP:FB PPU:231,105 CYC:12019 $DE0F:76 00 ROR $00,X @ 55 = #$55 A:55 X:55 Y:2A P:nvUbdIzc SP:FB PPU:243,105 CYC:12023 $DE11:B5 00 LDA $00,X @ 55 = #$2A A:55 X:55 Y:2A P:25 SP:FB PPU:261,105 CYC:12029 $DE13:20 FB F9 JSR $F9FB A:2A X:55 Y:2A P:25 SP:FB PPU:273,105 CYC:12033 $F9FB:90 0B BCC $FA08 A:2A X:55 Y:2A P:25 SP:F9 PPU:291,105 CYC:12039 $F9FD:F0 09 BEQ $FA08 A:2A X:55 Y:2A P:25 SP:F9 PPU:297,105 CYC:12041 $F9FF:30 07 BMI $FA08 A:2A X:55 Y:2A P:25 SP:F9 PPU:303,105 CYC:12043 $FA01:70 05 BVS $FA08 A:2A X:55 Y:2A P:25 SP:F9 PPU:309,105 CYC:12045 $FA03:C9 2A CMP #$2A A:2A X:55 Y:2A P:25 SP:F9 PPU:315,105 CYC:12047 $FA05:D0 01 BNE $FA08 A:2A X:55 Y:2A P:nvUbdIZC SP:F9 PPU:321,105 CYC:12049 $FA07:60 RTS A:2A X:55 Y:2A P:nvUbdIZC SP:F9 PPU:327,105 CYC:12051 $DE16:C8 INY A:2A X:55 Y:2A P:nvUbdIZC SP:FB PPU: 4,106 CYC:12057 $DE17:20 0A FA JSR $FA0A A:2A X:55 Y:2B P:25 SP:FB PPU: 10,106 CYC:12059 $FA0A:24 01 BIT $01 = #$FF A:2A X:55 Y:2B P:25 SP:F9 PPU: 28,106 CYC:12065 $FA0C:38 SEC A:2A X:55 Y:2B P:E5 SP:F9 PPU: 37,106 CYC:12068 $FA0D:A9 80 LDA #$80 A:2A X:55 Y:2B P:E5 SP:F9 PPU: 43,106 CYC:12070 $FA0F:60 RTS A:80 X:55 Y:2B P:E5 SP:F9 PPU: 49,106 CYC:12072 $DE1A:95 00 STA $00,X @ 55 = #$2A A:80 X:55 Y:2B P:E5 SP:FB PPU: 67,106 CYC:12078 $DE1C:36 00 ROL $00,X @ 55 = #$80 A:80 X:55 Y:2B P:E5 SP:FB PPU: 79,106 CYC:12082 $DE1E:B5 00 LDA $00,X @ 55 = #$01 A:80 X:55 Y:2B P:65 SP:FB PPU: 97,106 CYC:12088 $DE20:20 10 FA JSR $FA10 A:01 X:55 Y:2B P:65 SP:FB PPU:109,106 CYC:12092 $FA10:90 1C BCC $FA2E A:01 X:55 Y:2B P:65 SP:F9 PPU:127,106 CYC:12098 $FA12:F0 1A BEQ $FA2E A:01 X:55 Y:2B P:65 SP:F9 PPU:133,106 CYC:12100 $FA14:30 18 BMI $FA2E A:01 X:55 Y:2B P:65 SP:F9 PPU:139,106 CYC:12102 $FA16:50 16 BVC $FA2E A:01 X:55 Y:2B P:65 SP:F9 PPU:145,106 CYC:12104 $FA18:C9 01 CMP #$01 A:01 X:55 Y:2B P:65 SP:F9 PPU:151,106 CYC:12106 $FA1A:D0 12 BNE $FA2E A:01 X:55 Y:2B P:67 SP:F9 PPU:157,106 CYC:12108 $FA1C:B8 CLV A:01 X:55 Y:2B P:67 SP:F9 PPU:163,106 CYC:12110 $FA1D:18 CLC A:01 X:55 Y:2B P:nvUbdIZC SP:F9 PPU:169,106 CYC:12112 $FA1E:A9 55 LDA #$55 A:01 X:55 Y:2B P:nvUbdIZc SP:F9 PPU:175,106 CYC:12114 $FA20:60 RTS A:55 X:55 Y:2B P:nvUbdIzc SP:F9 PPU:181,106 CYC:12116 $DE23:C8 INY A:55 X:55 Y:2B P:nvUbdIzc SP:FB PPU:199,106 CYC:12122 $DE24:95 00 STA $00,X @ 55 = #$01 A:55 X:55 Y:2C P:nvUbdIzc SP:FB PPU:205,106 CYC:12124 $DE26:36 00 ROL $00,X @ 55 = #$55 A:55 X:55 Y:2C P:nvUbdIzc SP:FB PPU:217,106 CYC:12128 $DE28:B5 00 LDA $00,X @ 55 = #$AA A:55 X:55 Y:2C P:NvUbdIzc SP:FB PPU:235,106 CYC:12134 $DE2A:20 21 FA JSR $FA21 A:AA X:55 Y:2C P:NvUbdIzc SP:FB PPU:247,106 CYC:12138 $FA21:B0 0B BCS $FA2E A:AA X:55 Y:2C P:NvUbdIzc SP:F9 PPU:265,106 CYC:12144 $FA23:F0 09 BEQ $FA2E A:AA X:55 Y:2C P:NvUbdIzc SP:F9 PPU:271,106 CYC:12146 $FA25:10 07 BPL $FA2E A:AA X:55 Y:2C P:NvUbdIzc SP:F9 PPU:277,106 CYC:12148 $FA27:70 05 BVS $FA2E A:AA X:55 Y:2C P:NvUbdIzc SP:F9 PPU:283,106 CYC:12150 $FA29:C9 AA CMP #$AA A:AA X:55 Y:2C P:NvUbdIzc SP:F9 PPU:289,106 CYC:12152 $FA2B:D0 01 BNE $FA2E A:AA X:55 Y:2C P:nvUbdIZC SP:F9 PPU:295,106 CYC:12154 $FA2D:60 RTS A:AA X:55 Y:2C P:nvUbdIZC SP:F9 PPU:301,106 CYC:12156 $DE2D:A9 FF LDA #$FF A:AA X:55 Y:2C P:nvUbdIZC SP:FB PPU:319,106 CYC:12162 $DE2F:95 00 STA $00,X @ 55 = #$AA A:FF X:55 Y:2C P:A5 SP:FB PPU:325,106 CYC:12164 $DE31:85 01 STA $01 = #$FF A:FF X:55 Y:2C P:A5 SP:FB PPU:337,106 CYC:12168 $DE33:24 01 BIT $01 = #$FF A:FF X:55 Y:2C P:A5 SP:FB PPU: 5,107 CYC:12171 $DE35:38 SEC A:FF X:55 Y:2C P:E5 SP:FB PPU: 14,107 CYC:12174 $DE36:F6 00 INC $00,X @ 55 = #$FF A:FF X:55 Y:2C P:E5 SP:FB PPU: 20,107 CYC:12176 $DE38:D0 0C BNE $DE46 A:FF X:55 Y:2C P:67 SP:FB PPU: 38,107 CYC:12182 $DE3A:30 0A BMI $DE46 A:FF X:55 Y:2C P:67 SP:FB PPU: 44,107 CYC:12184 $DE3C:50 08 BVC $DE46 A:FF X:55 Y:2C P:67 SP:FB PPU: 50,107 CYC:12186 $DE3E:90 06 BCC $DE46 A:FF X:55 Y:2C P:67 SP:FB PPU: 56,107 CYC:12188 $DE40:B5 00 LDA $00,X @ 55 = #$00 A:FF X:55 Y:2C P:67 SP:FB PPU: 62,107 CYC:12190 $DE42:C9 00 CMP #$00 A:00 X:55 Y:2C P:67 SP:FB PPU: 74,107 CYC:12194 $DE44:F0 04 BEQ $DE4A A:00 X:55 Y:2C P:67 SP:FB PPU: 80,107 CYC:12196 $DE4A:A9 7F LDA #$7F A:00 X:55 Y:2C P:67 SP:FB PPU: 89,107 CYC:12199 $DE4C:95 00 STA $00,X @ 55 = #$00 A:7F X:55 Y:2C P:65 SP:FB PPU: 95,107 CYC:12201 $DE4E:B8 CLV A:7F X:55 Y:2C P:65 SP:FB PPU:107,107 CYC:12205 $DE4F:18 CLC A:7F X:55 Y:2C P:25 SP:FB PPU:113,107 CYC:12207 $DE50:F6 00 INC $00,X @ 55 = #$7F A:7F X:55 Y:2C P:nvUbdIzc SP:FB PPU:119,107 CYC:12209 $DE52:F0 0C BEQ $DE60 A:7F X:55 Y:2C P:NvUbdIzc SP:FB PPU:137,107 CYC:12215 $DE54:10 0A BPL $DE60 A:7F X:55 Y:2C P:NvUbdIzc SP:FB PPU:143,107 CYC:12217 $DE56:70 08 BVS $DE60 A:7F X:55 Y:2C P:NvUbdIzc SP:FB PPU:149,107 CYC:12219 $DE58:B0 06 BCS $DE60 A:7F X:55 Y:2C P:NvUbdIzc SP:FB PPU:155,107 CYC:12221 $DE5A:B5 00 LDA $00,X @ 55 = #$80 A:7F X:55 Y:2C P:NvUbdIzc SP:FB PPU:161,107 CYC:12223 $DE5C:C9 80 CMP #$80 A:80 X:55 Y:2C P:NvUbdIzc SP:FB PPU:173,107 CYC:12227 $DE5E:F0 04 BEQ $DE64 A:80 X:55 Y:2C P:nvUbdIZC SP:FB PPU:179,107 CYC:12229 $DE64:A9 00 LDA #$00 A:80 X:55 Y:2C P:nvUbdIZC SP:FB PPU:188,107 CYC:12232 $DE66:95 00 STA $00,X @ 55 = #$80 A:00 X:55 Y:2C P:nvUbdIZC SP:FB PPU:194,107 CYC:12234 $DE68:24 01 BIT $01 = #$FF A:00 X:55 Y:2C P:nvUbdIZC SP:FB PPU:206,107 CYC:12238 $DE6A:38 SEC A:00 X:55 Y:2C P:E7 SP:FB PPU:215,107 CYC:12241 $DE6B:D6 00 DEC $00,X @ 55 = #$00 A:00 X:55 Y:2C P:E7 SP:FB PPU:221,107 CYC:12243 $DE6D:F0 0C BEQ $DE7B A:00 X:55 Y:2C P:E5 SP:FB PPU:239,107 CYC:12249 $DE6F:10 0A BPL $DE7B A:00 X:55 Y:2C P:E5 SP:FB PPU:245,107 CYC:12251 $DE71:50 08 BVC $DE7B A:00 X:55 Y:2C P:E5 SP:FB PPU:251,107 CYC:12253 $DE73:90 06 BCC $DE7B A:00 X:55 Y:2C P:E5 SP:FB PPU:257,107 CYC:12255 $DE75:B5 00 LDA $00,X @ 55 = #$FF A:00 X:55 Y:2C P:E5 SP:FB PPU:263,107 CYC:12257 $DE77:C9 FF CMP #$FF A:FF X:55 Y:2C P:E5 SP:FB PPU:275,107 CYC:12261 $DE79:F0 04 BEQ $DE7F A:FF X:55 Y:2C P:67 SP:FB PPU:281,107 CYC:12263 $DE7F:A9 80 LDA #$80 A:FF X:55 Y:2C P:67 SP:FB PPU:290,107 CYC:12266 $DE81:95 00 STA $00,X @ 55 = #$FF A:80 X:55 Y:2C P:E5 SP:FB PPU:296,107 CYC:12268 $DE83:B8 CLV A:80 X:55 Y:2C P:E5 SP:FB PPU:308,107 CYC:12272 $DE84:18 CLC A:80 X:55 Y:2C P:A5 SP:FB PPU:314,107 CYC:12274 $DE85:D6 00 DEC $00,X @ 55 = #$80 A:80 X:55 Y:2C P:NvUbdIzc SP:FB PPU:320,107 CYC:12276 $DE87:F0 0C BEQ $DE95 A:80 X:55 Y:2C P:nvUbdIzc SP:FB PPU:338,107 CYC:12282 $DE89:30 0A BMI $DE95 A:80 X:55 Y:2C P:nvUbdIzc SP:FB PPU: 3,108 CYC:12284 $DE8B:70 08 BVS $DE95 A:80 X:55 Y:2C P:nvUbdIzc SP:FB PPU: 9,108 CYC:12286 $DE8D:B0 06 BCS $DE95 A:80 X:55 Y:2C P:nvUbdIzc SP:FB PPU: 15,108 CYC:12288 $DE8F:B5 00 LDA $00,X @ 55 = #$7F A:80 X:55 Y:2C P:nvUbdIzc SP:FB PPU: 21,108 CYC:12290 $DE91:C9 7F CMP #$7F A:7F X:55 Y:2C P:nvUbdIzc SP:FB PPU: 33,108 CYC:12294 $DE93:F0 04 BEQ $DE99 A:7F X:55 Y:2C P:nvUbdIZC SP:FB PPU: 39,108 CYC:12296 $DE99:A9 01 LDA #$01 A:7F X:55 Y:2C P:nvUbdIZC SP:FB PPU: 48,108 CYC:12299 $DE9B:95 00 STA $00,X @ 55 = #$7F A:01 X:55 Y:2C P:25 SP:FB PPU: 54,108 CYC:12301 $DE9D:D6 00 DEC $00,X @ 55 = #$01 A:01 X:55 Y:2C P:25 SP:FB PPU: 66,108 CYC:12305 $DE9F:F0 04 BEQ $DEA5 A:01 X:55 Y:2C P:nvUbdIZC SP:FB PPU: 84,108 CYC:12311 $DEA5:A9 33 LDA #$33 A:01 X:55 Y:2C P:nvUbdIZC SP:FB PPU: 93,108 CYC:12314 $DEA7:85 78 STA $78 = #$7F A:33 X:55 Y:2C P:25 SP:FB PPU: 99,108 CYC:12316 $DEA9:A9 44 LDA #$44 A:33 X:55 Y:2C P:25 SP:FB PPU:108,108 CYC:12319 $DEAB:A0 78 LDY #$78 A:44 X:55 Y:2C P:25 SP:FB PPU:114,108 CYC:12321 $DEAD:A2 00 LDX #$00 A:44 X:55 Y:78 P:25 SP:FB PPU:120,108 CYC:12323 $DEAF:38 SEC A:44 X:00 Y:78 P:nvUbdIZC SP:FB PPU:126,108 CYC:12325 $DEB0:24 01 BIT $01 = #$FF A:44 X:00 Y:78 P:nvUbdIZC SP:FB PPU:132,108 CYC:12327 $DEB2:B6 00 LDX $00,Y @ 78 = #$33 A:44 X:00 Y:78 P:E5 SP:FB PPU:141,108 CYC:12330 $DEB4:90 12 BCC $DEC8 A:44 X:33 Y:78 P:65 SP:FB PPU:153,108 CYC:12334 $DEB6:50 10 BVC $DEC8 A:44 X:33 Y:78 P:65 SP:FB PPU:159,108 CYC:12336 $DEB8:30 0E BMI $DEC8 A:44 X:33 Y:78 P:65 SP:FB PPU:165,108 CYC:12338 $DEBA:F0 0C BEQ $DEC8 A:44 X:33 Y:78 P:65 SP:FB PPU:171,108 CYC:12340 $DEBC:E0 33 CPX #$33 A:44 X:33 Y:78 P:65 SP:FB PPU:177,108 CYC:12342 $DEBE:D0 08 BNE $DEC8 A:44 X:33 Y:78 P:67 SP:FB PPU:183,108 CYC:12344 $DEC0:C0 78 CPY #$78 A:44 X:33 Y:78 P:67 SP:FB PPU:189,108 CYC:12346 $DEC2:D0 04 BNE $DEC8 A:44 X:33 Y:78 P:67 SP:FB PPU:195,108 CYC:12348 $DEC4:C9 44 CMP #$44 A:44 X:33 Y:78 P:67 SP:FB PPU:201,108 CYC:12350 $DEC6:F0 04 BEQ $DECC A:44 X:33 Y:78 P:67 SP:FB PPU:207,108 CYC:12352 $DECC:A9 97 LDA #$97 A:44 X:33 Y:78 P:67 SP:FB PPU:216,108 CYC:12355 $DECE:85 7F STA $7F = #$00 A:97 X:33 Y:78 P:E5 SP:FB PPU:222,108 CYC:12357 $DED0:A9 47 LDA #$47 A:97 X:33 Y:78 P:E5 SP:FB PPU:231,108 CYC:12360 $DED2:A0 FF LDY #$FF A:47 X:33 Y:78 P:65 SP:FB PPU:237,108 CYC:12362 $DED4:A2 00 LDX #$00 A:47 X:33 Y:FF P:E5 SP:FB PPU:243,108 CYC:12364 $DED6:18 CLC A:47 X:00 Y:FF P:67 SP:FB PPU:249,108 CYC:12366 $DED7:B8 CLV A:47 X:00 Y:FF P:nVUbdIZc SP:FB PPU:255,108 CYC:12368 $DED8:B6 80 LDX $80,Y @ 7F = #$97 A:47 X:00 Y:FF P:nvUbdIZc SP:FB PPU:261,108 CYC:12370 $DEDA:B0 12 BCS $DEEE A:47 X:97 Y:FF P:NvUbdIzc SP:FB PPU:273,108 CYC:12374 $DEDC:70 10 BVS $DEEE A:47 X:97 Y:FF P:NvUbdIzc SP:FB PPU:279,108 CYC:12376 $DEDE:10 0E BPL $DEEE A:47 X:97 Y:FF P:NvUbdIzc SP:FB PPU:285,108 CYC:12378 $DEE0:F0 0C BEQ $DEEE A:47 X:97 Y:FF P:NvUbdIzc SP:FB PPU:291,108 CYC:12380 $DEE2:E0 97 CPX #$97 A:47 X:97 Y:FF P:NvUbdIzc SP:FB PPU:297,108 CYC:12382 $DEE4:D0 08 BNE $DEEE A:47 X:97 Y:FF P:nvUbdIZC SP:FB PPU:303,108 CYC:12384 $DEE6:C0 FF CPY #$FF A:47 X:97 Y:FF P:nvUbdIZC SP:FB PPU:309,108 CYC:12386 $DEE8:D0 04 BNE $DEEE A:47 X:97 Y:FF P:nvUbdIZC SP:FB PPU:315,108 CYC:12388 $DEEA:C9 47 CMP #$47 A:47 X:97 Y:FF P:nvUbdIZC SP:FB PPU:321,108 CYC:12390 $DEEC:F0 04 BEQ $DEF2 A:47 X:97 Y:FF P:nvUbdIZC SP:FB PPU:327,108 CYC:12392 $DEF2:A9 00 LDA #$00 A:47 X:97 Y:FF P:nvUbdIZC SP:FB PPU:336,108 CYC:12395 $DEF4:85 7F STA $7F = #$97 A:00 X:97 Y:FF P:nvUbdIZC SP:FB PPU: 1,109 CYC:12397 $DEF6:A9 47 LDA #$47 A:00 X:97 Y:FF P:nvUbdIZC SP:FB PPU: 10,109 CYC:12400 $DEF8:A0 FF LDY #$FF A:47 X:97 Y:FF P:25 SP:FB PPU: 16,109 CYC:12402 $DEFA:A2 69 LDX #$69 A:47 X:97 Y:FF P:A5 SP:FB PPU: 22,109 CYC:12404 $DEFC:18 CLC A:47 X:69 Y:FF P:25 SP:FB PPU: 28,109 CYC:12406 $DEFD:B8 CLV A:47 X:69 Y:FF P:nvUbdIzc SP:FB PPU: 34,109 CYC:12408 $DEFE:96 80 STX $80,Y @ 7F = #$00 A:47 X:69 Y:FF P:nvUbdIzc SP:FB PPU: 40,109 CYC:12410 $DF00:B0 18 BCS $DF1A A:47 X:69 Y:FF P:nvUbdIzc SP:FB PPU: 52,109 CYC:12414 $DF02:70 16 BVS $DF1A A:47 X:69 Y:FF P:nvUbdIzc SP:FB PPU: 58,109 CYC:12416 $DF04:30 14 BMI $DF1A A:47 X:69 Y:FF P:nvUbdIzc SP:FB PPU: 64,109 CYC:12418 $DF06:F0 12 BEQ $DF1A A:47 X:69 Y:FF P:nvUbdIzc SP:FB PPU: 70,109 CYC:12420 $DF08:E0 69 CPX #$69 A:47 X:69 Y:FF P:nvUbdIzc SP:FB PPU: 76,109 CYC:12422 $DF0A:D0 0E BNE $DF1A A:47 X:69 Y:FF P:nvUbdIZC SP:FB PPU: 82,109 CYC:12424 $DF0C:C0 FF CPY #$FF A:47 X:69 Y:FF P:nvUbdIZC SP:FB PPU: 88,109 CYC:12426 $DF0E:D0 0A BNE $DF1A A:47 X:69 Y:FF P:nvUbdIZC SP:FB PPU: 94,109 CYC:12428 $DF10:C9 47 CMP #$47 A:47 X:69 Y:FF P:nvUbdIZC SP:FB PPU:100,109 CYC:12430 $DF12:D0 06 BNE $DF1A A:47 X:69 Y:FF P:nvUbdIZC SP:FB PPU:106,109 CYC:12432 $DF14:A5 7F LDA $7F = #$69 A:47 X:69 Y:FF P:nvUbdIZC SP:FB PPU:112,109 CYC:12434 $DF16:C9 69 CMP #$69 A:69 X:69 Y:FF P:25 SP:FB PPU:121,109 CYC:12437 $DF18:F0 04 BEQ $DF1E A:69 X:69 Y:FF P:nvUbdIZC SP:FB PPU:127,109 CYC:12439 $DF1E:A9 F5 LDA #$F5 A:69 X:69 Y:FF P:nvUbdIZC SP:FB PPU:136,109 CYC:12442 $DF20:85 4F STA $4F = #$00 A:F5 X:69 Y:FF P:A5 SP:FB PPU:142,109 CYC:12444 $DF22:A9 47 LDA #$47 A:F5 X:69 Y:FF P:A5 SP:FB PPU:151,109 CYC:12447 $DF24:A0 4F LDY #$4F A:47 X:69 Y:FF P:25 SP:FB PPU:157,109 CYC:12449 $DF26:24 01 BIT $01 = #$FF A:47 X:69 Y:4F P:25 SP:FB PPU:163,109 CYC:12451 $DF28:A2 00 LDX #$00 A:47 X:69 Y:4F P:E5 SP:FB PPU:172,109 CYC:12454 $DF2A:38 SEC A:47 X:00 Y:4F P:67 SP:FB PPU:178,109 CYC:12456 $DF2B:96 00 STX $00,Y @ 4F = #$F5 A:47 X:00 Y:4F P:67 SP:FB PPU:184,109 CYC:12458 $DF2D:90 16 BCC $DF45 A:47 X:00 Y:4F P:67 SP:FB PPU:196,109 CYC:12462 $DF2F:50 14 BVC $DF45 A:47 X:00 Y:4F P:67 SP:FB PPU:202,109 CYC:12464 $DF31:30 12 BMI $DF45 A:47 X:00 Y:4F P:67 SP:FB PPU:208,109 CYC:12466 $DF33:D0 10 BNE $DF45 A:47 X:00 Y:4F P:67 SP:FB PPU:214,109 CYC:12468 $DF35:E0 00 CPX #$00 A:47 X:00 Y:4F P:67 SP:FB PPU:220,109 CYC:12470 $DF37:D0 0C BNE $DF45 A:47 X:00 Y:4F P:67 SP:FB PPU:226,109 CYC:12472 $DF39:C0 4F CPY #$4F A:47 X:00 Y:4F P:67 SP:FB PPU:232,109 CYC:12474 $DF3B:D0 08 BNE $DF45 A:47 X:00 Y:4F P:67 SP:FB PPU:238,109 CYC:12476 $DF3D:C9 47 CMP #$47 A:47 X:00 Y:4F P:67 SP:FB PPU:244,109 CYC:12478 $DF3F:D0 04 BNE $DF45 A:47 X:00 Y:4F P:67 SP:FB PPU:250,109 CYC:12480 $DF41:A5 4F LDA $4F = #$00 A:47 X:00 Y:4F P:67 SP:FB PPU:256,109 CYC:12482 $DF43:F0 04 BEQ $DF49 A:00 X:00 Y:4F P:67 SP:FB PPU:265,109 CYC:12485 $DF49:60 RTS A:00 X:00 Y:4F P:67 SP:FB PPU:274,109 CYC:12488 $C62C:20 AA E1 JSR $E1AA A:00 X:00 Y:4F P:67 SP:FD PPU:292,109 CYC:12494 $E1AA:A9 FF LDA #$FF A:00 X:00 Y:4F P:67 SP:FB PPU:310,109 CYC:12500 $E1AC:85 01 STA $01 = #$FF A:FF X:00 Y:4F P:E5 SP:FB PPU:316,109 CYC:12502 $E1AE:A9 AA LDA #$AA A:FF X:00 Y:4F P:E5 SP:FB PPU:325,109 CYC:12505 $E1B0:8D 33 06 STA $0633 = #$00 A:AA X:00 Y:4F P:E5 SP:FB PPU:331,109 CYC:12507 $E1B3:A9 BB LDA #$BB A:AA X:00 Y:4F P:E5 SP:FB PPU: 2,110 CYC:12511 $E1B5:8D 89 06 STA $0689 = #$00 A:BB X:00 Y:4F P:E5 SP:FB PPU: 8,110 CYC:12513 $E1B8:A2 00 LDX #$00 A:BB X:00 Y:4F P:E5 SP:FB PPU: 20,110 CYC:12517 $E1BA:A9 66 LDA #$66 A:BB X:00 Y:4F P:67 SP:FB PPU: 26,110 CYC:12519 $E1BC:24 01 BIT $01 = #$FF A:66 X:00 Y:4F P:65 SP:FB PPU: 32,110 CYC:12521 $E1BE:38 SEC A:66 X:00 Y:4F P:E5 SP:FB PPU: 41,110 CYC:12524 $E1BF:A0 00 LDY #$00 A:66 X:00 Y:4F P:E5 SP:FB PPU: 47,110 CYC:12526 $E1C1:BC 33 06 LDY $0633,X @ 0633 = #$AA A:66 X:00 Y:00 P:67 SP:FB PPU: 53,110 CYC:12528 $E1C4:10 12 BPL $E1D8 A:66 X:00 Y:AA P:E5 SP:FB PPU: 65,110 CYC:12532 $E1C6:F0 10 BEQ $E1D8 A:66 X:00 Y:AA P:E5 SP:FB PPU: 71,110 CYC:12534 $E1C8:50 0E BVC $E1D8 A:66 X:00 Y:AA P:E5 SP:FB PPU: 77,110 CYC:12536 $E1CA:90 0C BCC $E1D8 A:66 X:00 Y:AA P:E5 SP:FB PPU: 83,110 CYC:12538 $E1CC:C9 66 CMP #$66 A:66 X:00 Y:AA P:E5 SP:FB PPU: 89,110 CYC:12540 $E1CE:D0 08 BNE $E1D8 A:66 X:00 Y:AA P:67 SP:FB PPU: 95,110 CYC:12542 $E1D0:E0 00 CPX #$00 A:66 X:00 Y:AA P:67 SP:FB PPU:101,110 CYC:12544 $E1D2:D0 04 BNE $E1D8 A:66 X:00 Y:AA P:67 SP:FB PPU:107,110 CYC:12546 $E1D4:C0 AA CPY #$AA A:66 X:00 Y:AA P:67 SP:FB PPU:113,110 CYC:12548 $E1D6:F0 04 BEQ $E1DC A:66 X:00 Y:AA P:67 SP:FB PPU:119,110 CYC:12550 $E1DC:A2 8A LDX #$8A A:66 X:00 Y:AA P:67 SP:FB PPU:128,110 CYC:12553 $E1DE:A9 66 LDA #$66 A:66 X:8A Y:AA P:E5 SP:FB PPU:134,110 CYC:12555 $E1E0:B8 CLV A:66 X:8A Y:AA P:65 SP:FB PPU:140,110 CYC:12557 $E1E1:18 CLC A:66 X:8A Y:AA P:25 SP:FB PPU:146,110 CYC:12559 $E1E2:A0 00 LDY #$00 A:66 X:8A Y:AA P:nvUbdIzc SP:FB PPU:152,110 CYC:12561 $E1E4:BC FF 05 LDY $05FF,X @ 0689 = #$BB A:66 X:8A Y:00 P:nvUbdIZc SP:FB PPU:158,110 CYC:12563 $E1E7:10 12 BPL $E1FB A:66 X:8A Y:BB P:NvUbdIzc SP:FB PPU:173,110 CYC:12568 $E1E9:F0 10 BEQ $E1FB A:66 X:8A Y:BB P:NvUbdIzc SP:FB PPU:179,110 CYC:12570 $E1EB:70 0E BVS $E1FB A:66 X:8A Y:BB P:NvUbdIzc SP:FB PPU:185,110 CYC:12572 $E1ED:B0 0C BCS $E1FB A:66 X:8A Y:BB P:NvUbdIzc SP:FB PPU:191,110 CYC:12574 $E1EF:C0 BB CPY #$BB A:66 X:8A Y:BB P:NvUbdIzc SP:FB PPU:197,110 CYC:12576 $E1F1:D0 08 BNE $E1FB A:66 X:8A Y:BB P:nvUbdIZC SP:FB PPU:203,110 CYC:12578 $E1F3:C9 66 CMP #$66 A:66 X:8A Y:BB P:nvUbdIZC SP:FB PPU:209,110 CYC:12580 $E1F5:D0 04 BNE $E1FB A:66 X:8A Y:BB P:nvUbdIZC SP:FB PPU:215,110 CYC:12582 $E1F7:E0 8A CPX #$8A A:66 X:8A Y:BB P:nvUbdIZC SP:FB PPU:221,110 CYC:12584 $E1F9:F0 04 BEQ $E1FF A:66 X:8A Y:BB P:nvUbdIZC SP:FB PPU:227,110 CYC:12586 $E1FF:A0 53 LDY #$53 A:66 X:8A Y:BB P:nvUbdIZC SP:FB PPU:236,110 CYC:12589 $E201:A9 AA LDA #$AA A:66 X:8A Y:53 P:25 SP:FB PPU:242,110 CYC:12591 $E203:A2 78 LDX #$78 A:AA X:8A Y:53 P:A5 SP:FB PPU:248,110 CYC:12593 $E205:8D 78 06 STA $0678 = #$00 A:AA X:78 Y:53 P:25 SP:FB PPU:254,110 CYC:12595 $E208:20 B6 F7 JSR $F7B6 A:AA X:78 Y:53 P:25 SP:FB PPU:266,110 CYC:12599 $F7B6:18 CLC A:AA X:78 Y:53 P:25 SP:F9 PPU:284,110 CYC:12605 $F7B7:A9 FF LDA #$FF A:AA X:78 Y:53 P:nvUbdIzc SP:F9 PPU:290,110 CYC:12607 $F7B9:85 01 STA $01 = #$FF A:FF X:78 Y:53 P:NvUbdIzc SP:F9 PPU:296,110 CYC:12609 $F7BB:24 01 BIT $01 = #$FF A:FF X:78 Y:53 P:NvUbdIzc SP:F9 PPU:305,110 CYC:12612 $F7BD:A9 55 LDA #$55 A:FF X:78 Y:53 P:NVUbdIzc SP:F9 PPU:314,110 CYC:12615 $F7BF:60 RTS A:55 X:78 Y:53 P:64 SP:F9 PPU:320,110 CYC:12617 $E20B:1D 00 06 ORA $0600,X @ 0678 = #$AA A:55 X:78 Y:53 P:64 SP:FB PPU:338,110 CYC:12623 $E20E:20 C0 F7 JSR $F7C0 A:FF X:78 Y:53 P:NVUbdIzc SP:FB PPU: 9,111 CYC:12627 $F7C0:B0 09 BCS $F7CB A:FF X:78 Y:53 P:NVUbdIzc SP:F9 PPU: 27,111 CYC:12633 $F7C2:10 07 BPL $F7CB A:FF X:78 Y:53 P:NVUbdIzc SP:F9 PPU: 33,111 CYC:12635 $F7C4:C9 FF CMP #$FF A:FF X:78 Y:53 P:NVUbdIzc SP:F9 PPU: 39,111 CYC:12637 $F7C6:D0 03 BNE $F7CB A:FF X:78 Y:53 P:67 SP:F9 PPU: 45,111 CYC:12639 $F7C8:50 01 BVC $F7CB A:FF X:78 Y:53 P:67 SP:F9 PPU: 51,111 CYC:12641 $F7CA:60 RTS A:FF X:78 Y:53 P:67 SP:F9 PPU: 57,111 CYC:12643 $E211:C8 INY A:FF X:78 Y:53 P:67 SP:FB PPU: 75,111 CYC:12649 $E212:A9 00 LDA #$00 A:FF X:78 Y:54 P:65 SP:FB PPU: 81,111 CYC:12651 $E214:8D 78 06 STA $0678 = #$AA A:00 X:78 Y:54 P:67 SP:FB PPU: 87,111 CYC:12653 $E217:20 CE F7 JSR $F7CE A:00 X:78 Y:54 P:67 SP:FB PPU: 99,111 CYC:12657 $F7CE:38 SEC A:00 X:78 Y:54 P:67 SP:F9 PPU:117,111 CYC:12663 $F7CF:B8 CLV A:00 X:78 Y:54 P:67 SP:F9 PPU:123,111 CYC:12665 $F7D0:A9 00 LDA #$00 A:00 X:78 Y:54 P:nvUbdIZC SP:F9 PPU:129,111 CYC:12667 $F7D2:60 RTS A:00 X:78 Y:54 P:nvUbdIZC SP:F9 PPU:135,111 CYC:12669 $E21A:1D 00 06 ORA $0600,X @ 0678 = #$00 A:00 X:78 Y:54 P:nvUbdIZC SP:FB PPU:153,111 CYC:12675 $E21D:20 D3 F7 JSR $F7D3 A:00 X:78 Y:54 P:nvUbdIZC SP:FB PPU:165,111 CYC:12679 $F7D3:D0 07 BNE $F7DC A:00 X:78 Y:54 P:nvUbdIZC SP:F9 PPU:183,111 CYC:12685 $F7D5:70 05 BVS $F7DC A:00 X:78 Y:54 P:nvUbdIZC SP:F9 PPU:189,111 CYC:12687 $F7D7:90 03 BCC $F7DC A:00 X:78 Y:54 P:nvUbdIZC SP:F9 PPU:195,111 CYC:12689 $F7D9:30 01 BMI $F7DC A:00 X:78 Y:54 P:nvUbdIZC SP:F9 PPU:201,111 CYC:12691 $F7DB:60 RTS A:00 X:78 Y:54 P:nvUbdIZC SP:F9 PPU:207,111 CYC:12693 $E220:C8 INY A:00 X:78 Y:54 P:nvUbdIZC SP:FB PPU:225,111 CYC:12699 $E221:A9 AA LDA #$AA A:00 X:78 Y:55 P:25 SP:FB PPU:231,111 CYC:12701 $E223:8D 78 06 STA $0678 = #$00 A:AA X:78 Y:55 P:A5 SP:FB PPU:237,111 CYC:12703 $E226:20 DF F7 JSR $F7DF A:AA X:78 Y:55 P:A5 SP:FB PPU:249,111 CYC:12707 $F7DF:18 CLC A:AA X:78 Y:55 P:A5 SP:F9 PPU:267,111 CYC:12713 $F7E0:24 01 BIT $01 = #$FF A:AA X:78 Y:55 P:NvUbdIzc SP:F9 PPU:273,111 CYC:12715 $F7E2:A9 55 LDA #$55 A:AA X:78 Y:55 P:NVUbdIzc SP:F9 PPU:282,111 CYC:12718 $F7E4:60 RTS A:55 X:78 Y:55 P:64 SP:F9 PPU:288,111 CYC:12720 $E229:3D 00 06 AND $0600,X @ 0678 = #$AA A:55 X:78 Y:55 P:64 SP:FB PPU:306,111 CYC:12726 $E22C:20 E5 F7 JSR $F7E5 A:00 X:78 Y:55 P:nVUbdIZc SP:FB PPU:318,111 CYC:12730 $F7E5:D0 07 BNE $F7EE A:00 X:78 Y:55 P:nVUbdIZc SP:F9 PPU:336,111 CYC:12736 $F7E7:50 05 BVC $F7EE A:00 X:78 Y:55 P:nVUbdIZc SP:F9 PPU: 1,112 CYC:12738 $F7E9:B0 03 BCS $F7EE A:00 X:78 Y:55 P:nVUbdIZc SP:F9 PPU: 7,112 CYC:12740 $F7EB:30 01 BMI $F7EE A:00 X:78 Y:55 P:nVUbdIZc SP:F9 PPU: 13,112 CYC:12742 $F7ED:60 RTS A:00 X:78 Y:55 P:nVUbdIZc SP:F9 PPU: 19,112 CYC:12744 $E22F:C8 INY A:00 X:78 Y:55 P:nVUbdIZc SP:FB PPU: 37,112 CYC:12750 $E230:A9 EF LDA #$EF A:00 X:78 Y:56 P:64 SP:FB PPU: 43,112 CYC:12752 $E232:8D 78 06 STA $0678 = #$AA A:EF X:78 Y:56 P:NVUbdIzc SP:FB PPU: 49,112 CYC:12754 $E235:20 F1 F7 JSR $F7F1 A:EF X:78 Y:56 P:NVUbdIzc SP:FB PPU: 61,112 CYC:12758 $F7F1:38 SEC A:EF X:78 Y:56 P:NVUbdIzc SP:F9 PPU: 79,112 CYC:12764 $F7F2:B8 CLV A:EF X:78 Y:56 P:E5 SP:F9 PPU: 85,112 CYC:12766 $F7F3:A9 F8 LDA #$F8 A:EF X:78 Y:56 P:A5 SP:F9 PPU: 91,112 CYC:12768 $F7F5:60 RTS A:F8 X:78 Y:56 P:A5 SP:F9 PPU: 97,112 CYC:12770 $E238:3D 00 06 AND $0600,X @ 0678 = #$EF A:F8 X:78 Y:56 P:A5 SP:FB PPU:115,112 CYC:12776 $E23B:20 F6 F7 JSR $F7F6 A:E8 X:78 Y:56 P:A5 SP:FB PPU:127,112 CYC:12780 $F7F6:90 09 BCC $F801 A:E8 X:78 Y:56 P:A5 SP:F9 PPU:145,112 CYC:12786 $F7F8:10 07 BPL $F801 A:E8 X:78 Y:56 P:A5 SP:F9 PPU:151,112 CYC:12788 $F7FA:C9 E8 CMP #$E8 A:E8 X:78 Y:56 P:A5 SP:F9 PPU:157,112 CYC:12790 $F7FC:D0 03 BNE $F801 A:E8 X:78 Y:56 P:nvUbdIZC SP:F9 PPU:163,112 CYC:12792 $F7FE:70 01 BVS $F801 A:E8 X:78 Y:56 P:nvUbdIZC SP:F9 PPU:169,112 CYC:12794 $F800:60 RTS A:E8 X:78 Y:56 P:nvUbdIZC SP:F9 PPU:175,112 CYC:12796 $E23E:C8 INY A:E8 X:78 Y:56 P:nvUbdIZC SP:FB PPU:193,112 CYC:12802 $E23F:A9 AA LDA #$AA A:E8 X:78 Y:57 P:25 SP:FB PPU:199,112 CYC:12804 $E241:8D 78 06 STA $0678 = #$EF A:AA X:78 Y:57 P:A5 SP:FB PPU:205,112 CYC:12806 $E244:20 04 F8 JSR $F804 A:AA X:78 Y:57 P:A5 SP:FB PPU:217,112 CYC:12810 $F804:18 CLC A:AA X:78 Y:57 P:A5 SP:F9 PPU:235,112 CYC:12816 $F805:24 01 BIT $01 = #$FF A:AA X:78 Y:57 P:NvUbdIzc SP:F9 PPU:241,112 CYC:12818 $F807:A9 5F LDA #$5F A:AA X:78 Y:57 P:NVUbdIzc SP:F9 PPU:250,112 CYC:12821 $F809:60 RTS A:5F X:78 Y:57 P:64 SP:F9 PPU:256,112 CYC:12823 $E247:5D 00 06 EOR $0600,X @ 0678 = #$AA A:5F X:78 Y:57 P:64 SP:FB PPU:274,112 CYC:12829 $E24A:20 0A F8 JSR $F80A A:F5 X:78 Y:57 P:NVUbdIzc SP:FB PPU:286,112 CYC:12833 $F80A:B0 09 BCS $F815 A:F5 X:78 Y:57 P:NVUbdIzc SP:F9 PPU:304,112 CYC:12839 $F80C:10 07 BPL $F815 A:F5 X:78 Y:57 P:NVUbdIzc SP:F9 PPU:310,112 CYC:12841 $F80E:C9 F5 CMP #$F5 A:F5 X:78 Y:57 P:NVUbdIzc SP:F9 PPU:316,112 CYC:12843 $F810:D0 03 BNE $F815 A:F5 X:78 Y:57 P:67 SP:F9 PPU:322,112 CYC:12845 $F812:50 01 BVC $F815 A:F5 X:78 Y:57 P:67 SP:F9 PPU:328,112 CYC:12847 $F814:60 RTS A:F5 X:78 Y:57 P:67 SP:F9 PPU:334,112 CYC:12849 $E24D:C8 INY A:F5 X:78 Y:57 P:67 SP:FB PPU: 11,113 CYC:12855 $E24E:A9 70 LDA #$70 A:F5 X:78 Y:58 P:65 SP:FB PPU: 17,113 CYC:12857 $E250:8D 78 06 STA $0678 = #$AA A:70 X:78 Y:58 P:65 SP:FB PPU: 23,113 CYC:12859 $E253:20 18 F8 JSR $F818 A:70 X:78 Y:58 P:65 SP:FB PPU: 35,113 CYC:12863 $F818:38 SEC A:70 X:78 Y:58 P:65 SP:F9 PPU: 53,113 CYC:12869 $F819:B8 CLV A:70 X:78 Y:58 P:65 SP:F9 PPU: 59,113 CYC:12871 $F81A:A9 70 LDA #$70 A:70 X:78 Y:58 P:25 SP:F9 PPU: 65,113 CYC:12873 $F81C:60 RTS A:70 X:78 Y:58 P:25 SP:F9 PPU: 71,113 CYC:12875 $E256:5D 00 06 EOR $0600,X @ 0678 = #$70 A:70 X:78 Y:58 P:25 SP:FB PPU: 89,113 CYC:12881 $E259:20 1D F8 JSR $F81D A:00 X:78 Y:58 P:nvUbdIZC SP:FB PPU:101,113 CYC:12885 $F81D:D0 07 BNE $F826 A:00 X:78 Y:58 P:nvUbdIZC SP:F9 PPU:119,113 CYC:12891 $F81F:70 05 BVS $F826 A:00 X:78 Y:58 P:nvUbdIZC SP:F9 PPU:125,113 CYC:12893 $F821:90 03 BCC $F826 A:00 X:78 Y:58 P:nvUbdIZC SP:F9 PPU:131,113 CYC:12895 $F823:30 01 BMI $F826 A:00 X:78 Y:58 P:nvUbdIZC SP:F9 PPU:137,113 CYC:12897 $F825:60 RTS A:00 X:78 Y:58 P:nvUbdIZC SP:F9 PPU:143,113 CYC:12899 $E25C:C8 INY A:00 X:78 Y:58 P:nvUbdIZC SP:FB PPU:161,113 CYC:12905 $E25D:A9 69 LDA #$69 A:00 X:78 Y:59 P:25 SP:FB PPU:167,113 CYC:12907 $E25F:8D 78 06 STA $0678 = #$70 A:69 X:78 Y:59 P:25 SP:FB PPU:173,113 CYC:12909 $E262:20 29 F8 JSR $F829 A:69 X:78 Y:59 P:25 SP:FB PPU:185,113 CYC:12913 $F829:18 CLC A:69 X:78 Y:59 P:25 SP:F9 PPU:203,113 CYC:12919 $F82A:24 01 BIT $01 = #$FF A:69 X:78 Y:59 P:nvUbdIzc SP:F9 PPU:209,113 CYC:12921 $F82C:A9 00 LDA #$00 A:69 X:78 Y:59 P:NVUbdIzc SP:F9 PPU:218,113 CYC:12924 $F82E:60 RTS A:00 X:78 Y:59 P:nVUbdIZc SP:F9 PPU:224,113 CYC:12926 $E265:7D 00 06 ADC $0600,X @ 0678 = #$69 A:00 X:78 Y:59 P:nVUbdIZc SP:FB PPU:242,113 CYC:12932 $E268:20 2F F8 JSR $F82F A:69 X:78 Y:59 P:nvUbdIzc SP:FB PPU:254,113 CYC:12936 $F82F:30 09 BMI $F83A A:69 X:78 Y:59 P:nvUbdIzc SP:F9 PPU:272,113 CYC:12942 $F831:B0 07 BCS $F83A A:69 X:78 Y:59 P:nvUbdIzc SP:F9 PPU:278,113 CYC:12944 $F833:C9 69 CMP #$69 A:69 X:78 Y:59 P:nvUbdIzc SP:F9 PPU:284,113 CYC:12946 $F835:D0 03 BNE $F83A A:69 X:78 Y:59 P:nvUbdIZC SP:F9 PPU:290,113 CYC:12948 $F837:70 01 BVS $F83A A:69 X:78 Y:59 P:nvUbdIZC SP:F9 PPU:296,113 CYC:12950 $F839:60 RTS A:69 X:78 Y:59 P:nvUbdIZC SP:F9 PPU:302,113 CYC:12952 $E26B:C8 INY A:69 X:78 Y:59 P:nvUbdIZC SP:FB PPU:320,113 CYC:12958 $E26C:20 3D F8 JSR $F83D A:69 X:78 Y:5A P:25 SP:FB PPU:326,113 CYC:12960 $F83D:38 SEC A:69 X:78 Y:5A P:25 SP:F9 PPU: 3,114 CYC:12966 $F83E:24 01 BIT $01 = #$FF A:69 X:78 Y:5A P:25 SP:F9 PPU: 9,114 CYC:12968 $F840:A9 00 LDA #$00 A:69 X:78 Y:5A P:E5 SP:F9 PPU: 18,114 CYC:12971 $F842:60 RTS A:00 X:78 Y:5A P:67 SP:F9 PPU: 24,114 CYC:12973 $E26F:7D 00 06 ADC $0600,X @ 0678 = #$69 A:00 X:78 Y:5A P:67 SP:FB PPU: 42,114 CYC:12979 $E272:20 43 F8 JSR $F843 A:6A X:78 Y:5A P:nvUbdIzc SP:FB PPU: 54,114 CYC:12983 $F843:30 09 BMI $F84E A:6A X:78 Y:5A P:nvUbdIzc SP:F9 PPU: 72,114 CYC:12989 $F845:B0 07 BCS $F84E A:6A X:78 Y:5A P:nvUbdIzc SP:F9 PPU: 78,114 CYC:12991 $F847:C9 6A CMP #$6A A:6A X:78 Y:5A P:nvUbdIzc SP:F9 PPU: 84,114 CYC:12993 $F849:D0 03 BNE $F84E A:6A X:78 Y:5A P:nvUbdIZC SP:F9 PPU: 90,114 CYC:12995 $F84B:70 01 BVS $F84E A:6A X:78 Y:5A P:nvUbdIZC SP:F9 PPU: 96,114 CYC:12997 $F84D:60 RTS A:6A X:78 Y:5A P:nvUbdIZC SP:F9 PPU:102,114 CYC:12999 $E275:C8 INY A:6A X:78 Y:5A P:nvUbdIZC SP:FB PPU:120,114 CYC:13005 $E276:A9 7F LDA #$7F A:6A X:78 Y:5B P:25 SP:FB PPU:126,114 CYC:13007 $E278:8D 78 06 STA $0678 = #$69 A:7F X:78 Y:5B P:25 SP:FB PPU:132,114 CYC:13009 $E27B:20 51 F8 JSR $F851 A:7F X:78 Y:5B P:25 SP:FB PPU:144,114 CYC:13013 $F851:38 SEC A:7F X:78 Y:5B P:25 SP:F9 PPU:162,114 CYC:13019 $F852:B8 CLV A:7F X:78 Y:5B P:25 SP:F9 PPU:168,114 CYC:13021 $F853:A9 7F LDA #$7F A:7F X:78 Y:5B P:25 SP:F9 PPU:174,114 CYC:13023 $F855:60 RTS A:7F X:78 Y:5B P:25 SP:F9 PPU:180,114 CYC:13025 $E27E:7D 00 06 ADC $0600,X @ 0678 = #$7F A:7F X:78 Y:5B P:25 SP:FB PPU:198,114 CYC:13031 $E281:20 56 F8 JSR $F856 A:FF X:78 Y:5B P:NVUbdIzc SP:FB PPU:210,114 CYC:13035 $F856:10 09 BPL $F861 A:FF X:78 Y:5B P:NVUbdIzc SP:F9 PPU:228,114 CYC:13041 $F858:B0 07 BCS $F861 A:FF X:78 Y:5B P:NVUbdIzc SP:F9 PPU:234,114 CYC:13043 $F85A:C9 FF CMP #$FF A:FF X:78 Y:5B P:NVUbdIzc SP:F9 PPU:240,114 CYC:13045 $F85C:D0 03 BNE $F861 A:FF X:78 Y:5B P:67 SP:F9 PPU:246,114 CYC:13047 $F85E:50 01 BVC $F861 A:FF X:78 Y:5B P:67 SP:F9 PPU:252,114 CYC:13049 $F860:60 RTS A:FF X:78 Y:5B P:67 SP:F9 PPU:258,114 CYC:13051 $E284:C8 INY A:FF X:78 Y:5B P:67 SP:FB PPU:276,114 CYC:13057 $E285:A9 80 LDA #$80 A:FF X:78 Y:5C P:65 SP:FB PPU:282,114 CYC:13059 $E287:8D 78 06 STA $0678 = #$7F A:80 X:78 Y:5C P:E5 SP:FB PPU:288,114 CYC:13061 $E28A:20 64 F8 JSR $F864 A:80 X:78 Y:5C P:E5 SP:FB PPU:300,114 CYC:13065 $F864:18 CLC A:80 X:78 Y:5C P:E5 SP:F9 PPU:318,114 CYC:13071 $F865:24 01 BIT $01 = #$FF A:80 X:78 Y:5C P:NVUbdIzc SP:F9 PPU:324,114 CYC:13073 $F867:A9 7F LDA #$7F A:80 X:78 Y:5C P:NVUbdIzc SP:F9 PPU:333,114 CYC:13076 $F869:60 RTS A:7F X:78 Y:5C P:64 SP:F9 PPU:339,114 CYC:13078 $E28D:7D 00 06 ADC $0600,X @ 0678 = #$80 A:7F X:78 Y:5C P:64 SP:FB PPU: 16,115 CYC:13084 $E290:20 6A F8 JSR $F86A A:FF X:78 Y:5C P:NvUbdIzc SP:FB PPU: 28,115 CYC:13088 $F86A:10 09 BPL $F875 A:FF X:78 Y:5C P:NvUbdIzc SP:F9 PPU: 46,115 CYC:13094 $F86C:B0 07 BCS $F875 A:FF X:78 Y:5C P:NvUbdIzc SP:F9 PPU: 52,115 CYC:13096 $F86E:C9 FF CMP #$FF A:FF X:78 Y:5C P:NvUbdIzc SP:F9 PPU: 58,115 CYC:13098 $F870:D0 03 BNE $F875 A:FF X:78 Y:5C P:nvUbdIZC SP:F9 PPU: 64,115 CYC:13100 $F872:70 01 BVS $F875 A:FF X:78 Y:5C P:nvUbdIZC SP:F9 PPU: 70,115 CYC:13102 $F874:60 RTS A:FF X:78 Y:5C P:nvUbdIZC SP:F9 PPU: 76,115 CYC:13104 $E293:C8 INY A:FF X:78 Y:5C P:nvUbdIZC SP:FB PPU: 94,115 CYC:13110 $E294:20 78 F8 JSR $F878 A:FF X:78 Y:5D P:25 SP:FB PPU:100,115 CYC:13112 $F878:38 SEC A:FF X:78 Y:5D P:25 SP:F9 PPU:118,115 CYC:13118 $F879:B8 CLV A:FF X:78 Y:5D P:25 SP:F9 PPU:124,115 CYC:13120 $F87A:A9 7F LDA #$7F A:FF X:78 Y:5D P:25 SP:F9 PPU:130,115 CYC:13122 $F87C:60 RTS A:7F X:78 Y:5D P:25 SP:F9 PPU:136,115 CYC:13124 $E297:7D 00 06 ADC $0600,X @ 0678 = #$80 A:7F X:78 Y:5D P:25 SP:FB PPU:154,115 CYC:13130 $E29A:20 7D F8 JSR $F87D A:00 X:78 Y:5D P:nvUbdIZC SP:FB PPU:166,115 CYC:13134 $F87D:D0 07 BNE $F886 A:00 X:78 Y:5D P:nvUbdIZC SP:F9 PPU:184,115 CYC:13140 $F87F:30 05 BMI $F886 A:00 X:78 Y:5D P:nvUbdIZC SP:F9 PPU:190,115 CYC:13142 $F881:70 03 BVS $F886 A:00 X:78 Y:5D P:nvUbdIZC SP:F9 PPU:196,115 CYC:13144 $F883:90 01 BCC $F886 A:00 X:78 Y:5D P:nvUbdIZC SP:F9 PPU:202,115 CYC:13146 $F885:60 RTS A:00 X:78 Y:5D P:nvUbdIZC SP:F9 PPU:208,115 CYC:13148 $E29D:C8 INY A:00 X:78 Y:5D P:nvUbdIZC SP:FB PPU:226,115 CYC:13154 $E29E:A9 40 LDA #$40 A:00 X:78 Y:5E P:25 SP:FB PPU:232,115 CYC:13156 $E2A0:8D 78 06 STA $0678 = #$80 A:40 X:78 Y:5E P:25 SP:FB PPU:238,115 CYC:13158 $E2A3:20 89 F8 JSR $F889 A:40 X:78 Y:5E P:25 SP:FB PPU:250,115 CYC:13162 $F889:24 01 BIT $01 = #$FF A:40 X:78 Y:5E P:25 SP:F9 PPU:268,115 CYC:13168 $F88B:A9 40 LDA #$40 A:40 X:78 Y:5E P:E5 SP:F9 PPU:277,115 CYC:13171 $F88D:60 RTS A:40 X:78 Y:5E P:65 SP:F9 PPU:283,115 CYC:13173 $E2A6:DD 00 06 CMP $0600,X @ 0678 = #$40 A:40 X:78 Y:5E P:65 SP:FB PPU:301,115 CYC:13179 $E2A9:20 8E F8 JSR $F88E A:40 X:78 Y:5E P:67 SP:FB PPU:313,115 CYC:13183 $F88E:30 07 BMI $F897 A:40 X:78 Y:5E P:67 SP:F9 PPU:331,115 CYC:13189 $F890:90 05 BCC $F897 A:40 X:78 Y:5E P:67 SP:F9 PPU:337,115 CYC:13191 $F892:D0 03 BNE $F897 A:40 X:78 Y:5E P:67 SP:F9 PPU: 2,116 CYC:13193 $F894:50 01 BVC $F897 A:40 X:78 Y:5E P:67 SP:F9 PPU: 8,116 CYC:13195 $F896:60 RTS A:40 X:78 Y:5E P:67 SP:F9 PPU: 14,116 CYC:13197 $E2AC:C8 INY A:40 X:78 Y:5E P:67 SP:FB PPU: 32,116 CYC:13203 $E2AD:48 PHA A:40 X:78 Y:5F P:65 SP:FB PPU: 38,116 CYC:13205 $E2AE:A9 3F LDA #$3F A:40 X:78 Y:5F P:65 SP:FA PPU: 47,116 CYC:13208 $E2B0:8D 78 06 STA $0678 = #$40 A:3F X:78 Y:5F P:65 SP:FA PPU: 53,116 CYC:13210 $E2B3:68 PLA A:3F X:78 Y:5F P:65 SP:FA PPU: 65,116 CYC:13214 $E2B4:20 9A F8 JSR $F89A A:40 X:78 Y:5F P:65 SP:FB PPU: 77,116 CYC:13218 $F89A:B8 CLV A:40 X:78 Y:5F P:65 SP:F9 PPU: 95,116 CYC:13224 $F89B:60 RTS A:40 X:78 Y:5F P:25 SP:F9 PPU:101,116 CYC:13226 $E2B7:DD 00 06 CMP $0600,X @ 0678 = #$3F A:40 X:78 Y:5F P:25 SP:FB PPU:119,116 CYC:13232 $E2BA:20 9C F8 JSR $F89C A:40 X:78 Y:5F P:25 SP:FB PPU:131,116 CYC:13236 $F89C:F0 07 BEQ $F8A5 A:40 X:78 Y:5F P:25 SP:F9 PPU:149,116 CYC:13242 $F89E:30 05 BMI $F8A5 A:40 X:78 Y:5F P:25 SP:F9 PPU:155,116 CYC:13244 $F8A0:90 03 BCC $F8A5 A:40 X:78 Y:5F P:25 SP:F9 PPU:161,116 CYC:13246 $F8A2:70 01 BVS $F8A5 A:40 X:78 Y:5F P:25 SP:F9 PPU:167,116 CYC:13248 $F8A4:60 RTS A:40 X:78 Y:5F P:25 SP:F9 PPU:173,116 CYC:13250 $E2BD:C8 INY A:40 X:78 Y:5F P:25 SP:FB PPU:191,116 CYC:13256 $E2BE:48 PHA A:40 X:78 Y:60 P:25 SP:FB PPU:197,116 CYC:13258 $E2BF:A9 41 LDA #$41 A:40 X:78 Y:60 P:25 SP:FA PPU:206,116 CYC:13261 $E2C1:8D 78 06 STA $0678 = #$3F A:41 X:78 Y:60 P:25 SP:FA PPU:212,116 CYC:13263 $E2C4:68 PLA A:41 X:78 Y:60 P:25 SP:FA PPU:224,116 CYC:13267 $E2C5:DD 00 06 CMP $0600,X @ 0678 = #$41 A:40 X:78 Y:60 P:25 SP:FB PPU:236,116 CYC:13271 $E2C8:20 A8 F8 JSR $F8A8 A:40 X:78 Y:60 P:NvUbdIzc SP:FB PPU:248,116 CYC:13275 $F8A8:F0 05 BEQ $F8AF A:40 X:78 Y:60 P:NvUbdIzc SP:F9 PPU:266,116 CYC:13281 $F8AA:10 03 BPL $F8AF A:40 X:78 Y:60 P:NvUbdIzc SP:F9 PPU:272,116 CYC:13283 $F8AC:10 01 BPL $F8AF A:40 X:78 Y:60 P:NvUbdIzc SP:F9 PPU:278,116 CYC:13285 $F8AE:60 RTS A:40 X:78 Y:60 P:NvUbdIzc SP:F9 PPU:284,116 CYC:13287 $E2CB:C8 INY A:40 X:78 Y:60 P:NvUbdIzc SP:FB PPU:302,116 CYC:13293 $E2CC:48 PHA A:40 X:78 Y:61 P:nvUbdIzc SP:FB PPU:308,116 CYC:13295 $E2CD:A9 00 LDA #$00 A:40 X:78 Y:61 P:nvUbdIzc SP:FA PPU:317,116 CYC:13298 $E2CF:8D 78 06 STA $0678 = #$41 A:00 X:78 Y:61 P:nvUbdIZc SP:FA PPU:323,116 CYC:13300 $E2D2:68 PLA A:00 X:78 Y:61 P:nvUbdIZc SP:FA PPU:335,116 CYC:13304 $E2D3:20 B2 F8 JSR $F8B2 A:40 X:78 Y:61 P:nvUbdIzc SP:FB PPU: 6,117 CYC:13308 $F8B2:A9 80 LDA #$80 A:40 X:78 Y:61 P:nvUbdIzc SP:F9 PPU: 24,117 CYC:13314 $F8B4:60 RTS A:80 X:78 Y:61 P:NvUbdIzc SP:F9 PPU: 30,117 CYC:13316 $E2D6:DD 00 06 CMP $0600,X @ 0678 = #$00 A:80 X:78 Y:61 P:NvUbdIzc SP:FB PPU: 48,117 CYC:13322 $E2D9:20 B5 F8 JSR $F8B5 A:80 X:78 Y:61 P:A5 SP:FB PPU: 60,117 CYC:13326 $F8B5:F0 05 BEQ $F8BC A:80 X:78 Y:61 P:A5 SP:F9 PPU: 78,117 CYC:13332 $F8B7:10 03 BPL $F8BC A:80 X:78 Y:61 P:A5 SP:F9 PPU: 84,117 CYC:13334 $F8B9:90 01 BCC $F8BC A:80 X:78 Y:61 P:A5 SP:F9 PPU: 90,117 CYC:13336 $F8BB:60 RTS A:80 X:78 Y:61 P:A5 SP:F9 PPU: 96,117 CYC:13338 $E2DC:C8 INY A:80 X:78 Y:61 P:A5 SP:FB PPU:114,117 CYC:13344 $E2DD:48 PHA A:80 X:78 Y:62 P:25 SP:FB PPU:120,117 CYC:13346 $E2DE:A9 80 LDA #$80 A:80 X:78 Y:62 P:25 SP:FA PPU:129,117 CYC:13349 $E2E0:8D 78 06 STA $0678 = #$00 A:80 X:78 Y:62 P:A5 SP:FA PPU:135,117 CYC:13351 $E2E3:68 PLA A:80 X:78 Y:62 P:A5 SP:FA PPU:147,117 CYC:13355 $E2E4:DD 00 06 CMP $0600,X @ 0678 = #$80 A:80 X:78 Y:62 P:A5 SP:FB PPU:159,117 CYC:13359 $E2E7:20 BF F8 JSR $F8BF A:80 X:78 Y:62 P:nvUbdIZC SP:FB PPU:171,117 CYC:13363 $F8BF:D0 05 BNE $F8C6 A:80 X:78 Y:62 P:nvUbdIZC SP:F9 PPU:189,117 CYC:13369 $F8C1:30 03 BMI $F8C6 A:80 X:78 Y:62 P:nvUbdIZC SP:F9 PPU:195,117 CYC:13371 $F8C3:90 01 BCC $F8C6 A:80 X:78 Y:62 P:nvUbdIZC SP:F9 PPU:201,117 CYC:13373 $F8C5:60 RTS A:80 X:78 Y:62 P:nvUbdIZC SP:F9 PPU:207,117 CYC:13375 $E2EA:C8 INY A:80 X:78 Y:62 P:nvUbdIZC SP:FB PPU:225,117 CYC:13381 $E2EB:48 PHA A:80 X:78 Y:63 P:25 SP:FB PPU:231,117 CYC:13383 $E2EC:A9 81 LDA #$81 A:80 X:78 Y:63 P:25 SP:FA PPU:240,117 CYC:13386 $E2EE:8D 78 06 STA $0678 = #$80 A:81 X:78 Y:63 P:A5 SP:FA PPU:246,117 CYC:13388 $E2F1:68 PLA A:81 X:78 Y:63 P:A5 SP:FA PPU:258,117 CYC:13392 $E2F2:DD 00 06 CMP $0600,X @ 0678 = #$81 A:80 X:78 Y:63 P:A5 SP:FB PPU:270,117 CYC:13396 $E2F5:20 C9 F8 JSR $F8C9 A:80 X:78 Y:63 P:NvUbdIzc SP:FB PPU:282,117 CYC:13400 $F8C9:B0 05 BCS $F8D0 A:80 X:78 Y:63 P:NvUbdIzc SP:F9 PPU:300,117 CYC:13406 $F8CB:F0 03 BEQ $F8D0 A:80 X:78 Y:63 P:NvUbdIzc SP:F9 PPU:306,117 CYC:13408 $F8CD:10 01 BPL $F8D0 A:80 X:78 Y:63 P:NvUbdIzc SP:F9 PPU:312,117 CYC:13410 $F8CF:60 RTS A:80 X:78 Y:63 P:NvUbdIzc SP:F9 PPU:318,117 CYC:13412 $E2F8:C8 INY A:80 X:78 Y:63 P:NvUbdIzc SP:FB PPU:336,117 CYC:13418 $E2F9:48 PHA A:80 X:78 Y:64 P:nvUbdIzc SP:FB PPU: 1,118 CYC:13420 $E2FA:A9 7F LDA #$7F A:80 X:78 Y:64 P:nvUbdIzc SP:FA PPU: 10,118 CYC:13423 $E2FC:8D 78 06 STA $0678 = #$81 A:7F X:78 Y:64 P:nvUbdIzc SP:FA PPU: 16,118 CYC:13425 $E2FF:68 PLA A:7F X:78 Y:64 P:nvUbdIzc SP:FA PPU: 28,118 CYC:13429 $E300:DD 00 06 CMP $0600,X @ 0678 = #$7F A:80 X:78 Y:64 P:NvUbdIzc SP:FB PPU: 40,118 CYC:13433 $E303:20 D3 F8 JSR $F8D3 A:80 X:78 Y:64 P:25 SP:FB PPU: 52,118 CYC:13437 $F8D3:90 05 BCC $F8DA A:80 X:78 Y:64 P:25 SP:F9 PPU: 70,118 CYC:13443 $F8D5:F0 03 BEQ $F8DA A:80 X:78 Y:64 P:25 SP:F9 PPU: 76,118 CYC:13445 $F8D7:30 01 BMI $F8DA A:80 X:78 Y:64 P:25 SP:F9 PPU: 82,118 CYC:13447 $F8D9:60 RTS A:80 X:78 Y:64 P:25 SP:F9 PPU: 88,118 CYC:13449 $E306:C8 INY A:80 X:78 Y:64 P:25 SP:FB PPU:106,118 CYC:13455 $E307:A9 40 LDA #$40 A:80 X:78 Y:65 P:25 SP:FB PPU:112,118 CYC:13457 $E309:8D 78 06 STA $0678 = #$7F A:40 X:78 Y:65 P:25 SP:FB PPU:118,118 CYC:13459 $E30C:20 31 F9 JSR $F931 A:40 X:78 Y:65 P:25 SP:FB PPU:130,118 CYC:13463 $F931:24 01 BIT $01 = #$FF A:40 X:78 Y:65 P:25 SP:F9 PPU:148,118 CYC:13469 $F933:A9 40 LDA #$40 A:40 X:78 Y:65 P:E5 SP:F9 PPU:157,118 CYC:13472 $F935:38 SEC A:40 X:78 Y:65 P:65 SP:F9 PPU:163,118 CYC:13474 $F936:60 RTS A:40 X:78 Y:65 P:65 SP:F9 PPU:169,118 CYC:13476 $E30F:FD 00 06 SBC $0600,X @ 0678 = #$40 A:40 X:78 Y:65 P:65 SP:FB PPU:187,118 CYC:13482 $E312:20 37 F9 JSR $F937 A:00 X:78 Y:65 P:nvUbdIZC SP:FB PPU:199,118 CYC:13486 $F937:30 0B BMI $F944 A:00 X:78 Y:65 P:nvUbdIZC SP:F9 PPU:217,118 CYC:13492 $F939:90 09 BCC $F944 A:00 X:78 Y:65 P:nvUbdIZC SP:F9 PPU:223,118 CYC:13494 $F93B:D0 07 BNE $F944 A:00 X:78 Y:65 P:nvUbdIZC SP:F9 PPU:229,118 CYC:13496 $F93D:70 05 BVS $F944 A:00 X:78 Y:65 P:nvUbdIZC SP:F9 PPU:235,118 CYC:13498 $F93F:C9 00 CMP #$00 A:00 X:78 Y:65 P:nvUbdIZC SP:F9 PPU:241,118 CYC:13500 $F941:D0 01 BNE $F944 A:00 X:78 Y:65 P:nvUbdIZC SP:F9 PPU:247,118 CYC:13502 $F943:60 RTS A:00 X:78 Y:65 P:nvUbdIZC SP:F9 PPU:253,118 CYC:13504 $E315:C8 INY A:00 X:78 Y:65 P:nvUbdIZC SP:FB PPU:271,118 CYC:13510 $E316:A9 3F LDA #$3F A:00 X:78 Y:66 P:25 SP:FB PPU:277,118 CYC:13512 $E318:8D 78 06 STA $0678 = #$40 A:3F X:78 Y:66 P:25 SP:FB PPU:283,118 CYC:13514 $E31B:20 47 F9 JSR $F947 A:3F X:78 Y:66 P:25 SP:FB PPU:295,118 CYC:13518 $F947:B8 CLV A:3F X:78 Y:66 P:25 SP:F9 PPU:313,118 CYC:13524 $F948:38 SEC A:3F X:78 Y:66 P:25 SP:F9 PPU:319,118 CYC:13526 $F949:A9 40 LDA #$40 A:3F X:78 Y:66 P:25 SP:F9 PPU:325,118 CYC:13528 $F94B:60 RTS A:40 X:78 Y:66 P:25 SP:F9 PPU:331,118 CYC:13530 $E31E:FD 00 06 SBC $0600,X @ 0678 = #$3F A:40 X:78 Y:66 P:25 SP:FB PPU: 8,119 CYC:13536 $E321:20 4C F9 JSR $F94C A:01 X:78 Y:66 P:25 SP:FB PPU: 20,119 CYC:13540 $F94C:F0 0B BEQ $F959 A:01 X:78 Y:66 P:25 SP:F9 PPU: 38,119 CYC:13546 $F94E:30 09 BMI $F959 A:01 X:78 Y:66 P:25 SP:F9 PPU: 44,119 CYC:13548 $F950:90 07 BCC $F959 A:01 X:78 Y:66 P:25 SP:F9 PPU: 50,119 CYC:13550 $F952:70 05 BVS $F959 A:01 X:78 Y:66 P:25 SP:F9 PPU: 56,119 CYC:13552 $F954:C9 01 CMP #$01 A:01 X:78 Y:66 P:25 SP:F9 PPU: 62,119 CYC:13554 $F956:D0 01 BNE $F959 A:01 X:78 Y:66 P:nvUbdIZC SP:F9 PPU: 68,119 CYC:13556 $F958:60 RTS A:01 X:78 Y:66 P:nvUbdIZC SP:F9 PPU: 74,119 CYC:13558 $E324:C8 INY A:01 X:78 Y:66 P:nvUbdIZC SP:FB PPU: 92,119 CYC:13564 $E325:A9 41 LDA #$41 A:01 X:78 Y:67 P:25 SP:FB PPU: 98,119 CYC:13566 $E327:8D 78 06 STA $0678 = #$3F A:41 X:78 Y:67 P:25 SP:FB PPU:104,119 CYC:13568 $E32A:20 5C F9 JSR $F95C A:41 X:78 Y:67 P:25 SP:FB PPU:116,119 CYC:13572 $F95C:A9 40 LDA #$40 A:41 X:78 Y:67 P:25 SP:F9 PPU:134,119 CYC:13578 $F95E:38 SEC A:40 X:78 Y:67 P:25 SP:F9 PPU:140,119 CYC:13580 $F95F:24 01 BIT $01 = #$FF A:40 X:78 Y:67 P:25 SP:F9 PPU:146,119 CYC:13582 $F961:60 RTS A:40 X:78 Y:67 P:E5 SP:F9 PPU:155,119 CYC:13585 $E32D:FD 00 06 SBC $0600,X @ 0678 = #$41 A:40 X:78 Y:67 P:E5 SP:FB PPU:173,119 CYC:13591 $E330:20 62 F9 JSR $F962 A:FF X:78 Y:67 P:NvUbdIzc SP:FB PPU:185,119 CYC:13595 $F962:B0 0B BCS $F96F A:FF X:78 Y:67 P:NvUbdIzc SP:F9 PPU:203,119 CYC:13601 $F964:F0 09 BEQ $F96F A:FF X:78 Y:67 P:NvUbdIzc SP:F9 PPU:209,119 CYC:13603 $F966:10 07 BPL $F96F A:FF X:78 Y:67 P:NvUbdIzc SP:F9 PPU:215,119 CYC:13605 $F968:70 05 BVS $F96F A:FF X:78 Y:67 P:NvUbdIzc SP:F9 PPU:221,119 CYC:13607 $F96A:C9 FF CMP #$FF A:FF X:78 Y:67 P:NvUbdIzc SP:F9 PPU:227,119 CYC:13609 $F96C:D0 01 BNE $F96F A:FF X:78 Y:67 P:nvUbdIZC SP:F9 PPU:233,119 CYC:13611 $F96E:60 RTS A:FF X:78 Y:67 P:nvUbdIZC SP:F9 PPU:239,119 CYC:13613 $E333:C8 INY A:FF X:78 Y:67 P:nvUbdIZC SP:FB PPU:257,119 CYC:13619 $E334:A9 00 LDA #$00 A:FF X:78 Y:68 P:25 SP:FB PPU:263,119 CYC:13621 $E336:8D 78 06 STA $0678 = #$41 A:00 X:78 Y:68 P:nvUbdIZC SP:FB PPU:269,119 CYC:13623 $E339:20 72 F9 JSR $F972 A:00 X:78 Y:68 P:nvUbdIZC SP:FB PPU:281,119 CYC:13627 $F972:18 CLC A:00 X:78 Y:68 P:nvUbdIZC SP:F9 PPU:299,119 CYC:13633 $F973:A9 80 LDA #$80 A:00 X:78 Y:68 P:nvUbdIZc SP:F9 PPU:305,119 CYC:13635 $F975:60 RTS A:80 X:78 Y:68 P:NvUbdIzc SP:F9 PPU:311,119 CYC:13637 $E33C:FD 00 06 SBC $0600,X @ 0678 = #$00 A:80 X:78 Y:68 P:NvUbdIzc SP:FB PPU:329,119 CYC:13643 $E33F:20 76 F9 JSR $F976 A:7F X:78 Y:68 P:65 SP:FB PPU: 0,120 CYC:13647 $F976:90 05 BCC $F97D A:7F X:78 Y:68 P:65 SP:F9 PPU: 18,120 CYC:13653 $F978:C9 7F CMP #$7F A:7F X:78 Y:68 P:65 SP:F9 PPU: 24,120 CYC:13655 $F97A:D0 01 BNE $F97D A:7F X:78 Y:68 P:67 SP:F9 PPU: 30,120 CYC:13657 $F97C:60 RTS A:7F X:78 Y:68 P:67 SP:F9 PPU: 36,120 CYC:13659 $E342:C8 INY A:7F X:78 Y:68 P:67 SP:FB PPU: 54,120 CYC:13665 $E343:A9 7F LDA #$7F A:7F X:78 Y:69 P:65 SP:FB PPU: 60,120 CYC:13667 $E345:8D 78 06 STA $0678 = #$00 A:7F X:78 Y:69 P:65 SP:FB PPU: 66,120 CYC:13669 $E348:20 80 F9 JSR $F980 A:7F X:78 Y:69 P:65 SP:FB PPU: 78,120 CYC:13673 $F980:38 SEC A:7F X:78 Y:69 P:65 SP:F9 PPU: 96,120 CYC:13679 $F981:A9 81 LDA #$81 A:7F X:78 Y:69 P:65 SP:F9 PPU:102,120 CYC:13681 $F983:60 RTS A:81 X:78 Y:69 P:E5 SP:F9 PPU:108,120 CYC:13683 $E34B:FD 00 06 SBC $0600,X @ 0678 = #$7F A:81 X:78 Y:69 P:E5 SP:FB PPU:126,120 CYC:13689 $E34E:20 84 F9 JSR $F984 A:02 X:78 Y:69 P:65 SP:FB PPU:138,120 CYC:13693 $F984:50 07 BVC $F98D A:02 X:78 Y:69 P:65 SP:F9 PPU:156,120 CYC:13699 $F986:90 05 BCC $F98D A:02 X:78 Y:69 P:65 SP:F9 PPU:162,120 CYC:13701 $F988:C9 02 CMP #$02 A:02 X:78 Y:69 P:65 SP:F9 PPU:168,120 CYC:13703 $F98A:D0 01 BNE $F98D A:02 X:78 Y:69 P:67 SP:F9 PPU:174,120 CYC:13705 $F98C:60 RTS A:02 X:78 Y:69 P:67 SP:F9 PPU:180,120 CYC:13707 $E351:A9 AA LDA #$AA A:02 X:78 Y:69 P:67 SP:FB PPU:198,120 CYC:13713 $E353:8D 33 06 STA $0633 = #$AA A:AA X:78 Y:69 P:E5 SP:FB PPU:204,120 CYC:13715 $E356:A9 BB LDA #$BB A:AA X:78 Y:69 P:E5 SP:FB PPU:216,120 CYC:13719 $E358:8D 89 06 STA $0689 = #$BB A:BB X:78 Y:69 P:E5 SP:FB PPU:222,120 CYC:13721 $E35B:A2 00 LDX #$00 A:BB X:78 Y:69 P:E5 SP:FB PPU:234,120 CYC:13725 $E35D:A0 66 LDY #$66 A:BB X:00 Y:69 P:67 SP:FB PPU:240,120 CYC:13727 $E35F:24 01 BIT $01 = #$FF A:BB X:00 Y:66 P:65 SP:FB PPU:246,120 CYC:13729 $E361:38 SEC A:BB X:00 Y:66 P:E5 SP:FB PPU:255,120 CYC:13732 $E362:A9 00 LDA #$00 A:BB X:00 Y:66 P:E5 SP:FB PPU:261,120 CYC:13734 $E364:BD 33 06 LDA $0633,X @ 0633 = #$AA A:00 X:00 Y:66 P:67 SP:FB PPU:267,120 CYC:13736 $E367:10 12 BPL $E37B A:AA X:00 Y:66 P:E5 SP:FB PPU:279,120 CYC:13740 $E369:F0 10 BEQ $E37B A:AA X:00 Y:66 P:E5 SP:FB PPU:285,120 CYC:13742 $E36B:50 0E BVC $E37B A:AA X:00 Y:66 P:E5 SP:FB PPU:291,120 CYC:13744 $E36D:90 0C BCC $E37B A:AA X:00 Y:66 P:E5 SP:FB PPU:297,120 CYC:13746 $E36F:C0 66 CPY #$66 A:AA X:00 Y:66 P:E5 SP:FB PPU:303,120 CYC:13748 $E371:D0 08 BNE $E37B A:AA X:00 Y:66 P:67 SP:FB PPU:309,120 CYC:13750 $E373:E0 00 CPX #$00 A:AA X:00 Y:66 P:67 SP:FB PPU:315,120 CYC:13752 $E375:D0 04 BNE $E37B A:AA X:00 Y:66 P:67 SP:FB PPU:321,120 CYC:13754 $E377:C9 AA CMP #$AA A:AA X:00 Y:66 P:67 SP:FB PPU:327,120 CYC:13756 $E379:F0 04 BEQ $E37F A:AA X:00 Y:66 P:67 SP:FB PPU:333,120 CYC:13758 $E37F:A2 8A LDX #$8A A:AA X:00 Y:66 P:67 SP:FB PPU: 1,121 CYC:13761 $E381:A0 66 LDY #$66 A:AA X:8A Y:66 P:E5 SP:FB PPU: 7,121 CYC:13763 $E383:B8 CLV A:AA X:8A Y:66 P:65 SP:FB PPU: 13,121 CYC:13765 $E384:18 CLC A:AA X:8A Y:66 P:25 SP:FB PPU: 19,121 CYC:13767 $E385:A9 00 LDA #$00 A:AA X:8A Y:66 P:nvUbdIzc SP:FB PPU: 25,121 CYC:13769 $E387:BD FF 05 LDA $05FF,X @ 0689 = #$BB A:00 X:8A Y:66 P:nvUbdIZc SP:FB PPU: 31,121 CYC:13771 $E38A:10 12 BPL $E39E A:BB X:8A Y:66 P:NvUbdIzc SP:FB PPU: 46,121 CYC:13776 $E38C:F0 10 BEQ $E39E A:BB X:8A Y:66 P:NvUbdIzc SP:FB PPU: 52,121 CYC:13778 $E38E:70 0E BVS $E39E A:BB X:8A Y:66 P:NvUbdIzc SP:FB PPU: 58,121 CYC:13780 $E390:B0 0C BCS $E39E A:BB X:8A Y:66 P:NvUbdIzc SP:FB PPU: 64,121 CYC:13782 $E392:C9 BB CMP #$BB A:BB X:8A Y:66 P:NvUbdIzc SP:FB PPU: 70,121 CYC:13784 $E394:D0 08 BNE $E39E A:BB X:8A Y:66 P:nvUbdIZC SP:FB PPU: 76,121 CYC:13786 $E396:C0 66 CPY #$66 A:BB X:8A Y:66 P:nvUbdIZC SP:FB PPU: 82,121 CYC:13788 $E398:D0 04 BNE $E39E A:BB X:8A Y:66 P:nvUbdIZC SP:FB PPU: 88,121 CYC:13790 $E39A:E0 8A CPX #$8A A:BB X:8A Y:66 P:nvUbdIZC SP:FB PPU: 94,121 CYC:13792 $E39C:F0 04 BEQ $E3A2 A:BB X:8A Y:66 P:nvUbdIZC SP:FB PPU:100,121 CYC:13794 $E3A2:24 01 BIT $01 = #$FF A:BB X:8A Y:66 P:nvUbdIZC SP:FB PPU:109,121 CYC:13797 $E3A4:38 SEC A:BB X:8A Y:66 P:E5 SP:FB PPU:118,121 CYC:13800 $E3A5:A9 44 LDA #$44 A:BB X:8A Y:66 P:E5 SP:FB PPU:124,121 CYC:13802 $E3A7:A2 00 LDX #$00 A:44 X:8A Y:66 P:65 SP:FB PPU:130,121 CYC:13804 $E3A9:9D 33 06 STA $0633,X @ 0633 = #$AA A:44 X:00 Y:66 P:67 SP:FB PPU:136,121 CYC:13806 $E3AC:AD 33 06 LDA $0633 = #$44 A:44 X:00 Y:66 P:67 SP:FB PPU:151,121 CYC:13811 $E3AF:90 1A BCC $E3CB A:44 X:00 Y:66 P:65 SP:FB PPU:163,121 CYC:13815 $E3B1:C9 44 CMP #$44 A:44 X:00 Y:66 P:65 SP:FB PPU:169,121 CYC:13817 $E3B3:D0 16 BNE $E3CB A:44 X:00 Y:66 P:67 SP:FB PPU:175,121 CYC:13819 $E3B5:50 14 BVC $E3CB A:44 X:00 Y:66 P:67 SP:FB PPU:181,121 CYC:13821 $E3B7:18 CLC A:44 X:00 Y:66 P:67 SP:FB PPU:187,121 CYC:13823 $E3B8:B8 CLV A:44 X:00 Y:66 P:nVUbdIZc SP:FB PPU:193,121 CYC:13825 $E3B9:A9 99 LDA #$99 A:44 X:00 Y:66 P:nvUbdIZc SP:FB PPU:199,121 CYC:13827 $E3BB:A2 80 LDX #$80 A:99 X:00 Y:66 P:NvUbdIzc SP:FB PPU:205,121 CYC:13829 $E3BD:9D 85 05 STA $0585,X @ 0605 = #$00 A:99 X:80 Y:66 P:NvUbdIzc SP:FB PPU:211,121 CYC:13831 $E3C0:AD 05 06 LDA $0605 = #$99 A:99 X:80 Y:66 P:NvUbdIzc SP:FB PPU:226,121 CYC:13836 $E3C3:B0 06 BCS $E3CB A:99 X:80 Y:66 P:NvUbdIzc SP:FB PPU:238,121 CYC:13840 $E3C5:C9 99 CMP #$99 A:99 X:80 Y:66 P:NvUbdIzc SP:FB PPU:244,121 CYC:13842 $E3C7:D0 02 BNE $E3CB A:99 X:80 Y:66 P:nvUbdIZC SP:FB PPU:250,121 CYC:13844 $E3C9:50 04 BVC $E3CF A:99 X:80 Y:66 P:nvUbdIZC SP:FB PPU:256,121 CYC:13846 $E3CF:A0 6D LDY #$6D A:99 X:80 Y:66 P:nvUbdIZC SP:FB PPU:265,121 CYC:13849 $E3D1:A2 6D LDX #$6D A:99 X:80 Y:6D P:25 SP:FB PPU:271,121 CYC:13851 $E3D3:20 90 F9 JSR $F990 A:99 X:6D Y:6D P:25 SP:FB PPU:277,121 CYC:13853 $F990:A2 55 LDX #$55 A:99 X:6D Y:6D P:25 SP:F9 PPU:295,121 CYC:13859 $F992:A9 FF LDA #$FF A:99 X:55 Y:6D P:25 SP:F9 PPU:301,121 CYC:13861 $F994:85 01 STA $01 = #$FF A:FF X:55 Y:6D P:A5 SP:F9 PPU:307,121 CYC:13863 $F996:EA NOP A:FF X:55 Y:6D P:A5 SP:F9 PPU:316,121 CYC:13866 $F997:24 01 BIT $01 = #$FF A:FF X:55 Y:6D P:A5 SP:F9 PPU:322,121 CYC:13868 $F999:38 SEC A:FF X:55 Y:6D P:E5 SP:F9 PPU:331,121 CYC:13871 $F99A:A9 01 LDA #$01 A:FF X:55 Y:6D P:E5 SP:F9 PPU:337,121 CYC:13873 $F99C:60 RTS A:01 X:55 Y:6D P:65 SP:F9 PPU: 2,122 CYC:13875 $E3D6:9D 00 06 STA $0600,X @ 0655 = #$00 A:01 X:55 Y:6D P:65 SP:FB PPU: 20,122 CYC:13881 $E3D9:5E 00 06 LSR $0600,X @ 0655 = #$01 A:01 X:55 Y:6D P:65 SP:FB PPU: 35,122 CYC:13886 $E3DC:BD 00 06 LDA $0600,X @ 0655 = #$00 A:01 X:55 Y:6D P:67 SP:FB PPU: 56,122 CYC:13893 $E3DF:20 9D F9 JSR $F99D A:00 X:55 Y:6D P:67 SP:FB PPU: 68,122 CYC:13897 $F99D:90 1B BCC $F9BA A:00 X:55 Y:6D P:67 SP:F9 PPU: 86,122 CYC:13903 $F99F:D0 19 BNE $F9BA A:00 X:55 Y:6D P:67 SP:F9 PPU: 92,122 CYC:13905 $F9A1:30 17 BMI $F9BA A:00 X:55 Y:6D P:67 SP:F9 PPU: 98,122 CYC:13907 $F9A3:50 15 BVC $F9BA A:00 X:55 Y:6D P:67 SP:F9 PPU:104,122 CYC:13909 $F9A5:C9 00 CMP #$00 A:00 X:55 Y:6D P:67 SP:F9 PPU:110,122 CYC:13911 $F9A7:D0 11 BNE $F9BA A:00 X:55 Y:6D P:67 SP:F9 PPU:116,122 CYC:13913 $F9A9:B8 CLV A:00 X:55 Y:6D P:67 SP:F9 PPU:122,122 CYC:13915 $F9AA:A9 AA LDA #$AA A:00 X:55 Y:6D P:nvUbdIZC SP:F9 PPU:128,122 CYC:13917 $F9AC:60 RTS A:AA X:55 Y:6D P:A5 SP:F9 PPU:134,122 CYC:13919 $E3E2:C8 INY A:AA X:55 Y:6D P:A5 SP:FB PPU:152,122 CYC:13925 $E3E3:9D 00 06 STA $0600,X @ 0655 = #$00 A:AA X:55 Y:6E P:25 SP:FB PPU:158,122 CYC:13927 $E3E6:5E 00 06 LSR $0600,X @ 0655 = #$AA A:AA X:55 Y:6E P:25 SP:FB PPU:173,122 CYC:13932 $E3E9:BD 00 06 LDA $0600,X @ 0655 = #$55 A:AA X:55 Y:6E P:nvUbdIzc SP:FB PPU:194,122 CYC:13939 $E3EC:20 AD F9 JSR $F9AD A:55 X:55 Y:6E P:nvUbdIzc SP:FB PPU:206,122 CYC:13943 $F9AD:B0 0B BCS $F9BA A:55 X:55 Y:6E P:nvUbdIzc SP:F9 PPU:224,122 CYC:13949 $F9AF:F0 09 BEQ $F9BA A:55 X:55 Y:6E P:nvUbdIzc SP:F9 PPU:230,122 CYC:13951 $F9B1:30 07 BMI $F9BA A:55 X:55 Y:6E P:nvUbdIzc SP:F9 PPU:236,122 CYC:13953 $F9B3:70 05 BVS $F9BA A:55 X:55 Y:6E P:nvUbdIzc SP:F9 PPU:242,122 CYC:13955 $F9B5:C9 55 CMP #$55 A:55 X:55 Y:6E P:nvUbdIzc SP:F9 PPU:248,122 CYC:13957 $F9B7:D0 01 BNE $F9BA A:55 X:55 Y:6E P:nvUbdIZC SP:F9 PPU:254,122 CYC:13959 $F9B9:60 RTS A:55 X:55 Y:6E P:nvUbdIZC SP:F9 PPU:260,122 CYC:13961 $E3EF:C8 INY A:55 X:55 Y:6E P:nvUbdIZC SP:FB PPU:278,122 CYC:13967 $E3F0:20 BD F9 JSR $F9BD A:55 X:55 Y:6F P:25 SP:FB PPU:284,122 CYC:13969 $F9BD:24 01 BIT $01 = #$FF A:55 X:55 Y:6F P:25 SP:F9 PPU:302,122 CYC:13975 $F9BF:38 SEC A:55 X:55 Y:6F P:E5 SP:F9 PPU:311,122 CYC:13978 $F9C0:A9 80 LDA #$80 A:55 X:55 Y:6F P:E5 SP:F9 PPU:317,122 CYC:13980 $F9C2:60 RTS A:80 X:55 Y:6F P:E5 SP:F9 PPU:323,122 CYC:13982 $E3F3:9D 00 06 STA $0600,X @ 0655 = #$55 A:80 X:55 Y:6F P:E5 SP:FB PPU: 0,123 CYC:13988 $E3F6:1E 00 06 ASL $0600,X @ 0655 = #$80 A:80 X:55 Y:6F P:E5 SP:FB PPU: 15,123 CYC:13993 $E3F9:BD 00 06 LDA $0600,X @ 0655 = #$00 A:80 X:55 Y:6F P:67 SP:FB PPU: 36,123 CYC:14000 $E3FC:20 C3 F9 JSR $F9C3 A:00 X:55 Y:6F P:67 SP:FB PPU: 48,123 CYC:14004 $F9C3:90 1C BCC $F9E1 A:00 X:55 Y:6F P:67 SP:F9 PPU: 66,123 CYC:14010 $F9C5:D0 1A BNE $F9E1 A:00 X:55 Y:6F P:67 SP:F9 PPU: 72,123 CYC:14012 $F9C7:30 18 BMI $F9E1 A:00 X:55 Y:6F P:67 SP:F9 PPU: 78,123 CYC:14014 $F9C9:50 16 BVC $F9E1 A:00 X:55 Y:6F P:67 SP:F9 PPU: 84,123 CYC:14016 $F9CB:C9 00 CMP #$00 A:00 X:55 Y:6F P:67 SP:F9 PPU: 90,123 CYC:14018 $F9CD:D0 12 BNE $F9E1 A:00 X:55 Y:6F P:67 SP:F9 PPU: 96,123 CYC:14020 $F9CF:B8 CLV A:00 X:55 Y:6F P:67 SP:F9 PPU:102,123 CYC:14022 $F9D0:A9 55 LDA #$55 A:00 X:55 Y:6F P:nvUbdIZC SP:F9 PPU:108,123 CYC:14024 $F9D2:38 SEC A:55 X:55 Y:6F P:25 SP:F9 PPU:114,123 CYC:14026 $F9D3:60 RTS A:55 X:55 Y:6F P:25 SP:F9 PPU:120,123 CYC:14028 $E3FF:C8 INY A:55 X:55 Y:6F P:25 SP:FB PPU:138,123 CYC:14034 $E400:9D 00 06 STA $0600,X @ 0655 = #$00 A:55 X:55 Y:70 P:25 SP:FB PPU:144,123 CYC:14036 $E403:1E 00 06 ASL $0600,X @ 0655 = #$55 A:55 X:55 Y:70 P:25 SP:FB PPU:159,123 CYC:14041 $E406:BD 00 06 LDA $0600,X @ 0655 = #$AA A:55 X:55 Y:70 P:NvUbdIzc SP:FB PPU:180,123 CYC:14048 $E409:20 D4 F9 JSR $F9D4 A:AA X:55 Y:70 P:NvUbdIzc SP:FB PPU:192,123 CYC:14052 $F9D4:B0 0B BCS $F9E1 A:AA X:55 Y:70 P:NvUbdIzc SP:F9 PPU:210,123 CYC:14058 $F9D6:F0 09 BEQ $F9E1 A:AA X:55 Y:70 P:NvUbdIzc SP:F9 PPU:216,123 CYC:14060 $F9D8:10 07 BPL $F9E1 A:AA X:55 Y:70 P:NvUbdIzc SP:F9 PPU:222,123 CYC:14062 $F9DA:70 05 BVS $F9E1 A:AA X:55 Y:70 P:NvUbdIzc SP:F9 PPU:228,123 CYC:14064 $F9DC:C9 AA CMP #$AA A:AA X:55 Y:70 P:NvUbdIzc SP:F9 PPU:234,123 CYC:14066 $F9DE:D0 01 BNE $F9E1 A:AA X:55 Y:70 P:nvUbdIZC SP:F9 PPU:240,123 CYC:14068 $F9E0:60 RTS A:AA X:55 Y:70 P:nvUbdIZC SP:F9 PPU:246,123 CYC:14070 $E40C:C8 INY A:AA X:55 Y:70 P:nvUbdIZC SP:FB PPU:264,123 CYC:14076 $E40D:20 E4 F9 JSR $F9E4 A:AA X:55 Y:71 P:25 SP:FB PPU:270,123 CYC:14078 $F9E4:24 01 BIT $01 = #$FF A:AA X:55 Y:71 P:25 SP:F9 PPU:288,123 CYC:14084 $F9E6:38 SEC A:AA X:55 Y:71 P:E5 SP:F9 PPU:297,123 CYC:14087 $F9E7:A9 01 LDA #$01 A:AA X:55 Y:71 P:E5 SP:F9 PPU:303,123 CYC:14089 $F9E9:60 RTS A:01 X:55 Y:71 P:65 SP:F9 PPU:309,123 CYC:14091 $E410:9D 00 06 STA $0600,X @ 0655 = #$AA A:01 X:55 Y:71 P:65 SP:FB PPU:327,123 CYC:14097 $E413:7E 00 06 ROR $0600,X @ 0655 = #$01 A:01 X:55 Y:71 P:65 SP:FB PPU: 1,124 CYC:14102 $E416:BD 00 06 LDA $0600,X @ 0655 = #$80 A:01 X:55 Y:71 P:E5 SP:FB PPU: 22,124 CYC:14109 $E419:20 EA F9 JSR $F9EA A:80 X:55 Y:71 P:E5 SP:FB PPU: 34,124 CYC:14113 $F9EA:90 1C BCC $FA08 A:80 X:55 Y:71 P:E5 SP:F9 PPU: 52,124 CYC:14119 $F9EC:F0 1A BEQ $FA08 A:80 X:55 Y:71 P:E5 SP:F9 PPU: 58,124 CYC:14121 $F9EE:10 18 BPL $FA08 A:80 X:55 Y:71 P:E5 SP:F9 PPU: 64,124 CYC:14123 $F9F0:50 16 BVC $FA08 A:80 X:55 Y:71 P:E5 SP:F9 PPU: 70,124 CYC:14125 $F9F2:C9 80 CMP #$80 A:80 X:55 Y:71 P:E5 SP:F9 PPU: 76,124 CYC:14127 $F9F4:D0 12 BNE $FA08 A:80 X:55 Y:71 P:67 SP:F9 PPU: 82,124 CYC:14129 $F9F6:B8 CLV A:80 X:55 Y:71 P:67 SP:F9 PPU: 88,124 CYC:14131 $F9F7:18 CLC A:80 X:55 Y:71 P:nvUbdIZC SP:F9 PPU: 94,124 CYC:14133 $F9F8:A9 55 LDA #$55 A:80 X:55 Y:71 P:nvUbdIZc SP:F9 PPU:100,124 CYC:14135 $F9FA:60 RTS A:55 X:55 Y:71 P:nvUbdIzc SP:F9 PPU:106,124 CYC:14137 $E41C:C8 INY A:55 X:55 Y:71 P:nvUbdIzc SP:FB PPU:124,124 CYC:14143 $E41D:9D 00 06 STA $0600,X @ 0655 = #$80 A:55 X:55 Y:72 P:nvUbdIzc SP:FB PPU:130,124 CYC:14145 $E420:7E 00 06 ROR $0600,X @ 0655 = #$55 A:55 X:55 Y:72 P:nvUbdIzc SP:FB PPU:145,124 CYC:14150 $E423:BD 00 06 LDA $0600,X @ 0655 = #$2A A:55 X:55 Y:72 P:25 SP:FB PPU:166,124 CYC:14157 $E426:20 FB F9 JSR $F9FB A:2A X:55 Y:72 P:25 SP:FB PPU:178,124 CYC:14161 $F9FB:90 0B BCC $FA08 A:2A X:55 Y:72 P:25 SP:F9 PPU:196,124 CYC:14167 $F9FD:F0 09 BEQ $FA08 A:2A X:55 Y:72 P:25 SP:F9 PPU:202,124 CYC:14169 $F9FF:30 07 BMI $FA08 A:2A X:55 Y:72 P:25 SP:F9 PPU:208,124 CYC:14171 $FA01:70 05 BVS $FA08 A:2A X:55 Y:72 P:25 SP:F9 PPU:214,124 CYC:14173 $FA03:C9 2A CMP #$2A A:2A X:55 Y:72 P:25 SP:F9 PPU:220,124 CYC:14175 $FA05:D0 01 BNE $FA08 A:2A X:55 Y:72 P:nvUbdIZC SP:F9 PPU:226,124 CYC:14177 $FA07:60 RTS A:2A X:55 Y:72 P:nvUbdIZC SP:F9 PPU:232,124 CYC:14179 $E429:C8 INY A:2A X:55 Y:72 P:nvUbdIZC SP:FB PPU:250,124 CYC:14185 $E42A:20 0A FA JSR $FA0A A:2A X:55 Y:73 P:25 SP:FB PPU:256,124 CYC:14187 $FA0A:24 01 BIT $01 = #$FF A:2A X:55 Y:73 P:25 SP:F9 PPU:274,124 CYC:14193 $FA0C:38 SEC A:2A X:55 Y:73 P:E5 SP:F9 PPU:283,124 CYC:14196 $FA0D:A9 80 LDA #$80 A:2A X:55 Y:73 P:E5 SP:F9 PPU:289,124 CYC:14198 $FA0F:60 RTS A:80 X:55 Y:73 P:E5 SP:F9 PPU:295,124 CYC:14200 $E42D:9D 00 06 STA $0600,X @ 0655 = #$2A A:80 X:55 Y:73 P:E5 SP:FB PPU:313,124 CYC:14206 $E430:3E 00 06 ROL $0600,X @ 0655 = #$80 A:80 X:55 Y:73 P:E5 SP:FB PPU:328,124 CYC:14211 $E433:BD 00 06 LDA $0600,X @ 0655 = #$01 A:80 X:55 Y:73 P:65 SP:FB PPU: 8,125 CYC:14218 $E436:20 10 FA JSR $FA10 A:01 X:55 Y:73 P:65 SP:FB PPU: 20,125 CYC:14222 $FA10:90 1C BCC $FA2E A:01 X:55 Y:73 P:65 SP:F9 PPU: 38,125 CYC:14228 $FA12:F0 1A BEQ $FA2E A:01 X:55 Y:73 P:65 SP:F9 PPU: 44,125 CYC:14230 $FA14:30 18 BMI $FA2E A:01 X:55 Y:73 P:65 SP:F9 PPU: 50,125 CYC:14232 $FA16:50 16 BVC $FA2E A:01 X:55 Y:73 P:65 SP:F9 PPU: 56,125 CYC:14234 $FA18:C9 01 CMP #$01 A:01 X:55 Y:73 P:65 SP:F9 PPU: 62,125 CYC:14236 $FA1A:D0 12 BNE $FA2E A:01 X:55 Y:73 P:67 SP:F9 PPU: 68,125 CYC:14238 $FA1C:B8 CLV A:01 X:55 Y:73 P:67 SP:F9 PPU: 74,125 CYC:14240 $FA1D:18 CLC A:01 X:55 Y:73 P:nvUbdIZC SP:F9 PPU: 80,125 CYC:14242 $FA1E:A9 55 LDA #$55 A:01 X:55 Y:73 P:nvUbdIZc SP:F9 PPU: 86,125 CYC:14244 $FA20:60 RTS A:55 X:55 Y:73 P:nvUbdIzc SP:F9 PPU: 92,125 CYC:14246 $E439:C8 INY A:55 X:55 Y:73 P:nvUbdIzc SP:FB PPU:110,125 CYC:14252 $E43A:9D 00 06 STA $0600,X @ 0655 = #$01 A:55 X:55 Y:74 P:nvUbdIzc SP:FB PPU:116,125 CYC:14254 $E43D:3E 00 06 ROL $0600,X @ 0655 = #$55 A:55 X:55 Y:74 P:nvUbdIzc SP:FB PPU:131,125 CYC:14259 $E440:BD 00 06 LDA $0600,X @ 0655 = #$AA A:55 X:55 Y:74 P:NvUbdIzc SP:FB PPU:152,125 CYC:14266 $E443:20 21 FA JSR $FA21 A:AA X:55 Y:74 P:NvUbdIzc SP:FB PPU:164,125 CYC:14270 $FA21:B0 0B BCS $FA2E A:AA X:55 Y:74 P:NvUbdIzc SP:F9 PPU:182,125 CYC:14276 $FA23:F0 09 BEQ $FA2E A:AA X:55 Y:74 P:NvUbdIzc SP:F9 PPU:188,125 CYC:14278 $FA25:10 07 BPL $FA2E A:AA X:55 Y:74 P:NvUbdIzc SP:F9 PPU:194,125 CYC:14280 $FA27:70 05 BVS $FA2E A:AA X:55 Y:74 P:NvUbdIzc SP:F9 PPU:200,125 CYC:14282 $FA29:C9 AA CMP #$AA A:AA X:55 Y:74 P:NvUbdIzc SP:F9 PPU:206,125 CYC:14284 $FA2B:D0 01 BNE $FA2E A:AA X:55 Y:74 P:nvUbdIZC SP:F9 PPU:212,125 CYC:14286 $FA2D:60 RTS A:AA X:55 Y:74 P:nvUbdIZC SP:F9 PPU:218,125 CYC:14288 $E446:A9 FF LDA #$FF A:AA X:55 Y:74 P:nvUbdIZC SP:FB PPU:236,125 CYC:14294 $E448:9D 00 06 STA $0600,X @ 0655 = #$AA A:FF X:55 Y:74 P:A5 SP:FB PPU:242,125 CYC:14296 $E44B:85 01 STA $01 = #$FF A:FF X:55 Y:74 P:A5 SP:FB PPU:257,125 CYC:14301 $E44D:24 01 BIT $01 = #$FF A:FF X:55 Y:74 P:A5 SP:FB PPU:266,125 CYC:14304 $E44F:38 SEC A:FF X:55 Y:74 P:E5 SP:FB PPU:275,125 CYC:14307 $E450:FE 00 06 INC $0600,X @ 0655 = #$FF A:FF X:55 Y:74 P:E5 SP:FB PPU:281,125 CYC:14309 $E453:D0 0D BNE $E462 A:FF X:55 Y:74 P:67 SP:FB PPU:302,125 CYC:14316 $E455:30 0B BMI $E462 A:FF X:55 Y:74 P:67 SP:FB PPU:308,125 CYC:14318 $E457:50 09 BVC $E462 A:FF X:55 Y:74 P:67 SP:FB PPU:314,125 CYC:14320 $E459:90 07 BCC $E462 A:FF X:55 Y:74 P:67 SP:FB PPU:320,125 CYC:14322 $E45B:BD 00 06 LDA $0600,X @ 0655 = #$00 A:FF X:55 Y:74 P:67 SP:FB PPU:326,125 CYC:14324 $E45E:C9 00 CMP #$00 A:00 X:55 Y:74 P:67 SP:FB PPU:338,125 CYC:14328 $E460:F0 04 BEQ $E466 A:00 X:55 Y:74 P:67 SP:FB PPU: 3,126 CYC:14330 $E466:A9 7F LDA #$7F A:00 X:55 Y:74 P:67 SP:FB PPU: 12,126 CYC:14333 $E468:9D 00 06 STA $0600,X @ 0655 = #$00 A:7F X:55 Y:74 P:65 SP:FB PPU: 18,126 CYC:14335 $E46B:B8 CLV A:7F X:55 Y:74 P:65 SP:FB PPU: 33,126 CYC:14340 $E46C:18 CLC A:7F X:55 Y:74 P:25 SP:FB PPU: 39,126 CYC:14342 $E46D:FE 00 06 INC $0600,X @ 0655 = #$7F A:7F X:55 Y:74 P:nvUbdIzc SP:FB PPU: 45,126 CYC:14344 $E470:F0 0D BEQ $E47F A:7F X:55 Y:74 P:NvUbdIzc SP:FB PPU: 66,126 CYC:14351 $E472:10 0B BPL $E47F A:7F X:55 Y:74 P:NvUbdIzc SP:FB PPU: 72,126 CYC:14353 $E474:70 09 BVS $E47F A:7F X:55 Y:74 P:NvUbdIzc SP:FB PPU: 78,126 CYC:14355 $E476:B0 07 BCS $E47F A:7F X:55 Y:74 P:NvUbdIzc SP:FB PPU: 84,126 CYC:14357 $E478:BD 00 06 LDA $0600,X @ 0655 = #$80 A:7F X:55 Y:74 P:NvUbdIzc SP:FB PPU: 90,126 CYC:14359 $E47B:C9 80 CMP #$80 A:80 X:55 Y:74 P:NvUbdIzc SP:FB PPU:102,126 CYC:14363 $E47D:F0 04 BEQ $E483 A:80 X:55 Y:74 P:nvUbdIZC SP:FB PPU:108,126 CYC:14365 $E483:A9 00 LDA #$00 A:80 X:55 Y:74 P:nvUbdIZC SP:FB PPU:117,126 CYC:14368 $E485:9D 00 06 STA $0600,X @ 0655 = #$80 A:00 X:55 Y:74 P:nvUbdIZC SP:FB PPU:123,126 CYC:14370 $E488:24 01 BIT $01 = #$FF A:00 X:55 Y:74 P:nvUbdIZC SP:FB PPU:138,126 CYC:14375 $E48A:38 SEC A:00 X:55 Y:74 P:E7 SP:FB PPU:147,126 CYC:14378 $E48B:DE 00 06 DEC $0600,X @ 0655 = #$00 A:00 X:55 Y:74 P:E7 SP:FB PPU:153,126 CYC:14380 $E48E:F0 0D BEQ $E49D A:00 X:55 Y:74 P:E5 SP:FB PPU:174,126 CYC:14387 $E490:10 0B BPL $E49D A:00 X:55 Y:74 P:E5 SP:FB PPU:180,126 CYC:14389 $E492:50 09 BVC $E49D A:00 X:55 Y:74 P:E5 SP:FB PPU:186,126 CYC:14391 $E494:90 07 BCC $E49D A:00 X:55 Y:74 P:E5 SP:FB PPU:192,126 CYC:14393 $E496:BD 00 06 LDA $0600,X @ 0655 = #$FF A:00 X:55 Y:74 P:E5 SP:FB PPU:198,126 CYC:14395 $E499:C9 FF CMP #$FF A:FF X:55 Y:74 P:E5 SP:FB PPU:210,126 CYC:14399 $E49B:F0 04 BEQ $E4A1 A:FF X:55 Y:74 P:67 SP:FB PPU:216,126 CYC:14401 $E4A1:A9 80 LDA #$80 A:FF X:55 Y:74 P:67 SP:FB PPU:225,126 CYC:14404 $E4A3:9D 00 06 STA $0600,X @ 0655 = #$FF A:80 X:55 Y:74 P:E5 SP:FB PPU:231,126 CYC:14406 $E4A6:B8 CLV A:80 X:55 Y:74 P:E5 SP:FB PPU:246,126 CYC:14411 $E4A7:18 CLC A:80 X:55 Y:74 P:A5 SP:FB PPU:252,126 CYC:14413 $E4A8:DE 00 06 DEC $0600,X @ 0655 = #$80 A:80 X:55 Y:74 P:NvUbdIzc SP:FB PPU:258,126 CYC:14415 $E4AB:F0 0D BEQ $E4BA A:80 X:55 Y:74 P:nvUbdIzc SP:FB PPU:279,126 CYC:14422 $E4AD:30 0B BMI $E4BA A:80 X:55 Y:74 P:nvUbdIzc SP:FB PPU:285,126 CYC:14424 $E4AF:70 09 BVS $E4BA A:80 X:55 Y:74 P:nvUbdIzc SP:FB PPU:291,126 CYC:14426 $E4B1:B0 07 BCS $E4BA A:80 X:55 Y:74 P:nvUbdIzc SP:FB PPU:297,126 CYC:14428 $E4B3:BD 00 06 LDA $0600,X @ 0655 = #$7F A:80 X:55 Y:74 P:nvUbdIzc SP:FB PPU:303,126 CYC:14430 $E4B6:C9 7F CMP #$7F A:7F X:55 Y:74 P:nvUbdIzc SP:FB PPU:315,126 CYC:14434 $E4B8:F0 04 BEQ $E4BE A:7F X:55 Y:74 P:nvUbdIZC SP:FB PPU:321,126 CYC:14436 $E4BE:A9 01 LDA #$01 A:7F X:55 Y:74 P:nvUbdIZC SP:FB PPU:330,126 CYC:14439 $E4C0:9D 00 06 STA $0600,X @ 0655 = #$7F A:01 X:55 Y:74 P:25 SP:FB PPU:336,126 CYC:14441 $E4C3:DE 00 06 DEC $0600,X @ 0655 = #$01 A:01 X:55 Y:74 P:25 SP:FB PPU: 10,127 CYC:14446 $E4C6:F0 04 BEQ $E4CC A:01 X:55 Y:74 P:nvUbdIZC SP:FB PPU: 31,127 CYC:14453 $E4CC:A9 33 LDA #$33 A:01 X:55 Y:74 P:nvUbdIZC SP:FB PPU: 40,127 CYC:14456 $E4CE:8D 78 06 STA $0678 = #$7F A:33 X:55 Y:74 P:25 SP:FB PPU: 46,127 CYC:14458 $E4D1:A9 44 LDA #$44 A:33 X:55 Y:74 P:25 SP:FB PPU: 58,127 CYC:14462 $E4D3:A0 78 LDY #$78 A:44 X:55 Y:74 P:25 SP:FB PPU: 64,127 CYC:14464 $E4D5:A2 00 LDX #$00 A:44 X:55 Y:78 P:25 SP:FB PPU: 70,127 CYC:14466 $E4D7:38 SEC A:44 X:00 Y:78 P:nvUbdIZC SP:FB PPU: 76,127 CYC:14468 $E4D8:24 01 BIT $01 = #$FF A:44 X:00 Y:78 P:nvUbdIZC SP:FB PPU: 82,127 CYC:14470 $E4DA:BE 00 06 LDX $0600,Y @ 0678 = #$33 A:44 X:00 Y:78 P:E5 SP:FB PPU: 91,127 CYC:14473 $E4DD:90 12 BCC $E4F1 A:44 X:33 Y:78 P:65 SP:FB PPU:103,127 CYC:14477 $E4DF:50 10 BVC $E4F1 A:44 X:33 Y:78 P:65 SP:FB PPU:109,127 CYC:14479 $E4E1:30 0E BMI $E4F1 A:44 X:33 Y:78 P:65 SP:FB PPU:115,127 CYC:14481 $E4E3:F0 0C BEQ $E4F1 A:44 X:33 Y:78 P:65 SP:FB PPU:121,127 CYC:14483 $E4E5:E0 33 CPX #$33 A:44 X:33 Y:78 P:65 SP:FB PPU:127,127 CYC:14485 $E4E7:D0 08 BNE $E4F1 A:44 X:33 Y:78 P:67 SP:FB PPU:133,127 CYC:14487 $E4E9:C0 78 CPY #$78 A:44 X:33 Y:78 P:67 SP:FB PPU:139,127 CYC:14489 $E4EB:D0 04 BNE $E4F1 A:44 X:33 Y:78 P:67 SP:FB PPU:145,127 CYC:14491 $E4ED:C9 44 CMP #$44 A:44 X:33 Y:78 P:67 SP:FB PPU:151,127 CYC:14493 $E4EF:F0 04 BEQ $E4F5 A:44 X:33 Y:78 P:67 SP:FB PPU:157,127 CYC:14495 $E4F5:A9 97 LDA #$97 A:44 X:33 Y:78 P:67 SP:FB PPU:166,127 CYC:14498 $E4F7:8D 7F 06 STA $067F = #$00 A:97 X:33 Y:78 P:E5 SP:FB PPU:172,127 CYC:14500 $E4FA:A9 47 LDA #$47 A:97 X:33 Y:78 P:E5 SP:FB PPU:184,127 CYC:14504 $E4FC:A0 FF LDY #$FF A:47 X:33 Y:78 P:65 SP:FB PPU:190,127 CYC:14506 $E4FE:A2 00 LDX #$00 A:47 X:33 Y:FF P:E5 SP:FB PPU:196,127 CYC:14508 $E500:18 CLC A:47 X:00 Y:FF P:67 SP:FB PPU:202,127 CYC:14510 $E501:B8 CLV A:47 X:00 Y:FF P:nVUbdIZc SP:FB PPU:208,127 CYC:14512 $E502:BE 80 05 LDX $0580,Y @ 067F = #$97 A:47 X:00 Y:FF P:nvUbdIZc SP:FB PPU:214,127 CYC:14514 $E505:B0 12 BCS $E519 A:47 X:97 Y:FF P:NvUbdIzc SP:FB PPU:229,127 CYC:14519 $E507:70 10 BVS $E519 A:47 X:97 Y:FF P:NvUbdIzc SP:FB PPU:235,127 CYC:14521 $E509:10 0E BPL $E519 A:47 X:97 Y:FF P:NvUbdIzc SP:FB PPU:241,127 CYC:14523 $E50B:F0 0C BEQ $E519 A:47 X:97 Y:FF P:NvUbdIzc SP:FB PPU:247,127 CYC:14525 $E50D:E0 97 CPX #$97 A:47 X:97 Y:FF P:NvUbdIzc SP:FB PPU:253,127 CYC:14527 $E50F:D0 08 BNE $E519 A:47 X:97 Y:FF P:nvUbdIZC SP:FB PPU:259,127 CYC:14529 $E511:C0 FF CPY #$FF A:47 X:97 Y:FF P:nvUbdIZC SP:FB PPU:265,127 CYC:14531 $E513:D0 04 BNE $E519 A:47 X:97 Y:FF P:nvUbdIZC SP:FB PPU:271,127 CYC:14533 $E515:C9 47 CMP #$47 A:47 X:97 Y:FF P:nvUbdIZC SP:FB PPU:277,127 CYC:14535 $E517:F0 04 BEQ $E51D A:47 X:97 Y:FF P:nvUbdIZC SP:FB PPU:283,127 CYC:14537 $E51D:60 RTS A:47 X:97 Y:FF P:nvUbdIZC SP:FB PPU:292,127 CYC:14540 $C62F:20 A3 C6 JSR $C6A3 A:47 X:97 Y:FF P:nvUbdIZC SP:FD PPU:310,127 CYC:14546 $C6A3:A0 4E LDY #$4E A:47 X:97 Y:FF P:nvUbdIZC SP:FB PPU:328,127 CYC:14552 $C6A5:A9 FF LDA #$FF A:47 X:97 Y:4E P:25 SP:FB PPU:334,127 CYC:14554 $C6A7:85 01 STA $01 = #$FF A:FF X:97 Y:4E P:A5 SP:FB PPU:340,127 CYC:14556 $C6A9:20 B0 C6 JSR $C6B0 A:FF X:97 Y:4E P:A5 SP:FB PPU: 8,128 CYC:14559 $C6B0:A9 FF LDA #$FF A:FF X:97 Y:4E P:A5 SP:F9 PPU: 26,128 CYC:14565 $C6B2:48 PHA A:FF X:97 Y:4E P:A5 SP:F9 PPU: 32,128 CYC:14567 $C6B3:A9 AA LDA #$AA A:FF X:97 Y:4E P:A5 SP:F8 PPU: 41,128 CYC:14570 $C6B5:D0 05 BNE $C6BC A:AA X:97 Y:4E P:A5 SP:F8 PPU: 47,128 CYC:14572 $C6BC:28 PLP A:AA X:97 Y:4E P:A5 SP:F8 PPU: 56,128 CYC:14575 $C6BD:04 A9 *NOP $A9 = #$00 A:AA X:97 Y:4E P:EF SP:F9 PPU: 68,128 CYC:14579 $C6BF:44 A9 *NOP $A9 = #$00 A:AA X:97 Y:4E P:EF SP:F9 PPU: 77,128 CYC:14582 $C6C1:64 A9 *NOP $A9 = #$00 A:AA X:97 Y:4E P:EF SP:F9 PPU: 86,128 CYC:14585 $C6C3:EA NOP A:AA X:97 Y:4E P:EF SP:F9 PPU: 95,128 CYC:14588 $C6C4:EA NOP A:AA X:97 Y:4E P:EF SP:F9 PPU:101,128 CYC:14590 $C6C5:EA NOP A:AA X:97 Y:4E P:EF SP:F9 PPU:107,128 CYC:14592 $C6C6:EA NOP A:AA X:97 Y:4E P:EF SP:F9 PPU:113,128 CYC:14594 $C6C7:08 PHP A:AA X:97 Y:4E P:EF SP:F9 PPU:119,128 CYC:14596 $C6C8:48 PHA A:AA X:97 Y:4E P:EF SP:F8 PPU:128,128 CYC:14599 $C6C9:0C A9 A9 *NOP $A9A9 = #$A9 A:AA X:97 Y:4E P:EF SP:F7 PPU:137,128 CYC:14602 $C6CC:EA NOP A:AA X:97 Y:4E P:EF SP:F7 PPU:149,128 CYC:14606 $C6CD:EA NOP A:AA X:97 Y:4E P:EF SP:F7 PPU:155,128 CYC:14608 $C6CE:EA NOP A:AA X:97 Y:4E P:EF SP:F7 PPU:161,128 CYC:14610 $C6CF:EA NOP A:AA X:97 Y:4E P:EF SP:F7 PPU:167,128 CYC:14612 $C6D0:08 PHP A:AA X:97 Y:4E P:EF SP:F7 PPU:173,128 CYC:14614 $C6D1:48 PHA A:AA X:97 Y:4E P:EF SP:F6 PPU:182,128 CYC:14617 $C6D2:14 A9 *NOP $A9,X @ 40 = #$00 A:AA X:97 Y:4E P:EF SP:F5 PPU:191,128 CYC:14620 $C6D4:34 A9 *NOP $A9,X @ 40 = #$00 A:AA X:97 Y:4E P:EF SP:F5 PPU:203,128 CYC:14624 $C6D6:54 A9 *NOP $A9,X @ 40 = #$00 A:AA X:97 Y:4E P:EF SP:F5 PPU:215,128 CYC:14628 $C6D8:74 A9 *NOP $A9,X @ 40 = #$00 A:AA X:97 Y:4E P:EF SP:F5 PPU:227,128 CYC:14632 $C6DA:D4 A9 *NOP $A9,X @ 40 = #$00 A:AA X:97 Y:4E P:EF SP:F5 PPU:239,128 CYC:14636 $C6DC:F4 A9 *NOP $A9,X @ 40 = #$00 A:AA X:97 Y:4E P:EF SP:F5 PPU:251,128 CYC:14640 $C6DE:EA NOP A:AA X:97 Y:4E P:EF SP:F5 PPU:263,128 CYC:14644 $C6DF:EA NOP A:AA X:97 Y:4E P:EF SP:F5 PPU:269,128 CYC:14646 $C6E0:EA NOP A:AA X:97 Y:4E P:EF SP:F5 PPU:275,128 CYC:14648 $C6E1:EA NOP A:AA X:97 Y:4E P:EF SP:F5 PPU:281,128 CYC:14650 $C6E2:08 PHP A:AA X:97 Y:4E P:EF SP:F5 PPU:287,128 CYC:14652 $C6E3:48 PHA A:AA X:97 Y:4E P:EF SP:F4 PPU:296,128 CYC:14655 $C6E4:1A *NOP A:AA X:97 Y:4E P:EF SP:F3 PPU:305,128 CYC:14658 $C6E5:3A *NOP A:AA X:97 Y:4E P:EF SP:F3 PPU:311,128 CYC:14660 $C6E6:5A *NOP A:AA X:97 Y:4E P:EF SP:F3 PPU:317,128 CYC:14662 $C6E7:7A *NOP A:AA X:97 Y:4E P:EF SP:F3 PPU:323,128 CYC:14664 $C6E8:DA *NOP A:AA X:97 Y:4E P:EF SP:F3 PPU:329,128 CYC:14666 $C6E9:FA *NOP A:AA X:97 Y:4E P:EF SP:F3 PPU:335,128 CYC:14668 $C6EA:80 89 *NOP #$89 A:AA X:97 Y:4E P:EF SP:F3 PPU: 0,129 CYC:14670 $C6EC:EA NOP A:AA X:97 Y:4E P:EF SP:F3 PPU: 6,129 CYC:14672 $C6ED:EA NOP A:AA X:97 Y:4E P:EF SP:F3 PPU: 12,129 CYC:14674 $C6EE:EA NOP A:AA X:97 Y:4E P:EF SP:F3 PPU: 18,129 CYC:14676 $C6EF:EA NOP A:AA X:97 Y:4E P:EF SP:F3 PPU: 24,129 CYC:14678 $C6F0:08 PHP A:AA X:97 Y:4E P:EF SP:F3 PPU: 30,129 CYC:14680 $C6F1:48 PHA A:AA X:97 Y:4E P:EF SP:F2 PPU: 39,129 CYC:14683 $C6F2:1C A9 A9 *NOP $A9A9,X @ AA40 = #$00 A:AA X:97 Y:4E P:EF SP:F1 PPU: 48,129 CYC:14686 $C6F5:3C A9 A9 *NOP $A9A9,X @ AA40 = #$00 A:AA X:97 Y:4E P:EF SP:F1 PPU: 63,129 CYC:14691 $C6F8:5C A9 A9 *NOP $A9A9,X @ AA40 = #$00 A:AA X:97 Y:4E P:EF SP:F1 PPU: 78,129 CYC:14696 $C6FB:7C A9 A9 *NOP $A9A9,X @ AA40 = #$00 A:AA X:97 Y:4E P:EF SP:F1 PPU: 93,129 CYC:14701 $C6FE:DC A9 A9 *NOP $A9A9,X @ AA40 = #$00 A:AA X:97 Y:4E P:EF SP:F1 PPU:108,129 CYC:14706 $C701:FC A9 A9 *NOP $A9A9,X @ AA40 = #$00 A:AA X:97 Y:4E P:EF SP:F1 PPU:123,129 CYC:14711 $C704:EA NOP A:AA X:97 Y:4E P:EF SP:F1 PPU:138,129 CYC:14716 $C705:EA NOP A:AA X:97 Y:4E P:EF SP:F1 PPU:144,129 CYC:14718 $C706:EA NOP A:AA X:97 Y:4E P:EF SP:F1 PPU:150,129 CYC:14720 $C707:EA NOP A:AA X:97 Y:4E P:EF SP:F1 PPU:156,129 CYC:14722 $C708:08 PHP A:AA X:97 Y:4E P:EF SP:F1 PPU:162,129 CYC:14724 $C709:48 PHA A:AA X:97 Y:4E P:EF SP:F0 PPU:171,129 CYC:14727 $C70A:A2 05 LDX #$05 A:AA X:97 Y:4E P:EF SP:EF PPU:180,129 CYC:14730 $C70C:68 PLA A:AA X:05 Y:4E P:6D SP:EF PPU:186,129 CYC:14732 $C70D:C9 55 CMP #$55 A:AA X:05 Y:4E P:ED SP:F0 PPU:198,129 CYC:14736 $C70F:F0 0A BEQ $C71B A:AA X:05 Y:4E P:6D SP:F0 PPU:204,129 CYC:14738 $C711:C9 AA CMP #$AA A:AA X:05 Y:4E P:6D SP:F0 PPU:210,129 CYC:14740 $C713:F0 06 BEQ $C71B A:AA X:05 Y:4E P:6F SP:F0 PPU:216,129 CYC:14742 $C71B:68 PLA A:AA X:05 Y:4E P:6F SP:F0 PPU:225,129 CYC:14745 $C71C:29 CB AND #$CB A:FF X:05 Y:4E P:ED SP:F1 PPU:237,129 CYC:14749 $C71E:C9 00 CMP #$00 A:CB X:05 Y:4E P:ED SP:F1 PPU:243,129 CYC:14751 $C720:F0 06 BEQ $C728 A:CB X:05 Y:4E P:ED SP:F1 PPU:249,129 CYC:14753 $C722:C9 CB CMP #$CB A:CB X:05 Y:4E P:ED SP:F1 PPU:255,129 CYC:14755 $C724:F0 02 BEQ $C728 A:CB X:05 Y:4E P:6F SP:F1 PPU:261,129 CYC:14757 $C728:C8 INY A:CB X:05 Y:4E P:6F SP:F1 PPU:270,129 CYC:14760 $C729:CA DEX A:CB X:05 Y:4F P:6D SP:F1 PPU:276,129 CYC:14762 $C72A:D0 E0 BNE $C70C A:CB X:04 Y:4F P:6D SP:F1 PPU:282,129 CYC:14764 $C70C:68 PLA A:CB X:04 Y:4F P:6D SP:F1 PPU:291,129 CYC:14767 $C70D:C9 55 CMP #$55 A:AA X:04 Y:4F P:ED SP:F2 PPU:303,129 CYC:14771 $C70F:F0 0A BEQ $C71B A:AA X:04 Y:4F P:6D SP:F2 PPU:309,129 CYC:14773 $C711:C9 AA CMP #$AA A:AA X:04 Y:4F P:6D SP:F2 PPU:315,129 CYC:14775 $C713:F0 06 BEQ $C71B A:AA X:04 Y:4F P:6F SP:F2 PPU:321,129 CYC:14777 $C71B:68 PLA A:AA X:04 Y:4F P:6F SP:F2 PPU:330,129 CYC:14780 $C71C:29 CB AND #$CB A:FF X:04 Y:4F P:ED SP:F3 PPU: 1,130 CYC:14784 $C71E:C9 00 CMP #$00 A:CB X:04 Y:4F P:ED SP:F3 PPU: 7,130 CYC:14786 $C720:F0 06 BEQ $C728 A:CB X:04 Y:4F P:ED SP:F3 PPU: 13,130 CYC:14788 $C722:C9 CB CMP #$CB A:CB X:04 Y:4F P:ED SP:F3 PPU: 19,130 CYC:14790 $C724:F0 02 BEQ $C728 A:CB X:04 Y:4F P:6F SP:F3 PPU: 25,130 CYC:14792 $C728:C8 INY A:CB X:04 Y:4F P:6F SP:F3 PPU: 34,130 CYC:14795 $C729:CA DEX A:CB X:04 Y:50 P:6D SP:F3 PPU: 40,130 CYC:14797 $C72A:D0 E0 BNE $C70C A:CB X:03 Y:50 P:6D SP:F3 PPU: 46,130 CYC:14799 $C70C:68 PLA A:CB X:03 Y:50 P:6D SP:F3 PPU: 55,130 CYC:14802 $C70D:C9 55 CMP #$55 A:AA X:03 Y:50 P:ED SP:F4 PPU: 67,130 CYC:14806 $C70F:F0 0A BEQ $C71B A:AA X:03 Y:50 P:6D SP:F4 PPU: 73,130 CYC:14808 $C711:C9 AA CMP #$AA A:AA X:03 Y:50 P:6D SP:F4 PPU: 79,130 CYC:14810 $C713:F0 06 BEQ $C71B A:AA X:03 Y:50 P:6F SP:F4 PPU: 85,130 CYC:14812 $C71B:68 PLA A:AA X:03 Y:50 P:6F SP:F4 PPU: 94,130 CYC:14815 $C71C:29 CB AND #$CB A:FF X:03 Y:50 P:ED SP:F5 PPU:106,130 CYC:14819 $C71E:C9 00 CMP #$00 A:CB X:03 Y:50 P:ED SP:F5 PPU:112,130 CYC:14821 $C720:F0 06 BEQ $C728 A:CB X:03 Y:50 P:ED SP:F5 PPU:118,130 CYC:14823 $C722:C9 CB CMP #$CB A:CB X:03 Y:50 P:ED SP:F5 PPU:124,130 CYC:14825 $C724:F0 02 BEQ $C728 A:CB X:03 Y:50 P:6F SP:F5 PPU:130,130 CYC:14827 $C728:C8 INY A:CB X:03 Y:50 P:6F SP:F5 PPU:139,130 CYC:14830 $C729:CA DEX A:CB X:03 Y:51 P:6D SP:F5 PPU:145,130 CYC:14832 $C72A:D0 E0 BNE $C70C A:CB X:02 Y:51 P:6D SP:F5 PPU:151,130 CYC:14834 $C70C:68 PLA A:CB X:02 Y:51 P:6D SP:F5 PPU:160,130 CYC:14837 $C70D:C9 55 CMP #$55 A:AA X:02 Y:51 P:ED SP:F6 PPU:172,130 CYC:14841 $C70F:F0 0A BEQ $C71B A:AA X:02 Y:51 P:6D SP:F6 PPU:178,130 CYC:14843 $C711:C9 AA CMP #$AA A:AA X:02 Y:51 P:6D SP:F6 PPU:184,130 CYC:14845 $C713:F0 06 BEQ $C71B A:AA X:02 Y:51 P:6F SP:F6 PPU:190,130 CYC:14847 $C71B:68 PLA A:AA X:02 Y:51 P:6F SP:F6 PPU:199,130 CYC:14850 $C71C:29 CB AND #$CB A:FF X:02 Y:51 P:ED SP:F7 PPU:211,130 CYC:14854 $C71E:C9 00 CMP #$00 A:CB X:02 Y:51 P:ED SP:F7 PPU:217,130 CYC:14856 $C720:F0 06 BEQ $C728 A:CB X:02 Y:51 P:ED SP:F7 PPU:223,130 CYC:14858 $C722:C9 CB CMP #$CB A:CB X:02 Y:51 P:ED SP:F7 PPU:229,130 CYC:14860 $C724:F0 02 BEQ $C728 A:CB X:02 Y:51 P:6F SP:F7 PPU:235,130 CYC:14862 $C728:C8 INY A:CB X:02 Y:51 P:6F SP:F7 PPU:244,130 CYC:14865 $C729:CA DEX A:CB X:02 Y:52 P:6D SP:F7 PPU:250,130 CYC:14867 $C72A:D0 E0 BNE $C70C A:CB X:01 Y:52 P:6D SP:F7 PPU:256,130 CYC:14869 $C70C:68 PLA A:CB X:01 Y:52 P:6D SP:F7 PPU:265,130 CYC:14872 $C70D:C9 55 CMP #$55 A:AA X:01 Y:52 P:ED SP:F8 PPU:277,130 CYC:14876 $C70F:F0 0A BEQ $C71B A:AA X:01 Y:52 P:6D SP:F8 PPU:283,130 CYC:14878 $C711:C9 AA CMP #$AA A:AA X:01 Y:52 P:6D SP:F8 PPU:289,130 CYC:14880 $C713:F0 06 BEQ $C71B A:AA X:01 Y:52 P:6F SP:F8 PPU:295,130 CYC:14882 $C71B:68 PLA A:AA X:01 Y:52 P:6F SP:F8 PPU:304,130 CYC:14885 $C71C:29 CB AND #$CB A:FF X:01 Y:52 P:ED SP:F9 PPU:316,130 CYC:14889 $C71E:C9 00 CMP #$00 A:CB X:01 Y:52 P:ED SP:F9 PPU:322,130 CYC:14891 $C720:F0 06 BEQ $C728 A:CB X:01 Y:52 P:ED SP:F9 PPU:328,130 CYC:14893 $C722:C9 CB CMP #$CB A:CB X:01 Y:52 P:ED SP:F9 PPU:334,130 CYC:14895 $C724:F0 02 BEQ $C728 A:CB X:01 Y:52 P:6F SP:F9 PPU:340,130 CYC:14897 $C728:C8 INY A:CB X:01 Y:52 P:6F SP:F9 PPU: 8,131 CYC:14900 $C729:CA DEX A:CB X:01 Y:53 P:6D SP:F9 PPU: 14,131 CYC:14902 $C72A:D0 E0 BNE $C70C A:CB X:00 Y:53 P:6F SP:F9 PPU: 20,131 CYC:14904 $C72C:60 RTS A:CB X:00 Y:53 P:6F SP:F9 PPU: 26,131 CYC:14906 $C6AC:20 B7 C6 JSR $C6B7 A:CB X:00 Y:53 P:6F SP:FB PPU: 44,131 CYC:14912 $C6B7:A9 34 LDA #$34 A:CB X:00 Y:53 P:6F SP:F9 PPU: 62,131 CYC:14918 $C6B9:48 PHA A:34 X:00 Y:53 P:6D SP:F9 PPU: 68,131 CYC:14920 $C6BA:A9 55 LDA #$55 A:34 X:00 Y:53 P:6D SP:F8 PPU: 77,131 CYC:14923 $C6BC:28 PLP A:55 X:00 Y:53 P:6D SP:F8 PPU: 83,131 CYC:14925 $C6BD:04 A9 *NOP $A9 = #$00 A:55 X:00 Y:53 P:nvUbdIzc SP:F9 PPU: 95,131 CYC:14929 $C6BF:44 A9 *NOP $A9 = #$00 A:55 X:00 Y:53 P:nvUbdIzc SP:F9 PPU:104,131 CYC:14932 $C6C1:64 A9 *NOP $A9 = #$00 A:55 X:00 Y:53 P:nvUbdIzc SP:F9 PPU:113,131 CYC:14935 $C6C3:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F9 PPU:122,131 CYC:14938 $C6C4:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F9 PPU:128,131 CYC:14940 $C6C5:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F9 PPU:134,131 CYC:14942 $C6C6:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F9 PPU:140,131 CYC:14944 $C6C7:08 PHP A:55 X:00 Y:53 P:nvUbdIzc SP:F9 PPU:146,131 CYC:14946 $C6C8:48 PHA A:55 X:00 Y:53 P:nvUbdIzc SP:F8 PPU:155,131 CYC:14949 $C6C9:0C A9 A9 *NOP $A9A9 = #$A9 A:55 X:00 Y:53 P:nvUbdIzc SP:F7 PPU:164,131 CYC:14952 $C6CC:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F7 PPU:176,131 CYC:14956 $C6CD:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F7 PPU:182,131 CYC:14958 $C6CE:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F7 PPU:188,131 CYC:14960 $C6CF:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F7 PPU:194,131 CYC:14962 $C6D0:08 PHP A:55 X:00 Y:53 P:nvUbdIzc SP:F7 PPU:200,131 CYC:14964 $C6D1:48 PHA A:55 X:00 Y:53 P:nvUbdIzc SP:F6 PPU:209,131 CYC:14967 $C6D2:14 A9 *NOP $A9,X @ A9 = #$00 A:55 X:00 Y:53 P:nvUbdIzc SP:F5 PPU:218,131 CYC:14970 $C6D4:34 A9 *NOP $A9,X @ A9 = #$00 A:55 X:00 Y:53 P:nvUbdIzc SP:F5 PPU:230,131 CYC:14974 $C6D6:54 A9 *NOP $A9,X @ A9 = #$00 A:55 X:00 Y:53 P:nvUbdIzc SP:F5 PPU:242,131 CYC:14978 $C6D8:74 A9 *NOP $A9,X @ A9 = #$00 A:55 X:00 Y:53 P:nvUbdIzc SP:F5 PPU:254,131 CYC:14982 $C6DA:D4 A9 *NOP $A9,X @ A9 = #$00 A:55 X:00 Y:53 P:nvUbdIzc SP:F5 PPU:266,131 CYC:14986 $C6DC:F4 A9 *NOP $A9,X @ A9 = #$00 A:55 X:00 Y:53 P:nvUbdIzc SP:F5 PPU:278,131 CYC:14990 $C6DE:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F5 PPU:290,131 CYC:14994 $C6DF:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F5 PPU:296,131 CYC:14996 $C6E0:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F5 PPU:302,131 CYC:14998 $C6E1:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F5 PPU:308,131 CYC:15000 $C6E2:08 PHP A:55 X:00 Y:53 P:nvUbdIzc SP:F5 PPU:314,131 CYC:15002 $C6E3:48 PHA A:55 X:00 Y:53 P:nvUbdIzc SP:F4 PPU:323,131 CYC:15005 $C6E4:1A *NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F3 PPU:332,131 CYC:15008 $C6E5:3A *NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F3 PPU:338,131 CYC:15010 $C6E6:5A *NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F3 PPU: 3,132 CYC:15012 $C6E7:7A *NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F3 PPU: 9,132 CYC:15014 $C6E8:DA *NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F3 PPU: 15,132 CYC:15016 $C6E9:FA *NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F3 PPU: 21,132 CYC:15018 $C6EA:80 89 *NOP #$89 A:55 X:00 Y:53 P:nvUbdIzc SP:F3 PPU: 27,132 CYC:15020 $C6EC:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F3 PPU: 33,132 CYC:15022 $C6ED:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F3 PPU: 39,132 CYC:15024 $C6EE:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F3 PPU: 45,132 CYC:15026 $C6EF:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F3 PPU: 51,132 CYC:15028 $C6F0:08 PHP A:55 X:00 Y:53 P:nvUbdIzc SP:F3 PPU: 57,132 CYC:15030 $C6F1:48 PHA A:55 X:00 Y:53 P:nvUbdIzc SP:F2 PPU: 66,132 CYC:15033 $C6F2:1C A9 A9 *NOP $A9A9,X @ A9A9 = #$A9 A:55 X:00 Y:53 P:nvUbdIzc SP:F1 PPU: 75,132 CYC:15036 $C6F5:3C A9 A9 *NOP $A9A9,X @ A9A9 = #$A9 A:55 X:00 Y:53 P:nvUbdIzc SP:F1 PPU: 87,132 CYC:15040 $C6F8:5C A9 A9 *NOP $A9A9,X @ A9A9 = #$A9 A:55 X:00 Y:53 P:nvUbdIzc SP:F1 PPU: 99,132 CYC:15044 $C6FB:7C A9 A9 *NOP $A9A9,X @ A9A9 = #$A9 A:55 X:00 Y:53 P:nvUbdIzc SP:F1 PPU:111,132 CYC:15048 $C6FE:DC A9 A9 *NOP $A9A9,X @ A9A9 = #$A9 A:55 X:00 Y:53 P:nvUbdIzc SP:F1 PPU:123,132 CYC:15052 $C701:FC A9 A9 *NOP $A9A9,X @ A9A9 = #$A9 A:55 X:00 Y:53 P:nvUbdIzc SP:F1 PPU:135,132 CYC:15056 $C704:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F1 PPU:147,132 CYC:15060 $C705:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F1 PPU:153,132 CYC:15062 $C706:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F1 PPU:159,132 CYC:15064 $C707:EA NOP A:55 X:00 Y:53 P:nvUbdIzc SP:F1 PPU:165,132 CYC:15066 $C708:08 PHP A:55 X:00 Y:53 P:nvUbdIzc SP:F1 PPU:171,132 CYC:15068 $C709:48 PHA A:55 X:00 Y:53 P:nvUbdIzc SP:F0 PPU:180,132 CYC:15071 $C70A:A2 05 LDX #$05 A:55 X:00 Y:53 P:nvUbdIzc SP:EF PPU:189,132 CYC:15074 $C70C:68 PLA A:55 X:05 Y:53 P:nvUbdIzc SP:EF PPU:195,132 CYC:15076 $C70D:C9 55 CMP #$55 A:55 X:05 Y:53 P:nvUbdIzc SP:F0 PPU:207,132 CYC:15080 $C70F:F0 0A BEQ $C71B A:55 X:05 Y:53 P:nvUbdIZC SP:F0 PPU:213,132 CYC:15082 $C71B:68 PLA A:55 X:05 Y:53 P:nvUbdIZC SP:F0 PPU:222,132 CYC:15085 $C71C:29 CB AND #$CB A:34 X:05 Y:53 P:25 SP:F1 PPU:234,132 CYC:15089 $C71E:C9 00 CMP #$00 A:00 X:05 Y:53 P:nvUbdIZC SP:F1 PPU:240,132 CYC:15091 $C720:F0 06 BEQ $C728 A:00 X:05 Y:53 P:nvUbdIZC SP:F1 PPU:246,132 CYC:15093 $C728:C8 INY A:00 X:05 Y:53 P:nvUbdIZC SP:F1 PPU:255,132 CYC:15096 $C729:CA DEX A:00 X:05 Y:54 P:25 SP:F1 PPU:261,132 CYC:15098 $C72A:D0 E0 BNE $C70C A:00 X:04 Y:54 P:25 SP:F1 PPU:267,132 CYC:15100 $C70C:68 PLA A:00 X:04 Y:54 P:25 SP:F1 PPU:276,132 CYC:15103 $C70D:C9 55 CMP #$55 A:55 X:04 Y:54 P:25 SP:F2 PPU:288,132 CYC:15107 $C70F:F0 0A BEQ $C71B A:55 X:04 Y:54 P:nvUbdIZC SP:F2 PPU:294,132 CYC:15109 $C71B:68 PLA A:55 X:04 Y:54 P:nvUbdIZC SP:F2 PPU:303,132 CYC:15112 $C71C:29 CB AND #$CB A:34 X:04 Y:54 P:25 SP:F3 PPU:315,132 CYC:15116 $C71E:C9 00 CMP #$00 A:00 X:04 Y:54 P:nvUbdIZC SP:F3 PPU:321,132 CYC:15118 $C720:F0 06 BEQ $C728 A:00 X:04 Y:54 P:nvUbdIZC SP:F3 PPU:327,132 CYC:15120 $C728:C8 INY A:00 X:04 Y:54 P:nvUbdIZC SP:F3 PPU:336,132 CYC:15123 $C729:CA DEX A:00 X:04 Y:55 P:25 SP:F3 PPU: 1,133 CYC:15125 $C72A:D0 E0 BNE $C70C A:00 X:03 Y:55 P:25 SP:F3 PPU: 7,133 CYC:15127 $C70C:68 PLA A:00 X:03 Y:55 P:25 SP:F3 PPU: 16,133 CYC:15130 $C70D:C9 55 CMP #$55 A:55 X:03 Y:55 P:25 SP:F4 PPU: 28,133 CYC:15134 $C70F:F0 0A BEQ $C71B A:55 X:03 Y:55 P:nvUbdIZC SP:F4 PPU: 34,133 CYC:15136 $C71B:68 PLA A:55 X:03 Y:55 P:nvUbdIZC SP:F4 PPU: 43,133 CYC:15139 $C71C:29 CB AND #$CB A:34 X:03 Y:55 P:25 SP:F5 PPU: 55,133 CYC:15143 $C71E:C9 00 CMP #$00 A:00 X:03 Y:55 P:nvUbdIZC SP:F5 PPU: 61,133 CYC:15145 $C720:F0 06 BEQ $C728 A:00 X:03 Y:55 P:nvUbdIZC SP:F5 PPU: 67,133 CYC:15147 $C728:C8 INY A:00 X:03 Y:55 P:nvUbdIZC SP:F5 PPU: 76,133 CYC:15150 $C729:CA DEX A:00 X:03 Y:56 P:25 SP:F5 PPU: 82,133 CYC:15152 $C72A:D0 E0 BNE $C70C A:00 X:02 Y:56 P:25 SP:F5 PPU: 88,133 CYC:15154 $C70C:68 PLA A:00 X:02 Y:56 P:25 SP:F5 PPU: 97,133 CYC:15157 $C70D:C9 55 CMP #$55 A:55 X:02 Y:56 P:25 SP:F6 PPU:109,133 CYC:15161 $C70F:F0 0A BEQ $C71B A:55 X:02 Y:56 P:nvUbdIZC SP:F6 PPU:115,133 CYC:15163 $C71B:68 PLA A:55 X:02 Y:56 P:nvUbdIZC SP:F6 PPU:124,133 CYC:15166 $C71C:29 CB AND #$CB A:34 X:02 Y:56 P:25 SP:F7 PPU:136,133 CYC:15170 $C71E:C9 00 CMP #$00 A:00 X:02 Y:56 P:nvUbdIZC SP:F7 PPU:142,133 CYC:15172 $C720:F0 06 BEQ $C728 A:00 X:02 Y:56 P:nvUbdIZC SP:F7 PPU:148,133 CYC:15174 $C728:C8 INY A:00 X:02 Y:56 P:nvUbdIZC SP:F7 PPU:157,133 CYC:15177 $C729:CA DEX A:00 X:02 Y:57 P:25 SP:F7 PPU:163,133 CYC:15179 $C72A:D0 E0 BNE $C70C A:00 X:01 Y:57 P:25 SP:F7 PPU:169,133 CYC:15181 $C70C:68 PLA A:00 X:01 Y:57 P:25 SP:F7 PPU:178,133 CYC:15184 $C70D:C9 55 CMP #$55 A:55 X:01 Y:57 P:25 SP:F8 PPU:190,133 CYC:15188 $C70F:F0 0A BEQ $C71B A:55 X:01 Y:57 P:nvUbdIZC SP:F8 PPU:196,133 CYC:15190 $C71B:68 PLA A:55 X:01 Y:57 P:nvUbdIZC SP:F8 PPU:205,133 CYC:15193 $C71C:29 CB AND #$CB A:34 X:01 Y:57 P:25 SP:F9 PPU:217,133 CYC:15197 $C71E:C9 00 CMP #$00 A:00 X:01 Y:57 P:nvUbdIZC SP:F9 PPU:223,133 CYC:15199 $C720:F0 06 BEQ $C728 A:00 X:01 Y:57 P:nvUbdIZC SP:F9 PPU:229,133 CYC:15201 $C728:C8 INY A:00 X:01 Y:57 P:nvUbdIZC SP:F9 PPU:238,133 CYC:15204 $C729:CA DEX A:00 X:01 Y:58 P:25 SP:F9 PPU:244,133 CYC:15206 $C72A:D0 E0 BNE $C70C A:00 X:00 Y:58 P:nvUbdIZC SP:F9 PPU:250,133 CYC:15208 $C72C:60 RTS A:00 X:00 Y:58 P:nvUbdIZC SP:F9 PPU:256,133 CYC:15210 $C6AF:60 RTS A:00 X:00 Y:58 P:nvUbdIZC SP:FB PPU:274,133 CYC:15216 $C632:20 1E E5 JSR $E51E A:00 X:00 Y:58 P:nvUbdIZC SP:FD PPU:292,133 CYC:15222 $E51E:A9 55 LDA #$55 A:00 X:00 Y:58 P:nvUbdIZC SP:FB PPU:310,133 CYC:15228 $E520:8D 80 05 STA $0580 = #$00 A:55 X:00 Y:58 P:25 SP:FB PPU:316,133 CYC:15230 $E523:A9 AA LDA #$AA A:55 X:00 Y:58 P:25 SP:FB PPU:328,133 CYC:15234 $E525:8D 32 04 STA $0432 = #$00 A:AA X:00 Y:58 P:A5 SP:FB PPU:334,133 CYC:15236 $E528:A9 80 LDA #$80 A:AA X:00 Y:58 P:A5 SP:FB PPU: 5,134 CYC:15240 $E52A:85 43 STA $43 = #$00 A:80 X:00 Y:58 P:A5 SP:FB PPU: 11,134 CYC:15242 $E52C:A9 05 LDA #$05 A:80 X:00 Y:58 P:A5 SP:FB PPU: 20,134 CYC:15245 $E52E:85 44 STA $44 = #$00 A:05 X:00 Y:58 P:25 SP:FB PPU: 26,134 CYC:15247 $E530:A9 32 LDA #$32 A:05 X:00 Y:58 P:25 SP:FB PPU: 35,134 CYC:15250 $E532:85 45 STA $45 = #$00 A:32 X:00 Y:58 P:25 SP:FB PPU: 41,134 CYC:15252 $E534:A9 04 LDA #$04 A:32 X:00 Y:58 P:25 SP:FB PPU: 50,134 CYC:15255 $E536:85 46 STA $46 = #$00 A:04 X:00 Y:58 P:25 SP:FB PPU: 56,134 CYC:15257 $E538:A2 03 LDX #$03 A:04 X:00 Y:58 P:25 SP:FB PPU: 65,134 CYC:15260 $E53A:A0 77 LDY #$77 A:04 X:03 Y:58 P:25 SP:FB PPU: 71,134 CYC:15262 $E53C:A9 FF LDA #$FF A:04 X:03 Y:77 P:25 SP:FB PPU: 77,134 CYC:15264 $E53E:85 01 STA $01 = #$FF A:FF X:03 Y:77 P:A5 SP:FB PPU: 83,134 CYC:15266 $E540:24 01 BIT $01 = #$FF A:FF X:03 Y:77 P:A5 SP:FB PPU: 92,134 CYC:15269 $E542:38 SEC A:FF X:03 Y:77 P:E5 SP:FB PPU:101,134 CYC:15272 $E543:A9 00 LDA #$00 A:FF X:03 Y:77 P:E5 SP:FB PPU:107,134 CYC:15274 $E545:A3 40 *LAX ($40,X) @ 43 = #$0580 = 55 A:00 X:03 Y:77 P:67 SP:FB PPU:113,134 CYC:15276 $E547:EA NOP A:55 X:55 Y:77 P:65 SP:FB PPU:131,134 CYC:15282 $E548:EA NOP A:55 X:55 Y:77 P:65 SP:FB PPU:137,134 CYC:15284 $E549:EA NOP A:55 X:55 Y:77 P:65 SP:FB PPU:143,134 CYC:15286 $E54A:EA NOP A:55 X:55 Y:77 P:65 SP:FB PPU:149,134 CYC:15288 $E54B:F0 12 BEQ $E55F A:55 X:55 Y:77 P:65 SP:FB PPU:155,134 CYC:15290 $E54D:30 10 BMI $E55F A:55 X:55 Y:77 P:65 SP:FB PPU:161,134 CYC:15292 $E54F:50 0E BVC $E55F A:55 X:55 Y:77 P:65 SP:FB PPU:167,134 CYC:15294 $E551:90 0C BCC $E55F A:55 X:55 Y:77 P:65 SP:FB PPU:173,134 CYC:15296 $E553:C9 55 CMP #$55 A:55 X:55 Y:77 P:65 SP:FB PPU:179,134 CYC:15298 $E555:D0 08 BNE $E55F A:55 X:55 Y:77 P:67 SP:FB PPU:185,134 CYC:15300 $E557:E0 55 CPX #$55 A:55 X:55 Y:77 P:67 SP:FB PPU:191,134 CYC:15302 $E559:D0 04 BNE $E55F A:55 X:55 Y:77 P:67 SP:FB PPU:197,134 CYC:15304 $E55B:C0 77 CPY #$77 A:55 X:55 Y:77 P:67 SP:FB PPU:203,134 CYC:15306 $E55D:F0 04 BEQ $E563 A:55 X:55 Y:77 P:67 SP:FB PPU:209,134 CYC:15308 $E563:A2 05 LDX #$05 A:55 X:55 Y:77 P:67 SP:FB PPU:218,134 CYC:15311 $E565:A0 33 LDY #$33 A:55 X:05 Y:77 P:65 SP:FB PPU:224,134 CYC:15313 $E567:B8 CLV A:55 X:05 Y:33 P:65 SP:FB PPU:230,134 CYC:15315 $E568:18 CLC A:55 X:05 Y:33 P:25 SP:FB PPU:236,134 CYC:15317 $E569:A9 00 LDA #$00 A:55 X:05 Y:33 P:nvUbdIzc SP:FB PPU:242,134 CYC:15319 $E56B:A3 40 *LAX ($40,X) @ 45 = #$0432 = AA A:00 X:05 Y:33 P:nvUbdIZc SP:FB PPU:248,134 CYC:15321 $E56D:EA NOP A:AA X:AA Y:33 P:NvUbdIzc SP:FB PPU:266,134 CYC:15327 $E56E:EA NOP A:AA X:AA Y:33 P:NvUbdIzc SP:FB PPU:272,134 CYC:15329 $E56F:EA NOP A:AA X:AA Y:33 P:NvUbdIzc SP:FB PPU:278,134 CYC:15331 $E570:EA NOP A:AA X:AA Y:33 P:NvUbdIzc SP:FB PPU:284,134 CYC:15333 $E571:F0 12 BEQ $E585 A:AA X:AA Y:33 P:NvUbdIzc SP:FB PPU:290,134 CYC:15335 $E573:10 10 BPL $E585 A:AA X:AA Y:33 P:NvUbdIzc SP:FB PPU:296,134 CYC:15337 $E575:70 0E BVS $E585 A:AA X:AA Y:33 P:NvUbdIzc SP:FB PPU:302,134 CYC:15339 $E577:B0 0C BCS $E585 A:AA X:AA Y:33 P:NvUbdIzc SP:FB PPU:308,134 CYC:15341 $E579:C9 AA CMP #$AA A:AA X:AA Y:33 P:NvUbdIzc SP:FB PPU:314,134 CYC:15343 $E57B:D0 08 BNE $E585 A:AA X:AA Y:33 P:nvUbdIZC SP:FB PPU:320,134 CYC:15345 $E57D:E0 AA CPX #$AA A:AA X:AA Y:33 P:nvUbdIZC SP:FB PPU:326,134 CYC:15347 $E57F:D0 04 BNE $E585 A:AA X:AA Y:33 P:nvUbdIZC SP:FB PPU:332,134 CYC:15349 $E581:C0 33 CPY #$33 A:AA X:AA Y:33 P:nvUbdIZC SP:FB PPU:338,134 CYC:15351 $E583:F0 04 BEQ $E589 A:AA X:AA Y:33 P:nvUbdIZC SP:FB PPU: 3,135 CYC:15353 $E589:A9 87 LDA #$87 A:AA X:AA Y:33 P:nvUbdIZC SP:FB PPU: 12,135 CYC:15356 $E58B:85 67 STA $67 = #$00 A:87 X:AA Y:33 P:A5 SP:FB PPU: 18,135 CYC:15358 $E58D:A9 32 LDA #$32 A:87 X:AA Y:33 P:A5 SP:FB PPU: 27,135 CYC:15361 $E58F:85 68 STA $68 = #$00 A:32 X:AA Y:33 P:25 SP:FB PPU: 33,135 CYC:15363 $E591:A0 57 LDY #$57 A:32 X:AA Y:33 P:25 SP:FB PPU: 42,135 CYC:15366 $E593:24 01 BIT $01 = #$FF A:32 X:AA Y:57 P:25 SP:FB PPU: 48,135 CYC:15368 $E595:38 SEC A:32 X:AA Y:57 P:E5 SP:FB PPU: 57,135 CYC:15371 $E596:A9 00 LDA #$00 A:32 X:AA Y:57 P:E5 SP:FB PPU: 63,135 CYC:15373 $E598:A7 67 *LAX $67 = #$87 A:00 X:AA Y:57 P:67 SP:FB PPU: 69,135 CYC:15375 $E59A:EA NOP A:87 X:87 Y:57 P:E5 SP:FB PPU: 78,135 CYC:15378 $E59B:EA NOP A:87 X:87 Y:57 P:E5 SP:FB PPU: 84,135 CYC:15380 $E59C:EA NOP A:87 X:87 Y:57 P:E5 SP:FB PPU: 90,135 CYC:15382 $E59D:EA NOP A:87 X:87 Y:57 P:E5 SP:FB PPU: 96,135 CYC:15384 $E59E:F0 12 BEQ $E5B2 A:87 X:87 Y:57 P:E5 SP:FB PPU:102,135 CYC:15386 $E5A0:10 10 BPL $E5B2 A:87 X:87 Y:57 P:E5 SP:FB PPU:108,135 CYC:15388 $E5A2:50 0E BVC $E5B2 A:87 X:87 Y:57 P:E5 SP:FB PPU:114,135 CYC:15390 $E5A4:90 0C BCC $E5B2 A:87 X:87 Y:57 P:E5 SP:FB PPU:120,135 CYC:15392 $E5A6:C9 87 CMP #$87 A:87 X:87 Y:57 P:E5 SP:FB PPU:126,135 CYC:15394 $E5A8:D0 08 BNE $E5B2 A:87 X:87 Y:57 P:67 SP:FB PPU:132,135 CYC:15396 $E5AA:E0 87 CPX #$87 A:87 X:87 Y:57 P:67 SP:FB PPU:138,135 CYC:15398 $E5AC:D0 04 BNE $E5B2 A:87 X:87 Y:57 P:67 SP:FB PPU:144,135 CYC:15400 $E5AE:C0 57 CPY #$57 A:87 X:87 Y:57 P:67 SP:FB PPU:150,135 CYC:15402 $E5B0:F0 04 BEQ $E5B6 A:87 X:87 Y:57 P:67 SP:FB PPU:156,135 CYC:15404 $E5B6:A0 53 LDY #$53 A:87 X:87 Y:57 P:67 SP:FB PPU:165,135 CYC:15407 $E5B8:B8 CLV A:87 X:87 Y:53 P:65 SP:FB PPU:171,135 CYC:15409 $E5B9:18 CLC A:87 X:87 Y:53 P:25 SP:FB PPU:177,135 CYC:15411 $E5BA:A9 00 LDA #$00 A:87 X:87 Y:53 P:nvUbdIzc SP:FB PPU:183,135 CYC:15413 $E5BC:A7 68 *LAX $68 = #$32 A:00 X:87 Y:53 P:nvUbdIZc SP:FB PPU:189,135 CYC:15415 $E5BE:EA NOP A:32 X:32 Y:53 P:nvUbdIzc SP:FB PPU:198,135 CYC:15418 $E5BF:EA NOP A:32 X:32 Y:53 P:nvUbdIzc SP:FB PPU:204,135 CYC:15420 $E5C0:EA NOP A:32 X:32 Y:53 P:nvUbdIzc SP:FB PPU:210,135 CYC:15422 $E5C1:EA NOP A:32 X:32 Y:53 P:nvUbdIzc SP:FB PPU:216,135 CYC:15424 $E5C2:F0 12 BEQ $E5D6 A:32 X:32 Y:53 P:nvUbdIzc SP:FB PPU:222,135 CYC:15426 $E5C4:30 10 BMI $E5D6 A:32 X:32 Y:53 P:nvUbdIzc SP:FB PPU:228,135 CYC:15428 $E5C6:70 0E BVS $E5D6 A:32 X:32 Y:53 P:nvUbdIzc SP:FB PPU:234,135 CYC:15430 $E5C8:B0 0C BCS $E5D6 A:32 X:32 Y:53 P:nvUbdIzc SP:FB PPU:240,135 CYC:15432 $E5CA:C9 32 CMP #$32 A:32 X:32 Y:53 P:nvUbdIzc SP:FB PPU:246,135 CYC:15434 $E5CC:D0 08 BNE $E5D6 A:32 X:32 Y:53 P:nvUbdIZC SP:FB PPU:252,135 CYC:15436 $E5CE:E0 32 CPX #$32 A:32 X:32 Y:53 P:nvUbdIZC SP:FB PPU:258,135 CYC:15438 $E5D0:D0 04 BNE $E5D6 A:32 X:32 Y:53 P:nvUbdIZC SP:FB PPU:264,135 CYC:15440 $E5D2:C0 53 CPY #$53 A:32 X:32 Y:53 P:nvUbdIZC SP:FB PPU:270,135 CYC:15442 $E5D4:F0 04 BEQ $E5DA A:32 X:32 Y:53 P:nvUbdIZC SP:FB PPU:276,135 CYC:15444 $E5DA:A9 87 LDA #$87 A:32 X:32 Y:53 P:nvUbdIZC SP:FB PPU:285,135 CYC:15447 $E5DC:8D 77 05 STA $0577 = #$00 A:87 X:32 Y:53 P:A5 SP:FB PPU:291,135 CYC:15449 $E5DF:A9 32 LDA #$32 A:87 X:32 Y:53 P:A5 SP:FB PPU:303,135 CYC:15453 $E5E1:8D 78 05 STA $0578 = #$00 A:32 X:32 Y:53 P:25 SP:FB PPU:309,135 CYC:15455 $E5E4:A0 57 LDY #$57 A:32 X:32 Y:53 P:25 SP:FB PPU:321,135 CYC:15459 $E5E6:24 01 BIT $01 = #$FF A:32 X:32 Y:57 P:25 SP:FB PPU:327,135 CYC:15461 $E5E8:38 SEC A:32 X:32 Y:57 P:E5 SP:FB PPU:336,135 CYC:15464 $E5E9:A9 00 LDA #$00 A:32 X:32 Y:57 P:E5 SP:FB PPU: 1,136 CYC:15466 $E5EB:AF 77 05 *LAX $0577 = #$87 A:00 X:32 Y:57 P:67 SP:FB PPU: 7,136 CYC:15468 $E5EE:EA NOP A:87 X:87 Y:57 P:E5 SP:FB PPU: 19,136 CYC:15472 $E5EF:EA NOP A:87 X:87 Y:57 P:E5 SP:FB PPU: 25,136 CYC:15474 $E5F0:EA NOP A:87 X:87 Y:57 P:E5 SP:FB PPU: 31,136 CYC:15476 $E5F1:EA NOP A:87 X:87 Y:57 P:E5 SP:FB PPU: 37,136 CYC:15478 $E5F2:F0 12 BEQ $E606 A:87 X:87 Y:57 P:E5 SP:FB PPU: 43,136 CYC:15480 $E5F4:10 10 BPL $E606 A:87 X:87 Y:57 P:E5 SP:FB PPU: 49,136 CYC:15482 $E5F6:50 0E BVC $E606 A:87 X:87 Y:57 P:E5 SP:FB PPU: 55,136 CYC:15484 $E5F8:90 0C BCC $E606 A:87 X:87 Y:57 P:E5 SP:FB PPU: 61,136 CYC:15486 $E5FA:C9 87 CMP #$87 A:87 X:87 Y:57 P:E5 SP:FB PPU: 67,136 CYC:15488 $E5FC:D0 08 BNE $E606 A:87 X:87 Y:57 P:67 SP:FB PPU: 73,136 CYC:15490 $E5FE:E0 87 CPX #$87 A:87 X:87 Y:57 P:67 SP:FB PPU: 79,136 CYC:15492 $E600:D0 04 BNE $E606 A:87 X:87 Y:57 P:67 SP:FB PPU: 85,136 CYC:15494 $E602:C0 57 CPY #$57 A:87 X:87 Y:57 P:67 SP:FB PPU: 91,136 CYC:15496 $E604:F0 04 BEQ $E60A A:87 X:87 Y:57 P:67 SP:FB PPU: 97,136 CYC:15498 $E60A:A0 53 LDY #$53 A:87 X:87 Y:57 P:67 SP:FB PPU:106,136 CYC:15501 $E60C:B8 CLV A:87 X:87 Y:53 P:65 SP:FB PPU:112,136 CYC:15503 $E60D:18 CLC A:87 X:87 Y:53 P:25 SP:FB PPU:118,136 CYC:15505 $E60E:A9 00 LDA #$00 A:87 X:87 Y:53 P:nvUbdIzc SP:FB PPU:124,136 CYC:15507 $E610:AF 78 05 *LAX $0578 = #$32 A:00 X:87 Y:53 P:nvUbdIZc SP:FB PPU:130,136 CYC:15509 $E613:EA NOP A:32 X:32 Y:53 P:nvUbdIzc SP:FB PPU:142,136 CYC:15513 $E614:EA NOP A:32 X:32 Y:53 P:nvUbdIzc SP:FB PPU:148,136 CYC:15515 $E615:EA NOP A:32 X:32 Y:53 P:nvUbdIzc SP:FB PPU:154,136 CYC:15517 $E616:EA NOP A:32 X:32 Y:53 P:nvUbdIzc SP:FB PPU:160,136 CYC:15519 $E617:F0 12 BEQ $E62B A:32 X:32 Y:53 P:nvUbdIzc SP:FB PPU:166,136 CYC:15521 $E619:30 10 BMI $E62B A:32 X:32 Y:53 P:nvUbdIzc SP:FB PPU:172,136 CYC:15523 $E61B:70 0E BVS $E62B A:32 X:32 Y:53 P:nvUbdIzc SP:FB PPU:178,136 CYC:15525 $E61D:B0 0C BCS $E62B A:32 X:32 Y:53 P:nvUbdIzc SP:FB PPU:184,136 CYC:15527 $E61F:C9 32 CMP #$32 A:32 X:32 Y:53 P:nvUbdIzc SP:FB PPU:190,136 CYC:15529 $E621:D0 08 BNE $E62B A:32 X:32 Y:53 P:nvUbdIZC SP:FB PPU:196,136 CYC:15531 $E623:E0 32 CPX #$32 A:32 X:32 Y:53 P:nvUbdIZC SP:FB PPU:202,136 CYC:15533 $E625:D0 04 BNE $E62B A:32 X:32 Y:53 P:nvUbdIZC SP:FB PPU:208,136 CYC:15535 $E627:C0 53 CPY #$53 A:32 X:32 Y:53 P:nvUbdIZC SP:FB PPU:214,136 CYC:15537 $E629:F0 04 BEQ $E62F A:32 X:32 Y:53 P:nvUbdIZC SP:FB PPU:220,136 CYC:15539 $E62F:A9 FF LDA #$FF A:32 X:32 Y:53 P:nvUbdIZC SP:FB PPU:229,136 CYC:15542 $E631:85 43 STA $43 = #$80 A:FF X:32 Y:53 P:A5 SP:FB PPU:235,136 CYC:15544 $E633:A9 04 LDA #$04 A:FF X:32 Y:53 P:A5 SP:FB PPU:244,136 CYC:15547 $E635:85 44 STA $44 = #$05 A:04 X:32 Y:53 P:25 SP:FB PPU:250,136 CYC:15549 $E637:A9 32 LDA #$32 A:04 X:32 Y:53 P:25 SP:FB PPU:259,136 CYC:15552 $E639:85 45 STA $45 = #$32 A:32 X:32 Y:53 P:25 SP:FB PPU:265,136 CYC:15554 $E63B:A9 04 LDA #$04 A:32 X:32 Y:53 P:25 SP:FB PPU:274,136 CYC:15557 $E63D:85 46 STA $46 = #$04 A:04 X:32 Y:53 P:25 SP:FB PPU:280,136 CYC:15559 $E63F:A9 55 LDA #$55 A:04 X:32 Y:53 P:25 SP:FB PPU:289,136 CYC:15562 $E641:8D 80 05 STA $0580 = #$55 A:55 X:32 Y:53 P:25 SP:FB PPU:295,136 CYC:15564 $E644:A9 AA LDA #$AA A:55 X:32 Y:53 P:25 SP:FB PPU:307,136 CYC:15568 $E646:8D 32 04 STA $0432 = #$AA A:AA X:32 Y:53 P:A5 SP:FB PPU:313,136 CYC:15570 $E649:A2 03 LDX #$03 A:AA X:32 Y:53 P:A5 SP:FB PPU:325,136 CYC:15574 $E64B:A0 81 LDY #$81 A:AA X:03 Y:53 P:25 SP:FB PPU:331,136 CYC:15576 $E64D:24 01 BIT $01 = #$FF A:AA X:03 Y:81 P:A5 SP:FB PPU:337,136 CYC:15578 $E64F:38 SEC A:AA X:03 Y:81 P:E5 SP:FB PPU: 5,137 CYC:15581 $E650:A9 00 LDA #$00 A:AA X:03 Y:81 P:E5 SP:FB PPU: 11,137 CYC:15583 $E652:B3 43 *LAX ($43),Y = #$04FF @ 0580 = 55 A:00 X:03 Y:81 P:67 SP:FB PPU: 17,137 CYC:15585 $E654:EA NOP A:55 X:55 Y:81 P:65 SP:FB PPU: 35,137 CYC:15591 $E655:EA NOP A:55 X:55 Y:81 P:65 SP:FB PPU: 41,137 CYC:15593 $E656:EA NOP A:55 X:55 Y:81 P:65 SP:FB PPU: 47,137 CYC:15595 $E657:EA NOP A:55 X:55 Y:81 P:65 SP:FB PPU: 53,137 CYC:15597 $E658:F0 12 BEQ $E66C A:55 X:55 Y:81 P:65 SP:FB PPU: 59,137 CYC:15599 $E65A:30 10 BMI $E66C A:55 X:55 Y:81 P:65 SP:FB PPU: 65,137 CYC:15601 $E65C:50 0E BVC $E66C A:55 X:55 Y:81 P:65 SP:FB PPU: 71,137 CYC:15603 $E65E:90 0C BCC $E66C A:55 X:55 Y:81 P:65 SP:FB PPU: 77,137 CYC:15605 $E660:C9 55 CMP #$55 A:55 X:55 Y:81 P:65 SP:FB PPU: 83,137 CYC:15607 $E662:D0 08 BNE $E66C A:55 X:55 Y:81 P:67 SP:FB PPU: 89,137 CYC:15609 $E664:E0 55 CPX #$55 A:55 X:55 Y:81 P:67 SP:FB PPU: 95,137 CYC:15611 $E666:D0 04 BNE $E66C A:55 X:55 Y:81 P:67 SP:FB PPU:101,137 CYC:15613 $E668:C0 81 CPY #$81 A:55 X:55 Y:81 P:67 SP:FB PPU:107,137 CYC:15615 $E66A:F0 04 BEQ $E670 A:55 X:55 Y:81 P:67 SP:FB PPU:113,137 CYC:15617 $E670:A2 05 LDX #$05 A:55 X:55 Y:81 P:67 SP:FB PPU:122,137 CYC:15620 $E672:A0 00 LDY #$00 A:55 X:05 Y:81 P:65 SP:FB PPU:128,137 CYC:15622 $E674:B8 CLV A:55 X:05 Y:00 P:67 SP:FB PPU:134,137 CYC:15624 $E675:18 CLC A:55 X:05 Y:00 P:nvUbdIZC SP:FB PPU:140,137 CYC:15626 $E676:A9 00 LDA #$00 A:55 X:05 Y:00 P:nvUbdIZc SP:FB PPU:146,137 CYC:15628 $E678:B3 45 *LAX ($45),Y = #$0432 @ 0432 = AA A:00 X:05 Y:00 P:nvUbdIZc SP:FB PPU:152,137 CYC:15630 $E67A:EA NOP A:AA X:AA Y:00 P:NvUbdIzc SP:FB PPU:167,137 CYC:15635 $E67B:EA NOP A:AA X:AA Y:00 P:NvUbdIzc SP:FB PPU:173,137 CYC:15637 $E67C:EA NOP A:AA X:AA Y:00 P:NvUbdIzc SP:FB PPU:179,137 CYC:15639 $E67D:EA NOP A:AA X:AA Y:00 P:NvUbdIzc SP:FB PPU:185,137 CYC:15641 $E67E:F0 12 BEQ $E692 A:AA X:AA Y:00 P:NvUbdIzc SP:FB PPU:191,137 CYC:15643 $E680:10 10 BPL $E692 A:AA X:AA Y:00 P:NvUbdIzc SP:FB PPU:197,137 CYC:15645 $E682:70 0E BVS $E692 A:AA X:AA Y:00 P:NvUbdIzc SP:FB PPU:203,137 CYC:15647 $E684:B0 0C BCS $E692 A:AA X:AA Y:00 P:NvUbdIzc SP:FB PPU:209,137 CYC:15649 $E686:C9 AA CMP #$AA A:AA X:AA Y:00 P:NvUbdIzc SP:FB PPU:215,137 CYC:15651 $E688:D0 08 BNE $E692 A:AA X:AA Y:00 P:nvUbdIZC SP:FB PPU:221,137 CYC:15653 $E68A:E0 AA CPX #$AA A:AA X:AA Y:00 P:nvUbdIZC SP:FB PPU:227,137 CYC:15655 $E68C:D0 04 BNE $E692 A:AA X:AA Y:00 P:nvUbdIZC SP:FB PPU:233,137 CYC:15657 $E68E:C0 00 CPY #$00 A:AA X:AA Y:00 P:nvUbdIZC SP:FB PPU:239,137 CYC:15659 $E690:F0 04 BEQ $E696 A:AA X:AA Y:00 P:nvUbdIZC SP:FB PPU:245,137 CYC:15661 $E696:A9 87 LDA #$87 A:AA X:AA Y:00 P:nvUbdIZC SP:FB PPU:254,137 CYC:15664 $E698:85 67 STA $67 = #$87 A:87 X:AA Y:00 P:A5 SP:FB PPU:260,137 CYC:15666 $E69A:A9 32 LDA #$32 A:87 X:AA Y:00 P:A5 SP:FB PPU:269,137 CYC:15669 $E69C:85 68 STA $68 = #$32 A:32 X:AA Y:00 P:25 SP:FB PPU:275,137 CYC:15671 $E69E:A0 57 LDY #$57 A:32 X:AA Y:00 P:25 SP:FB PPU:284,137 CYC:15674 $E6A0:24 01 BIT $01 = #$FF A:32 X:AA Y:57 P:25 SP:FB PPU:290,137 CYC:15676 $E6A2:38 SEC A:32 X:AA Y:57 P:E5 SP:FB PPU:299,137 CYC:15679 $E6A3:A9 00 LDA #$00 A:32 X:AA Y:57 P:E5 SP:FB PPU:305,137 CYC:15681 $E6A5:B7 10 *LAX $10,Y @ 67 = #$87 A:00 X:AA Y:57 P:67 SP:FB PPU:311,137 CYC:15683 $E6A7:EA NOP A:87 X:87 Y:57 P:E5 SP:FB PPU:323,137 CYC:15687 $E6A8:EA NOP A:87 X:87 Y:57 P:E5 SP:FB PPU:329,137 CYC:15689 $E6A9:EA NOP A:87 X:87 Y:57 P:E5 SP:FB PPU:335,137 CYC:15691 $E6AA:EA NOP A:87 X:87 Y:57 P:E5 SP:FB PPU: 0,138 CYC:15693 $E6AB:F0 12 BEQ $E6BF A:87 X:87 Y:57 P:E5 SP:FB PPU: 6,138 CYC:15695 $E6AD:10 10 BPL $E6BF A:87 X:87 Y:57 P:E5 SP:FB PPU: 12,138 CYC:15697 $E6AF:50 0E BVC $E6BF A:87 X:87 Y:57 P:E5 SP:FB PPU: 18,138 CYC:15699 $E6B1:90 0C BCC $E6BF A:87 X:87 Y:57 P:E5 SP:FB PPU: 24,138 CYC:15701 $E6B3:C9 87 CMP #$87 A:87 X:87 Y:57 P:E5 SP:FB PPU: 30,138 CYC:15703 $E6B5:D0 08 BNE $E6BF A:87 X:87 Y:57 P:67 SP:FB PPU: 36,138 CYC:15705 $E6B7:E0 87 CPX #$87 A:87 X:87 Y:57 P:67 SP:FB PPU: 42,138 CYC:15707 $E6B9:D0 04 BNE $E6BF A:87 X:87 Y:57 P:67 SP:FB PPU: 48,138 CYC:15709 $E6BB:C0 57 CPY #$57 A:87 X:87 Y:57 P:67 SP:FB PPU: 54,138 CYC:15711 $E6BD:F0 04 BEQ $E6C3 A:87 X:87 Y:57 P:67 SP:FB PPU: 60,138 CYC:15713 $E6C3:A0 FF LDY #$FF A:87 X:87 Y:57 P:67 SP:FB PPU: 69,138 CYC:15716 $E6C5:B8 CLV A:87 X:87 Y:FF P:E5 SP:FB PPU: 75,138 CYC:15718 $E6C6:18 CLC A:87 X:87 Y:FF P:A5 SP:FB PPU: 81,138 CYC:15720 $E6C7:A9 00 LDA #$00 A:87 X:87 Y:FF P:NvUbdIzc SP:FB PPU: 87,138 CYC:15722 $E6C9:B7 69 *LAX $69,Y @ 68 = #$32 A:00 X:87 Y:FF P:nvUbdIZc SP:FB PPU: 93,138 CYC:15724 $E6CB:EA NOP A:32 X:32 Y:FF P:nvUbdIzc SP:FB PPU:105,138 CYC:15728 $E6CC:EA NOP A:32 X:32 Y:FF P:nvUbdIzc SP:FB PPU:111,138 CYC:15730 $E6CD:EA NOP A:32 X:32 Y:FF P:nvUbdIzc SP:FB PPU:117,138 CYC:15732 $E6CE:EA NOP A:32 X:32 Y:FF P:nvUbdIzc SP:FB PPU:123,138 CYC:15734 $E6CF:F0 12 BEQ $E6E3 A:32 X:32 Y:FF P:nvUbdIzc SP:FB PPU:129,138 CYC:15736 $E6D1:30 10 BMI $E6E3 A:32 X:32 Y:FF P:nvUbdIzc SP:FB PPU:135,138 CYC:15738 $E6D3:70 0E BVS $E6E3 A:32 X:32 Y:FF P:nvUbdIzc SP:FB PPU:141,138 CYC:15740 $E6D5:B0 0C BCS $E6E3 A:32 X:32 Y:FF P:nvUbdIzc SP:FB PPU:147,138 CYC:15742 $E6D7:C9 32 CMP #$32 A:32 X:32 Y:FF P:nvUbdIzc SP:FB PPU:153,138 CYC:15744 $E6D9:D0 08 BNE $E6E3 A:32 X:32 Y:FF P:nvUbdIZC SP:FB PPU:159,138 CYC:15746 $E6DB:E0 32 CPX #$32 A:32 X:32 Y:FF P:nvUbdIZC SP:FB PPU:165,138 CYC:15748 $E6DD:D0 04 BNE $E6E3 A:32 X:32 Y:FF P:nvUbdIZC SP:FB PPU:171,138 CYC:15750 $E6DF:C0 FF CPY #$FF A:32 X:32 Y:FF P:nvUbdIZC SP:FB PPU:177,138 CYC:15752 $E6E1:F0 04 BEQ $E6E7 A:32 X:32 Y:FF P:nvUbdIZC SP:FB PPU:183,138 CYC:15754 $E6E7:A9 87 LDA #$87 A:32 X:32 Y:FF P:nvUbdIZC SP:FB PPU:192,138 CYC:15757 $E6E9:8D 87 05 STA $0587 = #$00 A:87 X:32 Y:FF P:A5 SP:FB PPU:198,138 CYC:15759 $E6EC:A9 32 LDA #$32 A:87 X:32 Y:FF P:A5 SP:FB PPU:210,138 CYC:15763 $E6EE:8D 88 05 STA $0588 = #$00 A:32 X:32 Y:FF P:25 SP:FB PPU:216,138 CYC:15765 $E6F1:A0 30 LDY #$30 A:32 X:32 Y:FF P:25 SP:FB PPU:228,138 CYC:15769 $E6F3:24 01 BIT $01 = #$FF A:32 X:32 Y:30 P:25 SP:FB PPU:234,138 CYC:15771 $E6F5:38 SEC A:32 X:32 Y:30 P:E5 SP:FB PPU:243,138 CYC:15774 $E6F6:A9 00 LDA #$00 A:32 X:32 Y:30 P:E5 SP:FB PPU:249,138 CYC:15776 $E6F8:BF 57 05 *LAX $0557,Y @ 0587 = #$87 A:00 X:32 Y:30 P:67 SP:FB PPU:255,138 CYC:15778 $E6FB:EA NOP A:87 X:87 Y:30 P:E5 SP:FB PPU:267,138 CYC:15782 $E6FC:EA NOP A:87 X:87 Y:30 P:E5 SP:FB PPU:273,138 CYC:15784 $E6FD:EA NOP A:87 X:87 Y:30 P:E5 SP:FB PPU:279,138 CYC:15786 $E6FE:EA NOP A:87 X:87 Y:30 P:E5 SP:FB PPU:285,138 CYC:15788 $E6FF:F0 12 BEQ $E713 A:87 X:87 Y:30 P:E5 SP:FB PPU:291,138 CYC:15790 $E701:10 10 BPL $E713 A:87 X:87 Y:30 P:E5 SP:FB PPU:297,138 CYC:15792 $E703:50 0E BVC $E713 A:87 X:87 Y:30 P:E5 SP:FB PPU:303,138 CYC:15794 $E705:90 0C BCC $E713 A:87 X:87 Y:30 P:E5 SP:FB PPU:309,138 CYC:15796 $E707:C9 87 CMP #$87 A:87 X:87 Y:30 P:E5 SP:FB PPU:315,138 CYC:15798 $E709:D0 08 BNE $E713 A:87 X:87 Y:30 P:67 SP:FB PPU:321,138 CYC:15800 $E70B:E0 87 CPX #$87 A:87 X:87 Y:30 P:67 SP:FB PPU:327,138 CYC:15802 $E70D:D0 04 BNE $E713 A:87 X:87 Y:30 P:67 SP:FB PPU:333,138 CYC:15804 $E70F:C0 30 CPY #$30 A:87 X:87 Y:30 P:67 SP:FB PPU:339,138 CYC:15806 $E711:F0 04 BEQ $E717 A:87 X:87 Y:30 P:67 SP:FB PPU: 4,139 CYC:15808 $E717:A0 40 LDY #$40 A:87 X:87 Y:30 P:67 SP:FB PPU: 13,139 CYC:15811 $E719:B8 CLV A:87 X:87 Y:40 P:65 SP:FB PPU: 19,139 CYC:15813 $E71A:18 CLC A:87 X:87 Y:40 P:25 SP:FB PPU: 25,139 CYC:15815 $E71B:A9 00 LDA #$00 A:87 X:87 Y:40 P:nvUbdIzc SP:FB PPU: 31,139 CYC:15817 $E71D:BF 48 05 *LAX $0548,Y @ 0588 = #$32 A:00 X:87 Y:40 P:nvUbdIZc SP:FB PPU: 37,139 CYC:15819 $E720:EA NOP A:32 X:32 Y:40 P:nvUbdIzc SP:FB PPU: 49,139 CYC:15823 $E721:EA NOP A:32 X:32 Y:40 P:nvUbdIzc SP:FB PPU: 55,139 CYC:15825 $E722:EA NOP A:32 X:32 Y:40 P:nvUbdIzc SP:FB PPU: 61,139 CYC:15827 $E723:EA NOP A:32 X:32 Y:40 P:nvUbdIzc SP:FB PPU: 67,139 CYC:15829 $E724:F0 12 BEQ $E738 A:32 X:32 Y:40 P:nvUbdIzc SP:FB PPU: 73,139 CYC:15831 $E726:30 10 BMI $E738 A:32 X:32 Y:40 P:nvUbdIzc SP:FB PPU: 79,139 CYC:15833 $E728:70 0E BVS $E738 A:32 X:32 Y:40 P:nvUbdIzc SP:FB PPU: 85,139 CYC:15835 $E72A:B0 0C BCS $E738 A:32 X:32 Y:40 P:nvUbdIzc SP:FB PPU: 91,139 CYC:15837 $E72C:C9 32 CMP #$32 A:32 X:32 Y:40 P:nvUbdIzc SP:FB PPU: 97,139 CYC:15839 $E72E:D0 08 BNE $E738 A:32 X:32 Y:40 P:nvUbdIZC SP:FB PPU:103,139 CYC:15841 $E730:E0 32 CPX #$32 A:32 X:32 Y:40 P:nvUbdIZC SP:FB PPU:109,139 CYC:15843 $E732:D0 04 BNE $E738 A:32 X:32 Y:40 P:nvUbdIZC SP:FB PPU:115,139 CYC:15845 $E734:C0 40 CPY #$40 A:32 X:32 Y:40 P:nvUbdIZC SP:FB PPU:121,139 CYC:15847 $E736:F0 04 BEQ $E73C A:32 X:32 Y:40 P:nvUbdIZC SP:FB PPU:127,139 CYC:15849 $E73C:60 RTS A:32 X:32 Y:40 P:nvUbdIZC SP:FB PPU:136,139 CYC:15852 $C635:20 3D E7 JSR $E73D A:32 X:32 Y:40 P:nvUbdIZC SP:FD PPU:154,139 CYC:15858 $E73D:A9 C0 LDA #$C0 A:32 X:32 Y:40 P:nvUbdIZC SP:FB PPU:172,139 CYC:15864 $E73F:85 01 STA $01 = #$FF A:C0 X:32 Y:40 P:A5 SP:FB PPU:178,139 CYC:15866 $E741:A9 00 LDA #$00 A:C0 X:32 Y:40 P:A5 SP:FB PPU:187,139 CYC:15869 $E743:8D 89 04 STA $0489 = #$00 A:00 X:32 Y:40 P:nvUbdIZC SP:FB PPU:193,139 CYC:15871 $E746:A9 89 LDA #$89 A:00 X:32 Y:40 P:nvUbdIZC SP:FB PPU:205,139 CYC:15875 $E748:85 60 STA $60 = #$00 A:89 X:32 Y:40 P:A5 SP:FB PPU:211,139 CYC:15877 $E74A:A9 04 LDA #$04 A:89 X:32 Y:40 P:A5 SP:FB PPU:220,139 CYC:15880 $E74C:85 61 STA $61 = #$00 A:04 X:32 Y:40 P:25 SP:FB PPU:226,139 CYC:15882 $E74E:A0 44 LDY #$44 A:04 X:32 Y:40 P:25 SP:FB PPU:235,139 CYC:15885 $E750:A2 17 LDX #$17 A:04 X:32 Y:44 P:25 SP:FB PPU:241,139 CYC:15887 $E752:A9 3E LDA #$3E A:04 X:17 Y:44 P:25 SP:FB PPU:247,139 CYC:15889 $E754:24 01 BIT $01 = #$C0 A:3E X:17 Y:44 P:25 SP:FB PPU:253,139 CYC:15891 $E756:18 CLC A:3E X:17 Y:44 P:E7 SP:FB PPU:262,139 CYC:15894 $E757:83 49 *SAX ($49,X) @ 60 = #$0489 = 00 A:3E X:17 Y:44 P:E6 SP:FB PPU:268,139 CYC:15896 $E759:EA NOP A:3E X:17 Y:44 P:E6 SP:FB PPU:286,139 CYC:15902 $E75A:EA NOP A:3E X:17 Y:44 P:E6 SP:FB PPU:292,139 CYC:15904 $E75B:EA NOP A:3E X:17 Y:44 P:E6 SP:FB PPU:298,139 CYC:15906 $E75C:EA NOP A:3E X:17 Y:44 P:E6 SP:FB PPU:304,139 CYC:15908 $E75D:D0 19 BNE $E778 A:3E X:17 Y:44 P:E6 SP:FB PPU:310,139 CYC:15910 $E75F:B0 17 BCS $E778 A:3E X:17 Y:44 P:E6 SP:FB PPU:316,139 CYC:15912 $E761:50 15 BVC $E778 A:3E X:17 Y:44 P:E6 SP:FB PPU:322,139 CYC:15914 $E763:10 13 BPL $E778 A:3E X:17 Y:44 P:E6 SP:FB PPU:328,139 CYC:15916 $E765:C9 3E CMP #$3E A:3E X:17 Y:44 P:E6 SP:FB PPU:334,139 CYC:15918 $E767:D0 0F BNE $E778 A:3E X:17 Y:44 P:67 SP:FB PPU:340,139 CYC:15920 $E769:C0 44 CPY #$44 A:3E X:17 Y:44 P:67 SP:FB PPU: 5,140 CYC:15922 $E76B:D0 0B BNE $E778 A:3E X:17 Y:44 P:67 SP:FB PPU: 11,140 CYC:15924 $E76D:E0 17 CPX #$17 A:3E X:17 Y:44 P:67 SP:FB PPU: 17,140 CYC:15926 $E76F:D0 07 BNE $E778 A:3E X:17 Y:44 P:67 SP:FB PPU: 23,140 CYC:15928 $E771:AD 89 04 LDA $0489 = #$16 A:3E X:17 Y:44 P:67 SP:FB PPU: 29,140 CYC:15930 $E774:C9 16 CMP #$16 A:16 X:17 Y:44 P:65 SP:FB PPU: 41,140 CYC:15934 $E776:F0 04 BEQ $E77C A:16 X:17 Y:44 P:67 SP:FB PPU: 47,140 CYC:15936 $E77C:A0 44 LDY #$44 A:16 X:17 Y:44 P:67 SP:FB PPU: 56,140 CYC:15939 $E77E:A2 7A LDX #$7A A:16 X:17 Y:44 P:65 SP:FB PPU: 62,140 CYC:15941 $E780:A9 66 LDA #$66 A:16 X:7A Y:44 P:65 SP:FB PPU: 68,140 CYC:15943 $E782:38 SEC A:66 X:7A Y:44 P:65 SP:FB PPU: 74,140 CYC:15945 $E783:B8 CLV A:66 X:7A Y:44 P:65 SP:FB PPU: 80,140 CYC:15947 $E784:83 E6 *SAX ($E6,X) @ 60 = #$0489 = 16 A:66 X:7A Y:44 P:25 SP:FB PPU: 86,140 CYC:15949 $E786:EA NOP A:66 X:7A Y:44 P:25 SP:FB PPU:104,140 CYC:15955 $E787:EA NOP A:66 X:7A Y:44 P:25 SP:FB PPU:110,140 CYC:15957 $E788:EA NOP A:66 X:7A Y:44 P:25 SP:FB PPU:116,140 CYC:15959 $E789:EA NOP A:66 X:7A Y:44 P:25 SP:FB PPU:122,140 CYC:15961 $E78A:F0 19 BEQ $E7A5 A:66 X:7A Y:44 P:25 SP:FB PPU:128,140 CYC:15963 $E78C:90 17 BCC $E7A5 A:66 X:7A Y:44 P:25 SP:FB PPU:134,140 CYC:15965 $E78E:70 15 BVS $E7A5 A:66 X:7A Y:44 P:25 SP:FB PPU:140,140 CYC:15967 $E790:30 13 BMI $E7A5 A:66 X:7A Y:44 P:25 SP:FB PPU:146,140 CYC:15969 $E792:C9 66 CMP #$66 A:66 X:7A Y:44 P:25 SP:FB PPU:152,140 CYC:15971 $E794:D0 0F BNE $E7A5 A:66 X:7A Y:44 P:nvUbdIZC SP:FB PPU:158,140 CYC:15973 $E796:C0 44 CPY #$44 A:66 X:7A Y:44 P:nvUbdIZC SP:FB PPU:164,140 CYC:15975 $E798:D0 0B BNE $E7A5 A:66 X:7A Y:44 P:nvUbdIZC SP:FB PPU:170,140 CYC:15977 $E79A:E0 7A CPX #$7A A:66 X:7A Y:44 P:nvUbdIZC SP:FB PPU:176,140 CYC:15979 $E79C:D0 07 BNE $E7A5 A:66 X:7A Y:44 P:nvUbdIZC SP:FB PPU:182,140 CYC:15981 $E79E:AD 89 04 LDA $0489 = #$62 A:66 X:7A Y:44 P:nvUbdIZC SP:FB PPU:188,140 CYC:15983 $E7A1:C9 62 CMP #$62 A:62 X:7A Y:44 P:25 SP:FB PPU:200,140 CYC:15987 $E7A3:F0 04 BEQ $E7A9 A:62 X:7A Y:44 P:nvUbdIZC SP:FB PPU:206,140 CYC:15989 $E7A9:A9 FF LDA #$FF A:62 X:7A Y:44 P:nvUbdIZC SP:FB PPU:215,140 CYC:15992 $E7AB:85 49 STA $49 = #$00 A:FF X:7A Y:44 P:A5 SP:FB PPU:221,140 CYC:15994 $E7AD:A0 44 LDY #$44 A:FF X:7A Y:44 P:A5 SP:FB PPU:230,140 CYC:15997 $E7AF:A2 AA LDX #$AA A:FF X:7A Y:44 P:25 SP:FB PPU:236,140 CYC:15999 $E7B1:A9 55 LDA #$55 A:FF X:AA Y:44 P:A5 SP:FB PPU:242,140 CYC:16001 $E7B3:24 01 BIT $01 = #$C0 A:55 X:AA Y:44 P:25 SP:FB PPU:248,140 CYC:16003 $E7B5:18 CLC A:55 X:AA Y:44 P:E5 SP:FB PPU:257,140 CYC:16006 $E7B6:87 49 *SAX $49 = #$FF A:55 X:AA Y:44 P:NVUbdIzc SP:FB PPU:263,140 CYC:16008 $E7B8:EA NOP A:55 X:AA Y:44 P:NVUbdIzc SP:FB PPU:272,140 CYC:16011 $E7B9:EA NOP A:55 X:AA Y:44 P:NVUbdIzc SP:FB PPU:278,140 CYC:16013 $E7BA:EA NOP A:55 X:AA Y:44 P:NVUbdIzc SP:FB PPU:284,140 CYC:16015 $E7BB:EA NOP A:55 X:AA Y:44 P:NVUbdIzc SP:FB PPU:290,140 CYC:16017 $E7BC:F0 18 BEQ $E7D6 A:55 X:AA Y:44 P:NVUbdIzc SP:FB PPU:296,140 CYC:16019 $E7BE:B0 16 BCS $E7D6 A:55 X:AA Y:44 P:NVUbdIzc SP:FB PPU:302,140 CYC:16021 $E7C0:50 14 BVC $E7D6 A:55 X:AA Y:44 P:NVUbdIzc SP:FB PPU:308,140 CYC:16023 $E7C2:10 12 BPL $E7D6 A:55 X:AA Y:44 P:NVUbdIzc SP:FB PPU:314,140 CYC:16025 $E7C4:C9 55 CMP #$55 A:55 X:AA Y:44 P:NVUbdIzc SP:FB PPU:320,140 CYC:16027 $E7C6:D0 0E BNE $E7D6 A:55 X:AA Y:44 P:67 SP:FB PPU:326,140 CYC:16029 $E7C8:C0 44 CPY #$44 A:55 X:AA Y:44 P:67 SP:FB PPU:332,140 CYC:16031 $E7CA:D0 0A BNE $E7D6 A:55 X:AA Y:44 P:67 SP:FB PPU:338,140 CYC:16033 $E7CC:E0 AA CPX #$AA A:55 X:AA Y:44 P:67 SP:FB PPU: 3,141 CYC:16035 $E7CE:D0 06 BNE $E7D6 A:55 X:AA Y:44 P:67 SP:FB PPU: 9,141 CYC:16037 $E7D0:A5 49 LDA $49 = #$00 A:55 X:AA Y:44 P:67 SP:FB PPU: 15,141 CYC:16039 $E7D2:C9 00 CMP #$00 A:00 X:AA Y:44 P:67 SP:FB PPU: 24,141 CYC:16042 $E7D4:F0 04 BEQ $E7DA A:00 X:AA Y:44 P:67 SP:FB PPU: 30,141 CYC:16044 $E7DA:A9 00 LDA #$00 A:00 X:AA Y:44 P:67 SP:FB PPU: 39,141 CYC:16047 $E7DC:85 56 STA $56 = #$00 A:00 X:AA Y:44 P:67 SP:FB PPU: 45,141 CYC:16049 $E7DE:A0 58 LDY #$58 A:00 X:AA Y:44 P:67 SP:FB PPU: 54,141 CYC:16052 $E7E0:A2 EF LDX #$EF A:00 X:AA Y:58 P:65 SP:FB PPU: 60,141 CYC:16054 $E7E2:A9 66 LDA #$66 A:00 X:EF Y:58 P:E5 SP:FB PPU: 66,141 CYC:16056 $E7E4:38 SEC A:66 X:EF Y:58 P:65 SP:FB PPU: 72,141 CYC:16058 $E7E5:B8 CLV A:66 X:EF Y:58 P:65 SP:FB PPU: 78,141 CYC:16060 $E7E6:87 56 *SAX $56 = #$00 A:66 X:EF Y:58 P:25 SP:FB PPU: 84,141 CYC:16062 $E7E8:EA NOP A:66 X:EF Y:58 P:25 SP:FB PPU: 93,141 CYC:16065 $E7E9:EA NOP A:66 X:EF Y:58 P:25 SP:FB PPU: 99,141 CYC:16067 $E7EA:EA NOP A:66 X:EF Y:58 P:25 SP:FB PPU:105,141 CYC:16069 $E7EB:EA NOP A:66 X:EF Y:58 P:25 SP:FB PPU:111,141 CYC:16071 $E7EC:F0 18 BEQ $E806 A:66 X:EF Y:58 P:25 SP:FB PPU:117,141 CYC:16073 $E7EE:90 16 BCC $E806 A:66 X:EF Y:58 P:25 SP:FB PPU:123,141 CYC:16075 $E7F0:70 14 BVS $E806 A:66 X:EF Y:58 P:25 SP:FB PPU:129,141 CYC:16077 $E7F2:30 12 BMI $E806 A:66 X:EF Y:58 P:25 SP:FB PPU:135,141 CYC:16079 $E7F4:C9 66 CMP #$66 A:66 X:EF Y:58 P:25 SP:FB PPU:141,141 CYC:16081 $E7F6:D0 0E BNE $E806 A:66 X:EF Y:58 P:nvUbdIZC SP:FB PPU:147,141 CYC:16083 $E7F8:C0 58 CPY #$58 A:66 X:EF Y:58 P:nvUbdIZC SP:FB PPU:153,141 CYC:16085 $E7FA:D0 0A BNE $E806 A:66 X:EF Y:58 P:nvUbdIZC SP:FB PPU:159,141 CYC:16087 $E7FC:E0 EF CPX #$EF A:66 X:EF Y:58 P:nvUbdIZC SP:FB PPU:165,141 CYC:16089 $E7FE:D0 06 BNE $E806 A:66 X:EF Y:58 P:nvUbdIZC SP:FB PPU:171,141 CYC:16091 $E800:A5 56 LDA $56 = #$66 A:66 X:EF Y:58 P:nvUbdIZC SP:FB PPU:177,141 CYC:16093 $E802:C9 66 CMP #$66 A:66 X:EF Y:58 P:25 SP:FB PPU:186,141 CYC:16096 $E804:F0 04 BEQ $E80A A:66 X:EF Y:58 P:nvUbdIZC SP:FB PPU:192,141 CYC:16098 $E80A:A9 FF LDA #$FF A:66 X:EF Y:58 P:nvUbdIZC SP:FB PPU:201,141 CYC:16101 $E80C:8D 49 05 STA $0549 = #$00 A:FF X:EF Y:58 P:A5 SP:FB PPU:207,141 CYC:16103 $E80F:A0 E5 LDY #$E5 A:FF X:EF Y:58 P:A5 SP:FB PPU:219,141 CYC:16107 $E811:A2 AF LDX #$AF A:FF X:EF Y:E5 P:A5 SP:FB PPU:225,141 CYC:16109 $E813:A9 F5 LDA #$F5 A:FF X:AF Y:E5 P:A5 SP:FB PPU:231,141 CYC:16111 $E815:24 01 BIT $01 = #$C0 A:F5 X:AF Y:E5 P:A5 SP:FB PPU:237,141 CYC:16113 $E817:18 CLC A:F5 X:AF Y:E5 P:E5 SP:FB PPU:246,141 CYC:16116 $E818:8F 49 05 *SAX $0549 = #$FF A:F5 X:AF Y:E5 P:NVUbdIzc SP:FB PPU:252,141 CYC:16118 $E81B:EA NOP A:F5 X:AF Y:E5 P:NVUbdIzc SP:FB PPU:264,141 CYC:16122 $E81C:EA NOP A:F5 X:AF Y:E5 P:NVUbdIzc SP:FB PPU:270,141 CYC:16124 $E81D:EA NOP A:F5 X:AF Y:E5 P:NVUbdIzc SP:FB PPU:276,141 CYC:16126 $E81E:EA NOP A:F5 X:AF Y:E5 P:NVUbdIzc SP:FB PPU:282,141 CYC:16128 $E81F:F0 19 BEQ $E83A A:F5 X:AF Y:E5 P:NVUbdIzc SP:FB PPU:288,141 CYC:16130 $E821:B0 17 BCS $E83A A:F5 X:AF Y:E5 P:NVUbdIzc SP:FB PPU:294,141 CYC:16132 $E823:50 15 BVC $E83A A:F5 X:AF Y:E5 P:NVUbdIzc SP:FB PPU:300,141 CYC:16134 $E825:10 13 BPL $E83A A:F5 X:AF Y:E5 P:NVUbdIzc SP:FB PPU:306,141 CYC:16136 $E827:C9 F5 CMP #$F5 A:F5 X:AF Y:E5 P:NVUbdIzc SP:FB PPU:312,141 CYC:16138 $E829:D0 0F BNE $E83A A:F5 X:AF Y:E5 P:67 SP:FB PPU:318,141 CYC:16140 $E82B:C0 E5 CPY #$E5 A:F5 X:AF Y:E5 P:67 SP:FB PPU:324,141 CYC:16142 $E82D:D0 0B BNE $E83A A:F5 X:AF Y:E5 P:67 SP:FB PPU:330,141 CYC:16144 $E82F:E0 AF CPX #$AF A:F5 X:AF Y:E5 P:67 SP:FB PPU:336,141 CYC:16146 $E831:D0 07 BNE $E83A A:F5 X:AF Y:E5 P:67 SP:FB PPU: 1,142 CYC:16148 $E833:AD 49 05 LDA $0549 = #$A5 A:F5 X:AF Y:E5 P:67 SP:FB PPU: 7,142 CYC:16150 $E836:C9 A5 CMP #$A5 A:A5 X:AF Y:E5 P:E5 SP:FB PPU: 19,142 CYC:16154 $E838:F0 04 BEQ $E83E A:A5 X:AF Y:E5 P:67 SP:FB PPU: 25,142 CYC:16156 $E83E:A9 00 LDA #$00 A:A5 X:AF Y:E5 P:67 SP:FB PPU: 34,142 CYC:16159 $E840:8D 56 05 STA $0556 = #$00 A:00 X:AF Y:E5 P:67 SP:FB PPU: 40,142 CYC:16161 $E843:A0 58 LDY #$58 A:00 X:AF Y:E5 P:67 SP:FB PPU: 52,142 CYC:16165 $E845:A2 B3 LDX #$B3 A:00 X:AF Y:58 P:65 SP:FB PPU: 58,142 CYC:16167 $E847:A9 97 LDA #$97 A:00 X:B3 Y:58 P:E5 SP:FB PPU: 64,142 CYC:16169 $E849:38 SEC A:97 X:B3 Y:58 P:E5 SP:FB PPU: 70,142 CYC:16171 $E84A:B8 CLV A:97 X:B3 Y:58 P:E5 SP:FB PPU: 76,142 CYC:16173 $E84B:8F 56 05 *SAX $0556 = #$00 A:97 X:B3 Y:58 P:A5 SP:FB PPU: 82,142 CYC:16175 $E84E:EA NOP A:97 X:B3 Y:58 P:A5 SP:FB PPU: 94,142 CYC:16179 $E84F:EA NOP A:97 X:B3 Y:58 P:A5 SP:FB PPU:100,142 CYC:16181 $E850:EA NOP A:97 X:B3 Y:58 P:A5 SP:FB PPU:106,142 CYC:16183 $E851:EA NOP A:97 X:B3 Y:58 P:A5 SP:FB PPU:112,142 CYC:16185 $E852:F0 19 BEQ $E86D A:97 X:B3 Y:58 P:A5 SP:FB PPU:118,142 CYC:16187 $E854:90 17 BCC $E86D A:97 X:B3 Y:58 P:A5 SP:FB PPU:124,142 CYC:16189 $E856:70 15 BVS $E86D A:97 X:B3 Y:58 P:A5 SP:FB PPU:130,142 CYC:16191 $E858:10 13 BPL $E86D A:97 X:B3 Y:58 P:A5 SP:FB PPU:136,142 CYC:16193 $E85A:C9 97 CMP #$97 A:97 X:B3 Y:58 P:A5 SP:FB PPU:142,142 CYC:16195 $E85C:D0 0F BNE $E86D A:97 X:B3 Y:58 P:nvUbdIZC SP:FB PPU:148,142 CYC:16197 $E85E:C0 58 CPY #$58 A:97 X:B3 Y:58 P:nvUbdIZC SP:FB PPU:154,142 CYC:16199 $E860:D0 0B BNE $E86D A:97 X:B3 Y:58 P:nvUbdIZC SP:FB PPU:160,142 CYC:16201 $E862:E0 B3 CPX #$B3 A:97 X:B3 Y:58 P:nvUbdIZC SP:FB PPU:166,142 CYC:16203 $E864:D0 07 BNE $E86D A:97 X:B3 Y:58 P:nvUbdIZC SP:FB PPU:172,142 CYC:16205 $E866:AD 56 05 LDA $0556 = #$93 A:97 X:B3 Y:58 P:nvUbdIZC SP:FB PPU:178,142 CYC:16207 $E869:C9 93 CMP #$93 A:93 X:B3 Y:58 P:A5 SP:FB PPU:190,142 CYC:16211 $E86B:F0 04 BEQ $E871 A:93 X:B3 Y:58 P:nvUbdIZC SP:FB PPU:196,142 CYC:16213 $E871:A9 FF LDA #$FF A:93 X:B3 Y:58 P:nvUbdIZC SP:FB PPU:205,142 CYC:16216 $E873:85 49 STA $49 = #$00 A:FF X:B3 Y:58 P:A5 SP:FB PPU:211,142 CYC:16218 $E875:A0 FF LDY #$FF A:FF X:B3 Y:58 P:A5 SP:FB PPU:220,142 CYC:16221 $E877:A2 AA LDX #$AA A:FF X:B3 Y:FF P:A5 SP:FB PPU:226,142 CYC:16223 $E879:A9 55 LDA #$55 A:FF X:AA Y:FF P:A5 SP:FB PPU:232,142 CYC:16225 $E87B:24 01 BIT $01 = #$C0 A:55 X:AA Y:FF P:25 SP:FB PPU:238,142 CYC:16227 $E87D:18 CLC A:55 X:AA Y:FF P:E5 SP:FB PPU:247,142 CYC:16230 $E87E:97 4A *SAX $4A,Y @ 49 = #$FF A:55 X:AA Y:FF P:NVUbdIzc SP:FB PPU:253,142 CYC:16232 $E880:EA NOP A:55 X:AA Y:FF P:NVUbdIzc SP:FB PPU:265,142 CYC:16236 $E881:EA NOP A:55 X:AA Y:FF P:NVUbdIzc SP:FB PPU:271,142 CYC:16238 $E882:EA NOP A:55 X:AA Y:FF P:NVUbdIzc SP:FB PPU:277,142 CYC:16240 $E883:EA NOP A:55 X:AA Y:FF P:NVUbdIzc SP:FB PPU:283,142 CYC:16242 $E884:F0 18 BEQ $E89E A:55 X:AA Y:FF P:NVUbdIzc SP:FB PPU:289,142 CYC:16244 $E886:B0 16 BCS $E89E A:55 X:AA Y:FF P:NVUbdIzc SP:FB PPU:295,142 CYC:16246 $E888:50 14 BVC $E89E A:55 X:AA Y:FF P:NVUbdIzc SP:FB PPU:301,142 CYC:16248 $E88A:10 12 BPL $E89E A:55 X:AA Y:FF P:NVUbdIzc SP:FB PPU:307,142 CYC:16250 $E88C:C9 55 CMP #$55 A:55 X:AA Y:FF P:NVUbdIzc SP:FB PPU:313,142 CYC:16252 $E88E:D0 0E BNE $E89E A:55 X:AA Y:FF P:67 SP:FB PPU:319,142 CYC:16254 $E890:C0 FF CPY #$FF A:55 X:AA Y:FF P:67 SP:FB PPU:325,142 CYC:16256 $E892:D0 0A BNE $E89E A:55 X:AA Y:FF P:67 SP:FB PPU:331,142 CYC:16258 $E894:E0 AA CPX #$AA A:55 X:AA Y:FF P:67 SP:FB PPU:337,142 CYC:16260 $E896:D0 06 BNE $E89E A:55 X:AA Y:FF P:67 SP:FB PPU: 2,143 CYC:16262 $E898:A5 49 LDA $49 = #$00 A:55 X:AA Y:FF P:67 SP:FB PPU: 8,143 CYC:16264 $E89A:C9 00 CMP #$00 A:00 X:AA Y:FF P:67 SP:FB PPU: 17,143 CYC:16267 $E89C:F0 04 BEQ $E8A2 A:00 X:AA Y:FF P:67 SP:FB PPU: 23,143 CYC:16269 $E8A2:A9 00 LDA #$00 A:00 X:AA Y:FF P:67 SP:FB PPU: 32,143 CYC:16272 $E8A4:85 56 STA $56 = #$66 A:00 X:AA Y:FF P:67 SP:FB PPU: 38,143 CYC:16274 $E8A6:A0 06 LDY #$06 A:00 X:AA Y:FF P:67 SP:FB PPU: 47,143 CYC:16277 $E8A8:A2 EF LDX #$EF A:00 X:AA Y:06 P:65 SP:FB PPU: 53,143 CYC:16279 $E8AA:A9 66 LDA #$66 A:00 X:EF Y:06 P:E5 SP:FB PPU: 59,143 CYC:16281 $E8AC:38 SEC A:66 X:EF Y:06 P:65 SP:FB PPU: 65,143 CYC:16283 $E8AD:B8 CLV A:66 X:EF Y:06 P:65 SP:FB PPU: 71,143 CYC:16285 $E8AE:97 50 *SAX $50,Y @ 56 = #$00 A:66 X:EF Y:06 P:25 SP:FB PPU: 77,143 CYC:16287 $E8B0:EA NOP A:66 X:EF Y:06 P:25 SP:FB PPU: 89,143 CYC:16291 $E8B1:EA NOP A:66 X:EF Y:06 P:25 SP:FB PPU: 95,143 CYC:16293 $E8B2:EA NOP A:66 X:EF Y:06 P:25 SP:FB PPU:101,143 CYC:16295 $E8B3:EA NOP A:66 X:EF Y:06 P:25 SP:FB PPU:107,143 CYC:16297 $E8B4:F0 18 BEQ $E8CE A:66 X:EF Y:06 P:25 SP:FB PPU:113,143 CYC:16299 $E8B6:90 16 BCC $E8CE A:66 X:EF Y:06 P:25 SP:FB PPU:119,143 CYC:16301 $E8B8:70 14 BVS $E8CE A:66 X:EF Y:06 P:25 SP:FB PPU:125,143 CYC:16303 $E8BA:30 12 BMI $E8CE A:66 X:EF Y:06 P:25 SP:FB PPU:131,143 CYC:16305 $E8BC:C9 66 CMP #$66 A:66 X:EF Y:06 P:25 SP:FB PPU:137,143 CYC:16307 $E8BE:D0 0E BNE $E8CE A:66 X:EF Y:06 P:nvUbdIZC SP:FB PPU:143,143 CYC:16309 $E8C0:C0 06 CPY #$06 A:66 X:EF Y:06 P:nvUbdIZC SP:FB PPU:149,143 CYC:16311 $E8C2:D0 0A BNE $E8CE A:66 X:EF Y:06 P:nvUbdIZC SP:FB PPU:155,143 CYC:16313 $E8C4:E0 EF CPX #$EF A:66 X:EF Y:06 P:nvUbdIZC SP:FB PPU:161,143 CYC:16315 $E8C6:D0 06 BNE $E8CE A:66 X:EF Y:06 P:nvUbdIZC SP:FB PPU:167,143 CYC:16317 $E8C8:A5 56 LDA $56 = #$66 A:66 X:EF Y:06 P:nvUbdIZC SP:FB PPU:173,143 CYC:16319 $E8CA:C9 66 CMP #$66 A:66 X:EF Y:06 P:25 SP:FB PPU:182,143 CYC:16322 $E8CC:F0 04 BEQ $E8D2 A:66 X:EF Y:06 P:nvUbdIZC SP:FB PPU:188,143 CYC:16324 $E8D2:60 RTS A:66 X:EF Y:06 P:nvUbdIZC SP:FB PPU:197,143 CYC:16327 $C638:20 D3 E8 JSR $E8D3 A:66 X:EF Y:06 P:nvUbdIZC SP:FD PPU:215,143 CYC:16333 $E8D3:A0 90 LDY #$90 A:66 X:EF Y:06 P:nvUbdIZC SP:FB PPU:233,143 CYC:16339 $E8D5:20 31 F9 JSR $F931 A:66 X:EF Y:90 P:A5 SP:FB PPU:239,143 CYC:16341 $F931:24 01 BIT $01 = #$C0 A:66 X:EF Y:90 P:A5 SP:F9 PPU:257,143 CYC:16347 $F933:A9 40 LDA #$40 A:66 X:EF Y:90 P:E5 SP:F9 PPU:266,143 CYC:16350 $F935:38 SEC A:40 X:EF Y:90 P:65 SP:F9 PPU:272,143 CYC:16352 $F936:60 RTS A:40 X:EF Y:90 P:65 SP:F9 PPU:278,143 CYC:16354 $E8D8:EB 40 *SBC #$40 A:40 X:EF Y:90 P:65 SP:FB PPU:296,143 CYC:16360 $E8DA:EA NOP A:00 X:EF Y:90 P:nvUbdIZC SP:FB PPU:302,143 CYC:16362 $E8DB:EA NOP A:00 X:EF Y:90 P:nvUbdIZC SP:FB PPU:308,143 CYC:16364 $E8DC:EA NOP A:00 X:EF Y:90 P:nvUbdIZC SP:FB PPU:314,143 CYC:16366 $E8DD:EA NOP A:00 X:EF Y:90 P:nvUbdIZC SP:FB PPU:320,143 CYC:16368 $E8DE:20 37 F9 JSR $F937 A:00 X:EF Y:90 P:nvUbdIZC SP:FB PPU:326,143 CYC:16370 $F937:30 0B BMI $F944 A:00 X:EF Y:90 P:nvUbdIZC SP:F9 PPU: 3,144 CYC:16376 $F939:90 09 BCC $F944 A:00 X:EF Y:90 P:nvUbdIZC SP:F9 PPU: 9,144 CYC:16378 $F93B:D0 07 BNE $F944 A:00 X:EF Y:90 P:nvUbdIZC SP:F9 PPU: 15,144 CYC:16380 $F93D:70 05 BVS $F944 A:00 X:EF Y:90 P:nvUbdIZC SP:F9 PPU: 21,144 CYC:16382 $F93F:C9 00 CMP #$00 A:00 X:EF Y:90 P:nvUbdIZC SP:F9 PPU: 27,144 CYC:16384 $F941:D0 01 BNE $F944 A:00 X:EF Y:90 P:nvUbdIZC SP:F9 PPU: 33,144 CYC:16386 $F943:60 RTS A:00 X:EF Y:90 P:nvUbdIZC SP:F9 PPU: 39,144 CYC:16388 $E8E1:C8 INY A:00 X:EF Y:90 P:nvUbdIZC SP:FB PPU: 57,144 CYC:16394 $E8E2:20 47 F9 JSR $F947 A:00 X:EF Y:91 P:A5 SP:FB PPU: 63,144 CYC:16396 $F947:B8 CLV A:00 X:EF Y:91 P:A5 SP:F9 PPU: 81,144 CYC:16402 $F948:38 SEC A:00 X:EF Y:91 P:A5 SP:F9 PPU: 87,144 CYC:16404 $F949:A9 40 LDA #$40 A:00 X:EF Y:91 P:A5 SP:F9 PPU: 93,144 CYC:16406 $F94B:60 RTS A:40 X:EF Y:91 P:25 SP:F9 PPU: 99,144 CYC:16408 $E8E5:EB 3F *SBC #$3F A:40 X:EF Y:91 P:25 SP:FB PPU:117,144 CYC:16414 $E8E7:EA NOP A:01 X:EF Y:91 P:25 SP:FB PPU:123,144 CYC:16416 $E8E8:EA NOP A:01 X:EF Y:91 P:25 SP:FB PPU:129,144 CYC:16418 $E8E9:EA NOP A:01 X:EF Y:91 P:25 SP:FB PPU:135,144 CYC:16420 $E8EA:EA NOP A:01 X:EF Y:91 P:25 SP:FB PPU:141,144 CYC:16422 $E8EB:20 4C F9 JSR $F94C A:01 X:EF Y:91 P:25 SP:FB PPU:147,144 CYC:16424 $F94C:F0 0B BEQ $F959 A:01 X:EF Y:91 P:25 SP:F9 PPU:165,144 CYC:16430 $F94E:30 09 BMI $F959 A:01 X:EF Y:91 P:25 SP:F9 PPU:171,144 CYC:16432 $F950:90 07 BCC $F959 A:01 X:EF Y:91 P:25 SP:F9 PPU:177,144 CYC:16434 $F952:70 05 BVS $F959 A:01 X:EF Y:91 P:25 SP:F9 PPU:183,144 CYC:16436 $F954:C9 01 CMP #$01 A:01 X:EF Y:91 P:25 SP:F9 PPU:189,144 CYC:16438 $F956:D0 01 BNE $F959 A:01 X:EF Y:91 P:nvUbdIZC SP:F9 PPU:195,144 CYC:16440 $F958:60 RTS A:01 X:EF Y:91 P:nvUbdIZC SP:F9 PPU:201,144 CYC:16442 $E8EE:C8 INY A:01 X:EF Y:91 P:nvUbdIZC SP:FB PPU:219,144 CYC:16448 $E8EF:20 5C F9 JSR $F95C A:01 X:EF Y:92 P:A5 SP:FB PPU:225,144 CYC:16450 $F95C:A9 40 LDA #$40 A:01 X:EF Y:92 P:A5 SP:F9 PPU:243,144 CYC:16456 $F95E:38 SEC A:40 X:EF Y:92 P:25 SP:F9 PPU:249,144 CYC:16458 $F95F:24 01 BIT $01 = #$C0 A:40 X:EF Y:92 P:25 SP:F9 PPU:255,144 CYC:16460 $F961:60 RTS A:40 X:EF Y:92 P:E5 SP:F9 PPU:264,144 CYC:16463 $E8F2:EB 41 *SBC #$41 A:40 X:EF Y:92 P:E5 SP:FB PPU:282,144 CYC:16469 $E8F4:EA NOP A:FF X:EF Y:92 P:NvUbdIzc SP:FB PPU:288,144 CYC:16471 $E8F5:EA NOP A:FF X:EF Y:92 P:NvUbdIzc SP:FB PPU:294,144 CYC:16473 $E8F6:EA NOP A:FF X:EF Y:92 P:NvUbdIzc SP:FB PPU:300,144 CYC:16475 $E8F7:EA NOP A:FF X:EF Y:92 P:NvUbdIzc SP:FB PPU:306,144 CYC:16477 $E8F8:20 62 F9 JSR $F962 A:FF X:EF Y:92 P:NvUbdIzc SP:FB PPU:312,144 CYC:16479 $F962:B0 0B BCS $F96F A:FF X:EF Y:92 P:NvUbdIzc SP:F9 PPU:330,144 CYC:16485 $F964:F0 09 BEQ $F96F A:FF X:EF Y:92 P:NvUbdIzc SP:F9 PPU:336,144 CYC:16487 $F966:10 07 BPL $F96F A:FF X:EF Y:92 P:NvUbdIzc SP:F9 PPU: 1,145 CYC:16489 $F968:70 05 BVS $F96F A:FF X:EF Y:92 P:NvUbdIzc SP:F9 PPU: 7,145 CYC:16491 $F96A:C9 FF CMP #$FF A:FF X:EF Y:92 P:NvUbdIzc SP:F9 PPU: 13,145 CYC:16493 $F96C:D0 01 BNE $F96F A:FF X:EF Y:92 P:nvUbdIZC SP:F9 PPU: 19,145 CYC:16495 $F96E:60 RTS A:FF X:EF Y:92 P:nvUbdIZC SP:F9 PPU: 25,145 CYC:16497 $E8FB:C8 INY A:FF X:EF Y:92 P:nvUbdIZC SP:FB PPU: 43,145 CYC:16503 $E8FC:20 72 F9 JSR $F972 A:FF X:EF Y:93 P:A5 SP:FB PPU: 49,145 CYC:16505 $F972:18 CLC A:FF X:EF Y:93 P:A5 SP:F9 PPU: 67,145 CYC:16511 $F973:A9 80 LDA #$80 A:FF X:EF Y:93 P:NvUbdIzc SP:F9 PPU: 73,145 CYC:16513 $F975:60 RTS A:80 X:EF Y:93 P:NvUbdIzc SP:F9 PPU: 79,145 CYC:16515 $E8FF:EB 00 *SBC #$00 A:80 X:EF Y:93 P:NvUbdIzc SP:FB PPU: 97,145 CYC:16521 $E901:EA NOP A:7F X:EF Y:93 P:65 SP:FB PPU:103,145 CYC:16523 $E902:EA NOP A:7F X:EF Y:93 P:65 SP:FB PPU:109,145 CYC:16525 $E903:EA NOP A:7F X:EF Y:93 P:65 SP:FB PPU:115,145 CYC:16527 $E904:EA NOP A:7F X:EF Y:93 P:65 SP:FB PPU:121,145 CYC:16529 $E905:20 76 F9 JSR $F976 A:7F X:EF Y:93 P:65 SP:FB PPU:127,145 CYC:16531 $F976:90 05 BCC $F97D A:7F X:EF Y:93 P:65 SP:F9 PPU:145,145 CYC:16537 $F978:C9 7F CMP #$7F A:7F X:EF Y:93 P:65 SP:F9 PPU:151,145 CYC:16539 $F97A:D0 01 BNE $F97D A:7F X:EF Y:93 P:67 SP:F9 PPU:157,145 CYC:16541 $F97C:60 RTS A:7F X:EF Y:93 P:67 SP:F9 PPU:163,145 CYC:16543 $E908:C8 INY A:7F X:EF Y:93 P:67 SP:FB PPU:181,145 CYC:16549 $E909:20 80 F9 JSR $F980 A:7F X:EF Y:94 P:E5 SP:FB PPU:187,145 CYC:16551 $F980:38 SEC A:7F X:EF Y:94 P:E5 SP:F9 PPU:205,145 CYC:16557 $F981:A9 81 LDA #$81 A:7F X:EF Y:94 P:E5 SP:F9 PPU:211,145 CYC:16559 $F983:60 RTS A:81 X:EF Y:94 P:E5 SP:F9 PPU:217,145 CYC:16561 $E90C:EB 7F *SBC #$7F A:81 X:EF Y:94 P:E5 SP:FB PPU:235,145 CYC:16567 $E90E:EA NOP A:02 X:EF Y:94 P:65 SP:FB PPU:241,145 CYC:16569 $E90F:EA NOP A:02 X:EF Y:94 P:65 SP:FB PPU:247,145 CYC:16571 $E910:EA NOP A:02 X:EF Y:94 P:65 SP:FB PPU:253,145 CYC:16573 $E911:EA NOP A:02 X:EF Y:94 P:65 SP:FB PPU:259,145 CYC:16575 $E912:20 84 F9 JSR $F984 A:02 X:EF Y:94 P:65 SP:FB PPU:265,145 CYC:16577 $F984:50 07 BVC $F98D A:02 X:EF Y:94 P:65 SP:F9 PPU:283,145 CYC:16583 $F986:90 05 BCC $F98D A:02 X:EF Y:94 P:65 SP:F9 PPU:289,145 CYC:16585 $F988:C9 02 CMP #$02 A:02 X:EF Y:94 P:65 SP:F9 PPU:295,145 CYC:16587 $F98A:D0 01 BNE $F98D A:02 X:EF Y:94 P:67 SP:F9 PPU:301,145 CYC:16589 $F98C:60 RTS A:02 X:EF Y:94 P:67 SP:F9 PPU:307,145 CYC:16591 $E915:60 RTS A:02 X:EF Y:94 P:67 SP:FB PPU:325,145 CYC:16597 $C63B:20 16 E9 JSR $E916 A:02 X:EF Y:94 P:67 SP:FD PPU: 2,146 CYC:16603 $E916:A9 FF LDA #$FF A:02 X:EF Y:94 P:67 SP:FB PPU: 20,146 CYC:16609 $E918:85 01 STA $01 = #$C0 A:FF X:EF Y:94 P:E5 SP:FB PPU: 26,146 CYC:16611 $E91A:A0 95 LDY #$95 A:FF X:EF Y:94 P:E5 SP:FB PPU: 35,146 CYC:16614 $E91C:A2 02 LDX #$02 A:FF X:EF Y:95 P:E5 SP:FB PPU: 41,146 CYC:16616 $E91E:A9 47 LDA #$47 A:FF X:02 Y:95 P:65 SP:FB PPU: 47,146 CYC:16618 $E920:85 47 STA $47 = #$00 A:47 X:02 Y:95 P:65 SP:FB PPU: 53,146 CYC:16620 $E922:A9 06 LDA #$06 A:47 X:02 Y:95 P:65 SP:FB PPU: 62,146 CYC:16623 $E924:85 48 STA $48 = #$00 A:06 X:02 Y:95 P:65 SP:FB PPU: 68,146 CYC:16625 $E926:A9 EB LDA #$EB A:06 X:02 Y:95 P:65 SP:FB PPU: 77,146 CYC:16628 $E928:8D 47 06 STA $0647 = #$00 A:EB X:02 Y:95 P:E5 SP:FB PPU: 83,146 CYC:16630 $E92B:20 31 FA JSR $FA31 A:EB X:02 Y:95 P:E5 SP:FB PPU: 95,146 CYC:16634 $FA31:24 01 BIT $01 = #$FF A:EB X:02 Y:95 P:E5 SP:F9 PPU:113,146 CYC:16640 $FA33:18 CLC A:EB X:02 Y:95 P:E5 SP:F9 PPU:122,146 CYC:16643 $FA34:A9 40 LDA #$40 A:EB X:02 Y:95 P:NVUbdIzc SP:F9 PPU:128,146 CYC:16645 $FA36:60 RTS A:40 X:02 Y:95 P:64 SP:F9 PPU:134,146 CYC:16647 $E92E:C3 45 *DCP ($45,X) @ 47 = #$0647 = EB A:40 X:02 Y:95 P:64 SP:FB PPU:152,146 CYC:16653 $E930:EA NOP A:40 X:02 Y:95 P:64 SP:FB PPU:176,146 CYC:16661 $E931:EA NOP A:40 X:02 Y:95 P:64 SP:FB PPU:182,146 CYC:16663 $E932:EA NOP A:40 X:02 Y:95 P:64 SP:FB PPU:188,146 CYC:16665 $E933:EA NOP A:40 X:02 Y:95 P:64 SP:FB PPU:194,146 CYC:16667 $E934:20 37 FA JSR $FA37 A:40 X:02 Y:95 P:64 SP:FB PPU:200,146 CYC:16669 $FA37:50 2C BVC $FA65 A:40 X:02 Y:95 P:64 SP:F9 PPU:218,146 CYC:16675 $FA39:B0 2A BCS $FA65 A:40 X:02 Y:95 P:64 SP:F9 PPU:224,146 CYC:16677 $FA3B:30 28 BMI $FA65 A:40 X:02 Y:95 P:64 SP:F9 PPU:230,146 CYC:16679 $FA3D:C9 40 CMP #$40 A:40 X:02 Y:95 P:64 SP:F9 PPU:236,146 CYC:16681 $FA3F:D0 24 BNE $FA65 A:40 X:02 Y:95 P:67 SP:F9 PPU:242,146 CYC:16683 $FA41:60 RTS A:40 X:02 Y:95 P:67 SP:F9 PPU:248,146 CYC:16685 $E937:AD 47 06 LDA $0647 = #$EA A:40 X:02 Y:95 P:67 SP:FB PPU:266,146 CYC:16691 $E93A:C9 EA CMP #$EA A:EA X:02 Y:95 P:E5 SP:FB PPU:278,146 CYC:16695 $E93C:F0 02 BEQ $E940 A:EA X:02 Y:95 P:67 SP:FB PPU:284,146 CYC:16697 $E940:C8 INY A:EA X:02 Y:95 P:67 SP:FB PPU:293,146 CYC:16700 $E941:A9 00 LDA #$00 A:EA X:02 Y:96 P:E5 SP:FB PPU:299,146 CYC:16702 $E943:8D 47 06 STA $0647 = #$EA A:00 X:02 Y:96 P:67 SP:FB PPU:305,146 CYC:16704 $E946:20 42 FA JSR $FA42 A:00 X:02 Y:96 P:67 SP:FB PPU:317,146 CYC:16708 $FA42:B8 CLV A:00 X:02 Y:96 P:67 SP:F9 PPU:335,146 CYC:16714 $FA43:38 SEC A:00 X:02 Y:96 P:nvUbdIZC SP:F9 PPU: 0,147 CYC:16716 $FA44:A9 FF LDA #$FF A:00 X:02 Y:96 P:nvUbdIZC SP:F9 PPU: 6,147 CYC:16718 $FA46:60 RTS A:FF X:02 Y:96 P:A5 SP:F9 PPU: 12,147 CYC:16720 $E949:C3 45 *DCP ($45,X) @ 47 = #$0647 = 00 A:FF X:02 Y:96 P:A5 SP:FB PPU: 30,147 CYC:16726 $E94B:EA NOP A:FF X:02 Y:96 P:nvUbdIZC SP:FB PPU: 54,147 CYC:16734 $E94C:EA NOP A:FF X:02 Y:96 P:nvUbdIZC SP:FB PPU: 60,147 CYC:16736 $E94D:EA NOP A:FF X:02 Y:96 P:nvUbdIZC SP:FB PPU: 66,147 CYC:16738 $E94E:EA NOP A:FF X:02 Y:96 P:nvUbdIZC SP:FB PPU: 72,147 CYC:16740 $E94F:20 47 FA JSR $FA47 A:FF X:02 Y:96 P:nvUbdIZC SP:FB PPU: 78,147 CYC:16742 $FA47:70 1C BVS $FA65 A:FF X:02 Y:96 P:nvUbdIZC SP:F9 PPU: 96,147 CYC:16748 $FA49:D0 1A BNE $FA65 A:FF X:02 Y:96 P:nvUbdIZC SP:F9 PPU:102,147 CYC:16750 $FA4B:30 18 BMI $FA65 A:FF X:02 Y:96 P:nvUbdIZC SP:F9 PPU:108,147 CYC:16752 $FA4D:90 16 BCC $FA65 A:FF X:02 Y:96 P:nvUbdIZC SP:F9 PPU:114,147 CYC:16754 $FA4F:C9 FF CMP #$FF A:FF X:02 Y:96 P:nvUbdIZC SP:F9 PPU:120,147 CYC:16756 $FA51:D0 12 BNE $FA65 A:FF X:02 Y:96 P:nvUbdIZC SP:F9 PPU:126,147 CYC:16758 $FA53:60 RTS A:FF X:02 Y:96 P:nvUbdIZC SP:F9 PPU:132,147 CYC:16760 $E952:AD 47 06 LDA $0647 = #$FF A:FF X:02 Y:96 P:nvUbdIZC SP:FB PPU:150,147 CYC:16766 $E955:C9 FF CMP #$FF A:FF X:02 Y:96 P:A5 SP:FB PPU:162,147 CYC:16770 $E957:F0 02 BEQ $E95B A:FF X:02 Y:96 P:nvUbdIZC SP:FB PPU:168,147 CYC:16772 $E95B:C8 INY A:FF X:02 Y:96 P:nvUbdIZC SP:FB PPU:177,147 CYC:16775 $E95C:A9 37 LDA #$37 A:FF X:02 Y:97 P:A5 SP:FB PPU:183,147 CYC:16777 $E95E:8D 47 06 STA $0647 = #$FF A:37 X:02 Y:97 P:25 SP:FB PPU:189,147 CYC:16779 $E961:20 54 FA JSR $FA54 A:37 X:02 Y:97 P:25 SP:FB PPU:201,147 CYC:16783 $FA54:24 01 BIT $01 = #$FF A:37 X:02 Y:97 P:25 SP:F9 PPU:219,147 CYC:16789 $FA56:A9 F0 LDA #$F0 A:37 X:02 Y:97 P:E5 SP:F9 PPU:228,147 CYC:16792 $FA58:60 RTS A:F0 X:02 Y:97 P:E5 SP:F9 PPU:234,147 CYC:16794 $E964:C3 45 *DCP ($45,X) @ 47 = #$0647 = 37 A:F0 X:02 Y:97 P:E5 SP:FB PPU:252,147 CYC:16800 $E966:EA NOP A:F0 X:02 Y:97 P:E5 SP:FB PPU:276,147 CYC:16808 $E967:EA NOP A:F0 X:02 Y:97 P:E5 SP:FB PPU:282,147 CYC:16810 $E968:EA NOP A:F0 X:02 Y:97 P:E5 SP:FB PPU:288,147 CYC:16812 $E969:EA NOP A:F0 X:02 Y:97 P:E5 SP:FB PPU:294,147 CYC:16814 $E96A:20 59 FA JSR $FA59 A:F0 X:02 Y:97 P:E5 SP:FB PPU:300,147 CYC:16816 $FA59:50 0A BVC $FA65 A:F0 X:02 Y:97 P:E5 SP:F9 PPU:318,147 CYC:16822 $FA5B:F0 08 BEQ $FA65 A:F0 X:02 Y:97 P:E5 SP:F9 PPU:324,147 CYC:16824 $FA5D:10 06 BPL $FA65 A:F0 X:02 Y:97 P:E5 SP:F9 PPU:330,147 CYC:16826 $FA5F:90 04 BCC $FA65 A:F0 X:02 Y:97 P:E5 SP:F9 PPU:336,147 CYC:16828 $FA61:C9 F0 CMP #$F0 A:F0 X:02 Y:97 P:E5 SP:F9 PPU: 1,148 CYC:16830 $FA63:F0 02 BEQ $FA67 A:F0 X:02 Y:97 P:67 SP:F9 PPU: 7,148 CYC:16832 $FA67:60 RTS A:F0 X:02 Y:97 P:67 SP:F9 PPU: 16,148 CYC:16835 $E96D:AD 47 06 LDA $0647 = #$36 A:F0 X:02 Y:97 P:67 SP:FB PPU: 34,148 CYC:16841 $E970:C9 36 CMP #$36 A:36 X:02 Y:97 P:65 SP:FB PPU: 46,148 CYC:16845 $E972:F0 02 BEQ $E976 A:36 X:02 Y:97 P:67 SP:FB PPU: 52,148 CYC:16847 $E976:C8 INY A:36 X:02 Y:97 P:67 SP:FB PPU: 61,148 CYC:16850 $E977:A9 EB LDA #$EB A:36 X:02 Y:98 P:E5 SP:FB PPU: 67,148 CYC:16852 $E979:85 47 STA $47 = #$47 A:EB X:02 Y:98 P:E5 SP:FB PPU: 73,148 CYC:16854 $E97B:20 31 FA JSR $FA31 A:EB X:02 Y:98 P:E5 SP:FB PPU: 82,148 CYC:16857 $FA31:24 01 BIT $01 = #$FF A:EB X:02 Y:98 P:E5 SP:F9 PPU:100,148 CYC:16863 $FA33:18 CLC A:EB X:02 Y:98 P:E5 SP:F9 PPU:109,148 CYC:16866 $FA34:A9 40 LDA #$40 A:EB X:02 Y:98 P:NVUbdIzc SP:F9 PPU:115,148 CYC:16868 $FA36:60 RTS A:40 X:02 Y:98 P:64 SP:F9 PPU:121,148 CYC:16870 $E97E:C7 47 *DCP $47 = #$EB A:40 X:02 Y:98 P:64 SP:FB PPU:139,148 CYC:16876 $E980:EA NOP A:40 X:02 Y:98 P:64 SP:FB PPU:154,148 CYC:16881 $E981:EA NOP A:40 X:02 Y:98 P:64 SP:FB PPU:160,148 CYC:16883 $E982:EA NOP A:40 X:02 Y:98 P:64 SP:FB PPU:166,148 CYC:16885 $E983:EA NOP A:40 X:02 Y:98 P:64 SP:FB PPU:172,148 CYC:16887 $E984:20 37 FA JSR $FA37 A:40 X:02 Y:98 P:64 SP:FB PPU:178,148 CYC:16889 $FA37:50 2C BVC $FA65 A:40 X:02 Y:98 P:64 SP:F9 PPU:196,148 CYC:16895 $FA39:B0 2A BCS $FA65 A:40 X:02 Y:98 P:64 SP:F9 PPU:202,148 CYC:16897 $FA3B:30 28 BMI $FA65 A:40 X:02 Y:98 P:64 SP:F9 PPU:208,148 CYC:16899 $FA3D:C9 40 CMP #$40 A:40 X:02 Y:98 P:64 SP:F9 PPU:214,148 CYC:16901 $FA3F:D0 24 BNE $FA65 A:40 X:02 Y:98 P:67 SP:F9 PPU:220,148 CYC:16903 $FA41:60 RTS A:40 X:02 Y:98 P:67 SP:F9 PPU:226,148 CYC:16905 $E987:A5 47 LDA $47 = #$EA A:40 X:02 Y:98 P:67 SP:FB PPU:244,148 CYC:16911 $E989:C9 EA CMP #$EA A:EA X:02 Y:98 P:E5 SP:FB PPU:253,148 CYC:16914 $E98B:F0 02 BEQ $E98F A:EA X:02 Y:98 P:67 SP:FB PPU:259,148 CYC:16916 $E98F:C8 INY A:EA X:02 Y:98 P:67 SP:FB PPU:268,148 CYC:16919 $E990:A9 00 LDA #$00 A:EA X:02 Y:99 P:E5 SP:FB PPU:274,148 CYC:16921 $E992:85 47 STA $47 = #$EA A:00 X:02 Y:99 P:67 SP:FB PPU:280,148 CYC:16923 $E994:20 42 FA JSR $FA42 A:00 X:02 Y:99 P:67 SP:FB PPU:289,148 CYC:16926 $FA42:B8 CLV A:00 X:02 Y:99 P:67 SP:F9 PPU:307,148 CYC:16932 $FA43:38 SEC A:00 X:02 Y:99 P:nvUbdIZC SP:F9 PPU:313,148 CYC:16934 $FA44:A9 FF LDA #$FF A:00 X:02 Y:99 P:nvUbdIZC SP:F9 PPU:319,148 CYC:16936 $FA46:60 RTS A:FF X:02 Y:99 P:A5 SP:F9 PPU:325,148 CYC:16938 $E997:C7 47 *DCP $47 = #$00 A:FF X:02 Y:99 P:A5 SP:FB PPU: 2,149 CYC:16944 $E999:EA NOP A:FF X:02 Y:99 P:nvUbdIZC SP:FB PPU: 17,149 CYC:16949 $E99A:EA NOP A:FF X:02 Y:99 P:nvUbdIZC SP:FB PPU: 23,149 CYC:16951 $E99B:EA NOP A:FF X:02 Y:99 P:nvUbdIZC SP:FB PPU: 29,149 CYC:16953 $E99C:EA NOP A:FF X:02 Y:99 P:nvUbdIZC SP:FB PPU: 35,149 CYC:16955 $E99D:20 47 FA JSR $FA47 A:FF X:02 Y:99 P:nvUbdIZC SP:FB PPU: 41,149 CYC:16957 $FA47:70 1C BVS $FA65 A:FF X:02 Y:99 P:nvUbdIZC SP:F9 PPU: 59,149 CYC:16963 $FA49:D0 1A BNE $FA65 A:FF X:02 Y:99 P:nvUbdIZC SP:F9 PPU: 65,149 CYC:16965 $FA4B:30 18 BMI $FA65 A:FF X:02 Y:99 P:nvUbdIZC SP:F9 PPU: 71,149 CYC:16967 $FA4D:90 16 BCC $FA65 A:FF X:02 Y:99 P:nvUbdIZC SP:F9 PPU: 77,149 CYC:16969 $FA4F:C9 FF CMP #$FF A:FF X:02 Y:99 P:nvUbdIZC SP:F9 PPU: 83,149 CYC:16971 $FA51:D0 12 BNE $FA65 A:FF X:02 Y:99 P:nvUbdIZC SP:F9 PPU: 89,149 CYC:16973 $FA53:60 RTS A:FF X:02 Y:99 P:nvUbdIZC SP:F9 PPU: 95,149 CYC:16975 $E9A0:A5 47 LDA $47 = #$FF A:FF X:02 Y:99 P:nvUbdIZC SP:FB PPU:113,149 CYC:16981 $E9A2:C9 FF CMP #$FF A:FF X:02 Y:99 P:A5 SP:FB PPU:122,149 CYC:16984 $E9A4:F0 02 BEQ $E9A8 A:FF X:02 Y:99 P:nvUbdIZC SP:FB PPU:128,149 CYC:16986 $E9A8:C8 INY A:FF X:02 Y:99 P:nvUbdIZC SP:FB PPU:137,149 CYC:16989 $E9A9:A9 37 LDA #$37 A:FF X:02 Y:9A P:A5 SP:FB PPU:143,149 CYC:16991 $E9AB:85 47 STA $47 = #$FF A:37 X:02 Y:9A P:25 SP:FB PPU:149,149 CYC:16993 $E9AD:20 54 FA JSR $FA54 A:37 X:02 Y:9A P:25 SP:FB PPU:158,149 CYC:16996 $FA54:24 01 BIT $01 = #$FF A:37 X:02 Y:9A P:25 SP:F9 PPU:176,149 CYC:17002 $FA56:A9 F0 LDA #$F0 A:37 X:02 Y:9A P:E5 SP:F9 PPU:185,149 CYC:17005 $FA58:60 RTS A:F0 X:02 Y:9A P:E5 SP:F9 PPU:191,149 CYC:17007 $E9B0:C7 47 *DCP $47 = #$37 A:F0 X:02 Y:9A P:E5 SP:FB PPU:209,149 CYC:17013 $E9B2:EA NOP A:F0 X:02 Y:9A P:E5 SP:FB PPU:224,149 CYC:17018 $E9B3:EA NOP A:F0 X:02 Y:9A P:E5 SP:FB PPU:230,149 CYC:17020 $E9B4:EA NOP A:F0 X:02 Y:9A P:E5 SP:FB PPU:236,149 CYC:17022 $E9B5:EA NOP A:F0 X:02 Y:9A P:E5 SP:FB PPU:242,149 CYC:17024 $E9B6:20 59 FA JSR $FA59 A:F0 X:02 Y:9A P:E5 SP:FB PPU:248,149 CYC:17026 $FA59:50 0A BVC $FA65 A:F0 X:02 Y:9A P:E5 SP:F9 PPU:266,149 CYC:17032 $FA5B:F0 08 BEQ $FA65 A:F0 X:02 Y:9A P:E5 SP:F9 PPU:272,149 CYC:17034 $FA5D:10 06 BPL $FA65 A:F0 X:02 Y:9A P:E5 SP:F9 PPU:278,149 CYC:17036 $FA5F:90 04 BCC $FA65 A:F0 X:02 Y:9A P:E5 SP:F9 PPU:284,149 CYC:17038 $FA61:C9 F0 CMP #$F0 A:F0 X:02 Y:9A P:E5 SP:F9 PPU:290,149 CYC:17040 $FA63:F0 02 BEQ $FA67 A:F0 X:02 Y:9A P:67 SP:F9 PPU:296,149 CYC:17042 $FA67:60 RTS A:F0 X:02 Y:9A P:67 SP:F9 PPU:305,149 CYC:17045 $E9B9:A5 47 LDA $47 = #$36 A:F0 X:02 Y:9A P:67 SP:FB PPU:323,149 CYC:17051 $E9BB:C9 36 CMP #$36 A:36 X:02 Y:9A P:65 SP:FB PPU:332,149 CYC:17054 $E9BD:F0 02 BEQ $E9C1 A:36 X:02 Y:9A P:67 SP:FB PPU:338,149 CYC:17056 $E9C1:C8 INY A:36 X:02 Y:9A P:67 SP:FB PPU: 6,150 CYC:17059 $E9C2:A9 EB LDA #$EB A:36 X:02 Y:9B P:E5 SP:FB PPU: 12,150 CYC:17061 $E9C4:8D 47 06 STA $0647 = #$36 A:EB X:02 Y:9B P:E5 SP:FB PPU: 18,150 CYC:17063 $E9C7:20 31 FA JSR $FA31 A:EB X:02 Y:9B P:E5 SP:FB PPU: 30,150 CYC:17067 $FA31:24 01 BIT $01 = #$FF A:EB X:02 Y:9B P:E5 SP:F9 PPU: 48,150 CYC:17073 $FA33:18 CLC A:EB X:02 Y:9B P:E5 SP:F9 PPU: 57,150 CYC:17076 $FA34:A9 40 LDA #$40 A:EB X:02 Y:9B P:NVUbdIzc SP:F9 PPU: 63,150 CYC:17078 $FA36:60 RTS A:40 X:02 Y:9B P:64 SP:F9 PPU: 69,150 CYC:17080 $E9CA:CF 47 06 *DCP $0647 = #$EB A:40 X:02 Y:9B P:64 SP:FB PPU: 87,150 CYC:17086 $E9CD:EA NOP A:40 X:02 Y:9B P:64 SP:FB PPU:105,150 CYC:17092 $E9CE:EA NOP A:40 X:02 Y:9B P:64 SP:FB PPU:111,150 CYC:17094 $E9CF:EA NOP A:40 X:02 Y:9B P:64 SP:FB PPU:117,150 CYC:17096 $E9D0:EA NOP A:40 X:02 Y:9B P:64 SP:FB PPU:123,150 CYC:17098 $E9D1:20 37 FA JSR $FA37 A:40 X:02 Y:9B P:64 SP:FB PPU:129,150 CYC:17100 $FA37:50 2C BVC $FA65 A:40 X:02 Y:9B P:64 SP:F9 PPU:147,150 CYC:17106 $FA39:B0 2A BCS $FA65 A:40 X:02 Y:9B P:64 SP:F9 PPU:153,150 CYC:17108 $FA3B:30 28 BMI $FA65 A:40 X:02 Y:9B P:64 SP:F9 PPU:159,150 CYC:17110 $FA3D:C9 40 CMP #$40 A:40 X:02 Y:9B P:64 SP:F9 PPU:165,150 CYC:17112 $FA3F:D0 24 BNE $FA65 A:40 X:02 Y:9B P:67 SP:F9 PPU:171,150 CYC:17114 $FA41:60 RTS A:40 X:02 Y:9B P:67 SP:F9 PPU:177,150 CYC:17116 $E9D4:AD 47 06 LDA $0647 = #$EA A:40 X:02 Y:9B P:67 SP:FB PPU:195,150 CYC:17122 $E9D7:C9 EA CMP #$EA A:EA X:02 Y:9B P:E5 SP:FB PPU:207,150 CYC:17126 $E9D9:F0 02 BEQ $E9DD A:EA X:02 Y:9B P:67 SP:FB PPU:213,150 CYC:17128 $E9DD:C8 INY A:EA X:02 Y:9B P:67 SP:FB PPU:222,150 CYC:17131 $E9DE:A9 00 LDA #$00 A:EA X:02 Y:9C P:E5 SP:FB PPU:228,150 CYC:17133 $E9E0:8D 47 06 STA $0647 = #$EA A:00 X:02 Y:9C P:67 SP:FB PPU:234,150 CYC:17135 $E9E3:20 42 FA JSR $FA42 A:00 X:02 Y:9C P:67 SP:FB PPU:246,150 CYC:17139 $FA42:B8 CLV A:00 X:02 Y:9C P:67 SP:F9 PPU:264,150 CYC:17145 $FA43:38 SEC A:00 X:02 Y:9C P:nvUbdIZC SP:F9 PPU:270,150 CYC:17147 $FA44:A9 FF LDA #$FF A:00 X:02 Y:9C P:nvUbdIZC SP:F9 PPU:276,150 CYC:17149 $FA46:60 RTS A:FF X:02 Y:9C P:A5 SP:F9 PPU:282,150 CYC:17151 $E9E6:CF 47 06 *DCP $0647 = #$00 A:FF X:02 Y:9C P:A5 SP:FB PPU:300,150 CYC:17157 $E9E9:EA NOP A:FF X:02 Y:9C P:nvUbdIZC SP:FB PPU:318,150 CYC:17163 $E9EA:EA NOP A:FF X:02 Y:9C P:nvUbdIZC SP:FB PPU:324,150 CYC:17165 $E9EB:EA NOP A:FF X:02 Y:9C P:nvUbdIZC SP:FB PPU:330,150 CYC:17167 $E9EC:EA NOP A:FF X:02 Y:9C P:nvUbdIZC SP:FB PPU:336,150 CYC:17169 $E9ED:20 47 FA JSR $FA47 A:FF X:02 Y:9C P:nvUbdIZC SP:FB PPU: 1,151 CYC:17171 $FA47:70 1C BVS $FA65 A:FF X:02 Y:9C P:nvUbdIZC SP:F9 PPU: 19,151 CYC:17177 $FA49:D0 1A BNE $FA65 A:FF X:02 Y:9C P:nvUbdIZC SP:F9 PPU: 25,151 CYC:17179 $FA4B:30 18 BMI $FA65 A:FF X:02 Y:9C P:nvUbdIZC SP:F9 PPU: 31,151 CYC:17181 $FA4D:90 16 BCC $FA65 A:FF X:02 Y:9C P:nvUbdIZC SP:F9 PPU: 37,151 CYC:17183 $FA4F:C9 FF CMP #$FF A:FF X:02 Y:9C P:nvUbdIZC SP:F9 PPU: 43,151 CYC:17185 $FA51:D0 12 BNE $FA65 A:FF X:02 Y:9C P:nvUbdIZC SP:F9 PPU: 49,151 CYC:17187 $FA53:60 RTS A:FF X:02 Y:9C P:nvUbdIZC SP:F9 PPU: 55,151 CYC:17189 $E9F0:AD 47 06 LDA $0647 = #$FF A:FF X:02 Y:9C P:nvUbdIZC SP:FB PPU: 73,151 CYC:17195 $E9F3:C9 FF CMP #$FF A:FF X:02 Y:9C P:A5 SP:FB PPU: 85,151 CYC:17199 $E9F5:F0 02 BEQ $E9F9 A:FF X:02 Y:9C P:nvUbdIZC SP:FB PPU: 91,151 CYC:17201 $E9F9:C8 INY A:FF X:02 Y:9C P:nvUbdIZC SP:FB PPU:100,151 CYC:17204 $E9FA:A9 37 LDA #$37 A:FF X:02 Y:9D P:A5 SP:FB PPU:106,151 CYC:17206 $E9FC:8D 47 06 STA $0647 = #$FF A:37 X:02 Y:9D P:25 SP:FB PPU:112,151 CYC:17208 $E9FF:20 54 FA JSR $FA54 A:37 X:02 Y:9D P:25 SP:FB PPU:124,151 CYC:17212 $FA54:24 01 BIT $01 = #$FF A:37 X:02 Y:9D P:25 SP:F9 PPU:142,151 CYC:17218 $FA56:A9 F0 LDA #$F0 A:37 X:02 Y:9D P:E5 SP:F9 PPU:151,151 CYC:17221 $FA58:60 RTS A:F0 X:02 Y:9D P:E5 SP:F9 PPU:157,151 CYC:17223 $EA02:CF 47 06 *DCP $0647 = #$37 A:F0 X:02 Y:9D P:E5 SP:FB PPU:175,151 CYC:17229 $EA05:EA NOP A:F0 X:02 Y:9D P:E5 SP:FB PPU:193,151 CYC:17235 $EA06:EA NOP A:F0 X:02 Y:9D P:E5 SP:FB PPU:199,151 CYC:17237 $EA07:EA NOP A:F0 X:02 Y:9D P:E5 SP:FB PPU:205,151 CYC:17239 $EA08:EA NOP A:F0 X:02 Y:9D P:E5 SP:FB PPU:211,151 CYC:17241 $EA09:20 59 FA JSR $FA59 A:F0 X:02 Y:9D P:E5 SP:FB PPU:217,151 CYC:17243 $FA59:50 0A BVC $FA65 A:F0 X:02 Y:9D P:E5 SP:F9 PPU:235,151 CYC:17249 $FA5B:F0 08 BEQ $FA65 A:F0 X:02 Y:9D P:E5 SP:F9 PPU:241,151 CYC:17251 $FA5D:10 06 BPL $FA65 A:F0 X:02 Y:9D P:E5 SP:F9 PPU:247,151 CYC:17253 $FA5F:90 04 BCC $FA65 A:F0 X:02 Y:9D P:E5 SP:F9 PPU:253,151 CYC:17255 $FA61:C9 F0 CMP #$F0 A:F0 X:02 Y:9D P:E5 SP:F9 PPU:259,151 CYC:17257 $FA63:F0 02 BEQ $FA67 A:F0 X:02 Y:9D P:67 SP:F9 PPU:265,151 CYC:17259 $FA67:60 RTS A:F0 X:02 Y:9D P:67 SP:F9 PPU:274,151 CYC:17262 $EA0C:AD 47 06 LDA $0647 = #$36 A:F0 X:02 Y:9D P:67 SP:FB PPU:292,151 CYC:17268 $EA0F:C9 36 CMP #$36 A:36 X:02 Y:9D P:65 SP:FB PPU:304,151 CYC:17272 $EA11:F0 02 BEQ $EA15 A:36 X:02 Y:9D P:67 SP:FB PPU:310,151 CYC:17274 $EA15:A9 EB LDA #$EB A:36 X:02 Y:9D P:67 SP:FB PPU:319,151 CYC:17277 $EA17:8D 47 06 STA $0647 = #$36 A:EB X:02 Y:9D P:E5 SP:FB PPU:325,151 CYC:17279 $EA1A:A9 48 LDA #$48 A:EB X:02 Y:9D P:E5 SP:FB PPU:337,151 CYC:17283 $EA1C:85 45 STA $45 = #$32 A:48 X:02 Y:9D P:65 SP:FB PPU: 2,152 CYC:17285 $EA1E:A9 05 LDA #$05 A:48 X:02 Y:9D P:65 SP:FB PPU: 11,152 CYC:17288 $EA20:85 46 STA $46 = #$04 A:05 X:02 Y:9D P:65 SP:FB PPU: 17,152 CYC:17290 $EA22:A0 FF LDY #$FF A:05 X:02 Y:9D P:65 SP:FB PPU: 26,152 CYC:17293 $EA24:20 31 FA JSR $FA31 A:05 X:02 Y:FF P:E5 SP:FB PPU: 32,152 CYC:17295 $FA31:24 01 BIT $01 = #$FF A:05 X:02 Y:FF P:E5 SP:F9 PPU: 50,152 CYC:17301 $FA33:18 CLC A:05 X:02 Y:FF P:E5 SP:F9 PPU: 59,152 CYC:17304 $FA34:A9 40 LDA #$40 A:05 X:02 Y:FF P:NVUbdIzc SP:F9 PPU: 65,152 CYC:17306 $FA36:60 RTS A:40 X:02 Y:FF P:64 SP:F9 PPU: 71,152 CYC:17308 $EA27:D3 45 *DCP ($45),Y = #$0548 @ 0647 = EB A:40 X:02 Y:FF P:64 SP:FB PPU: 89,152 CYC:17314 $EA29:EA NOP A:40 X:02 Y:FF P:64 SP:FB PPU:113,152 CYC:17322 $EA2A:EA NOP A:40 X:02 Y:FF P:64 SP:FB PPU:119,152 CYC:17324 $EA2B:08 PHP A:40 X:02 Y:FF P:64 SP:FB PPU:125,152 CYC:17326 $EA2C:48 PHA A:40 X:02 Y:FF P:64 SP:FA PPU:134,152 CYC:17329 $EA2D:A0 9E LDY #$9E A:40 X:02 Y:FF P:64 SP:F9 PPU:143,152 CYC:17332 $EA2F:68 PLA A:40 X:02 Y:9E P:NVUbdIzc SP:F9 PPU:149,152 CYC:17334 $EA30:28 PLP A:40 X:02 Y:9E P:64 SP:FA PPU:161,152 CYC:17338 $EA31:20 37 FA JSR $FA37 A:40 X:02 Y:9E P:64 SP:FB PPU:173,152 CYC:17342 $FA37:50 2C BVC $FA65 A:40 X:02 Y:9E P:64 SP:F9 PPU:191,152 CYC:17348 $FA39:B0 2A BCS $FA65 A:40 X:02 Y:9E P:64 SP:F9 PPU:197,152 CYC:17350 $FA3B:30 28 BMI $FA65 A:40 X:02 Y:9E P:64 SP:F9 PPU:203,152 CYC:17352 $FA3D:C9 40 CMP #$40 A:40 X:02 Y:9E P:64 SP:F9 PPU:209,152 CYC:17354 $FA3F:D0 24 BNE $FA65 A:40 X:02 Y:9E P:67 SP:F9 PPU:215,152 CYC:17356 $FA41:60 RTS A:40 X:02 Y:9E P:67 SP:F9 PPU:221,152 CYC:17358 $EA34:AD 47 06 LDA $0647 = #$EA A:40 X:02 Y:9E P:67 SP:FB PPU:239,152 CYC:17364 $EA37:C9 EA CMP #$EA A:EA X:02 Y:9E P:E5 SP:FB PPU:251,152 CYC:17368 $EA39:F0 02 BEQ $EA3D A:EA X:02 Y:9E P:67 SP:FB PPU:257,152 CYC:17370 $EA3D:A0 FF LDY #$FF A:EA X:02 Y:9E P:67 SP:FB PPU:266,152 CYC:17373 $EA3F:A9 00 LDA #$00 A:EA X:02 Y:FF P:E5 SP:FB PPU:272,152 CYC:17375 $EA41:8D 47 06 STA $0647 = #$EA A:00 X:02 Y:FF P:67 SP:FB PPU:278,152 CYC:17377 $EA44:20 42 FA JSR $FA42 A:00 X:02 Y:FF P:67 SP:FB PPU:290,152 CYC:17381 $FA42:B8 CLV A:00 X:02 Y:FF P:67 SP:F9 PPU:308,152 CYC:17387 $FA43:38 SEC A:00 X:02 Y:FF P:nvUbdIZC SP:F9 PPU:314,152 CYC:17389 $FA44:A9 FF LDA #$FF A:00 X:02 Y:FF P:nvUbdIZC SP:F9 PPU:320,152 CYC:17391 $FA46:60 RTS A:FF X:02 Y:FF P:A5 SP:F9 PPU:326,152 CYC:17393 $EA47:D3 45 *DCP ($45),Y = #$0548 @ 0647 = 00 A:FF X:02 Y:FF P:A5 SP:FB PPU: 3,153 CYC:17399 $EA49:EA NOP A:FF X:02 Y:FF P:nvUbdIZC SP:FB PPU: 27,153 CYC:17407 $EA4A:EA NOP A:FF X:02 Y:FF P:nvUbdIZC SP:FB PPU: 33,153 CYC:17409 $EA4B:08 PHP A:FF X:02 Y:FF P:nvUbdIZC SP:FB PPU: 39,153 CYC:17411 $EA4C:48 PHA A:FF X:02 Y:FF P:nvUbdIZC SP:FA PPU: 48,153 CYC:17414 $EA4D:A0 9F LDY #$9F A:FF X:02 Y:FF P:nvUbdIZC SP:F9 PPU: 57,153 CYC:17417 $EA4F:68 PLA A:FF X:02 Y:9F P:A5 SP:F9 PPU: 63,153 CYC:17419 $EA50:28 PLP A:FF X:02 Y:9F P:A5 SP:FA PPU: 75,153 CYC:17423 $EA51:20 47 FA JSR $FA47 A:FF X:02 Y:9F P:nvUbdIZC SP:FB PPU: 87,153 CYC:17427 $FA47:70 1C BVS $FA65 A:FF X:02 Y:9F P:nvUbdIZC SP:F9 PPU:105,153 CYC:17433 $FA49:D0 1A BNE $FA65 A:FF X:02 Y:9F P:nvUbdIZC SP:F9 PPU:111,153 CYC:17435 $FA4B:30 18 BMI $FA65 A:FF X:02 Y:9F P:nvUbdIZC SP:F9 PPU:117,153 CYC:17437 $FA4D:90 16 BCC $FA65 A:FF X:02 Y:9F P:nvUbdIZC SP:F9 PPU:123,153 CYC:17439 $FA4F:C9 FF CMP #$FF A:FF X:02 Y:9F P:nvUbdIZC SP:F9 PPU:129,153 CYC:17441 $FA51:D0 12 BNE $FA65 A:FF X:02 Y:9F P:nvUbdIZC SP:F9 PPU:135,153 CYC:17443 $FA53:60 RTS A:FF X:02 Y:9F P:nvUbdIZC SP:F9 PPU:141,153 CYC:17445 $EA54:AD 47 06 LDA $0647 = #$FF A:FF X:02 Y:9F P:nvUbdIZC SP:FB PPU:159,153 CYC:17451 $EA57:C9 FF CMP #$FF A:FF X:02 Y:9F P:A5 SP:FB PPU:171,153 CYC:17455 $EA59:F0 02 BEQ $EA5D A:FF X:02 Y:9F P:nvUbdIZC SP:FB PPU:177,153 CYC:17457 $EA5D:A0 FF LDY #$FF A:FF X:02 Y:9F P:nvUbdIZC SP:FB PPU:186,153 CYC:17460 $EA5F:A9 37 LDA #$37 A:FF X:02 Y:FF P:A5 SP:FB PPU:192,153 CYC:17462 $EA61:8D 47 06 STA $0647 = #$FF A:37 X:02 Y:FF P:25 SP:FB PPU:198,153 CYC:17464 $EA64:20 54 FA JSR $FA54 A:37 X:02 Y:FF P:25 SP:FB PPU:210,153 CYC:17468 $FA54:24 01 BIT $01 = #$FF A:37 X:02 Y:FF P:25 SP:F9 PPU:228,153 CYC:17474 $FA56:A9 F0 LDA #$F0 A:37 X:02 Y:FF P:E5 SP:F9 PPU:237,153 CYC:17477 $FA58:60 RTS A:F0 X:02 Y:FF P:E5 SP:F9 PPU:243,153 CYC:17479 $EA67:D3 45 *DCP ($45),Y = #$0548 @ 0647 = 37 A:F0 X:02 Y:FF P:E5 SP:FB PPU:261,153 CYC:17485 $EA69:EA NOP A:F0 X:02 Y:FF P:E5 SP:FB PPU:285,153 CYC:17493 $EA6A:EA NOP A:F0 X:02 Y:FF P:E5 SP:FB PPU:291,153 CYC:17495 $EA6B:08 PHP A:F0 X:02 Y:FF P:E5 SP:FB PPU:297,153 CYC:17497 $EA6C:48 PHA A:F0 X:02 Y:FF P:E5 SP:FA PPU:306,153 CYC:17500 $EA6D:A0 A0 LDY #$A0 A:F0 X:02 Y:FF P:E5 SP:F9 PPU:315,153 CYC:17503 $EA6F:68 PLA A:F0 X:02 Y:A0 P:E5 SP:F9 PPU:321,153 CYC:17505 $EA70:28 PLP A:F0 X:02 Y:A0 P:E5 SP:FA PPU:333,153 CYC:17509 $EA71:20 59 FA JSR $FA59 A:F0 X:02 Y:A0 P:E5 SP:FB PPU: 4,154 CYC:17513 $FA59:50 0A BVC $FA65 A:F0 X:02 Y:A0 P:E5 SP:F9 PPU: 22,154 CYC:17519 $FA5B:F0 08 BEQ $FA65 A:F0 X:02 Y:A0 P:E5 SP:F9 PPU: 28,154 CYC:17521 $FA5D:10 06 BPL $FA65 A:F0 X:02 Y:A0 P:E5 SP:F9 PPU: 34,154 CYC:17523 $FA5F:90 04 BCC $FA65 A:F0 X:02 Y:A0 P:E5 SP:F9 PPU: 40,154 CYC:17525 $FA61:C9 F0 CMP #$F0 A:F0 X:02 Y:A0 P:E5 SP:F9 PPU: 46,154 CYC:17527 $FA63:F0 02 BEQ $FA67 A:F0 X:02 Y:A0 P:67 SP:F9 PPU: 52,154 CYC:17529 $FA67:60 RTS A:F0 X:02 Y:A0 P:67 SP:F9 PPU: 61,154 CYC:17532 $EA74:AD 47 06 LDA $0647 = #$36 A:F0 X:02 Y:A0 P:67 SP:FB PPU: 79,154 CYC:17538 $EA77:C9 36 CMP #$36 A:36 X:02 Y:A0 P:65 SP:FB PPU: 91,154 CYC:17542 $EA79:F0 02 BEQ $EA7D A:36 X:02 Y:A0 P:67 SP:FB PPU: 97,154 CYC:17544 $EA7D:A0 A1 LDY #$A1 A:36 X:02 Y:A0 P:67 SP:FB PPU:106,154 CYC:17547 $EA7F:A2 FF LDX #$FF A:36 X:02 Y:A1 P:E5 SP:FB PPU:112,154 CYC:17549 $EA81:A9 EB LDA #$EB A:36 X:FF Y:A1 P:E5 SP:FB PPU:118,154 CYC:17551 $EA83:85 47 STA $47 = #$36 A:EB X:FF Y:A1 P:E5 SP:FB PPU:124,154 CYC:17553 $EA85:20 31 FA JSR $FA31 A:EB X:FF Y:A1 P:E5 SP:FB PPU:133,154 CYC:17556 $FA31:24 01 BIT $01 = #$FF A:EB X:FF Y:A1 P:E5 SP:F9 PPU:151,154 CYC:17562 $FA33:18 CLC A:EB X:FF Y:A1 P:E5 SP:F9 PPU:160,154 CYC:17565 $FA34:A9 40 LDA #$40 A:EB X:FF Y:A1 P:NVUbdIzc SP:F9 PPU:166,154 CYC:17567 $FA36:60 RTS A:40 X:FF Y:A1 P:64 SP:F9 PPU:172,154 CYC:17569 $EA88:D7 48 *DCP $48,X @ 47 = #$EB A:40 X:FF Y:A1 P:64 SP:FB PPU:190,154 CYC:17575 $EA8A:EA NOP A:40 X:FF Y:A1 P:64 SP:FB PPU:208,154 CYC:17581 $EA8B:EA NOP A:40 X:FF Y:A1 P:64 SP:FB PPU:214,154 CYC:17583 $EA8C:EA NOP A:40 X:FF Y:A1 P:64 SP:FB PPU:220,154 CYC:17585 $EA8D:EA NOP A:40 X:FF Y:A1 P:64 SP:FB PPU:226,154 CYC:17587 $EA8E:20 37 FA JSR $FA37 A:40 X:FF Y:A1 P:64 SP:FB PPU:232,154 CYC:17589 $FA37:50 2C BVC $FA65 A:40 X:FF Y:A1 P:64 SP:F9 PPU:250,154 CYC:17595 $FA39:B0 2A BCS $FA65 A:40 X:FF Y:A1 P:64 SP:F9 PPU:256,154 CYC:17597 $FA3B:30 28 BMI $FA65 A:40 X:FF Y:A1 P:64 SP:F9 PPU:262,154 CYC:17599 $FA3D:C9 40 CMP #$40 A:40 X:FF Y:A1 P:64 SP:F9 PPU:268,154 CYC:17601 $FA3F:D0 24 BNE $FA65 A:40 X:FF Y:A1 P:67 SP:F9 PPU:274,154 CYC:17603 $FA41:60 RTS A:40 X:FF Y:A1 P:67 SP:F9 PPU:280,154 CYC:17605 $EA91:A5 47 LDA $47 = #$EA A:40 X:FF Y:A1 P:67 SP:FB PPU:298,154 CYC:17611 $EA93:C9 EA CMP #$EA A:EA X:FF Y:A1 P:E5 SP:FB PPU:307,154 CYC:17614 $EA95:F0 02 BEQ $EA99 A:EA X:FF Y:A1 P:67 SP:FB PPU:313,154 CYC:17616 $EA99:C8 INY A:EA X:FF Y:A1 P:67 SP:FB PPU:322,154 CYC:17619 $EA9A:A9 00 LDA #$00 A:EA X:FF Y:A2 P:E5 SP:FB PPU:328,154 CYC:17621 $EA9C:85 47 STA $47 = #$EA A:00 X:FF Y:A2 P:67 SP:FB PPU:334,154 CYC:17623 $EA9E:20 42 FA JSR $FA42 A:00 X:FF Y:A2 P:67 SP:FB PPU: 2,155 CYC:17626 $FA42:B8 CLV A:00 X:FF Y:A2 P:67 SP:F9 PPU: 20,155 CYC:17632 $FA43:38 SEC A:00 X:FF Y:A2 P:nvUbdIZC SP:F9 PPU: 26,155 CYC:17634 $FA44:A9 FF LDA #$FF A:00 X:FF Y:A2 P:nvUbdIZC SP:F9 PPU: 32,155 CYC:17636 $FA46:60 RTS A:FF X:FF Y:A2 P:A5 SP:F9 PPU: 38,155 CYC:17638 $EAA1:D7 48 *DCP $48,X @ 47 = #$00 A:FF X:FF Y:A2 P:A5 SP:FB PPU: 56,155 CYC:17644 $EAA3:EA NOP A:FF X:FF Y:A2 P:nvUbdIZC SP:FB PPU: 74,155 CYC:17650 $EAA4:EA NOP A:FF X:FF Y:A2 P:nvUbdIZC SP:FB PPU: 80,155 CYC:17652 $EAA5:EA NOP A:FF X:FF Y:A2 P:nvUbdIZC SP:FB PPU: 86,155 CYC:17654 $EAA6:EA NOP A:FF X:FF Y:A2 P:nvUbdIZC SP:FB PPU: 92,155 CYC:17656 $EAA7:20 47 FA JSR $FA47 A:FF X:FF Y:A2 P:nvUbdIZC SP:FB PPU: 98,155 CYC:17658 $FA47:70 1C BVS $FA65 A:FF X:FF Y:A2 P:nvUbdIZC SP:F9 PPU:116,155 CYC:17664 $FA49:D0 1A BNE $FA65 A:FF X:FF Y:A2 P:nvUbdIZC SP:F9 PPU:122,155 CYC:17666 $FA4B:30 18 BMI $FA65 A:FF X:FF Y:A2 P:nvUbdIZC SP:F9 PPU:128,155 CYC:17668 $FA4D:90 16 BCC $FA65 A:FF X:FF Y:A2 P:nvUbdIZC SP:F9 PPU:134,155 CYC:17670 $FA4F:C9 FF CMP #$FF A:FF X:FF Y:A2 P:nvUbdIZC SP:F9 PPU:140,155 CYC:17672 $FA51:D0 12 BNE $FA65 A:FF X:FF Y:A2 P:nvUbdIZC SP:F9 PPU:146,155 CYC:17674 $FA53:60 RTS A:FF X:FF Y:A2 P:nvUbdIZC SP:F9 PPU:152,155 CYC:17676 $EAAA:A5 47 LDA $47 = #$FF A:FF X:FF Y:A2 P:nvUbdIZC SP:FB PPU:170,155 CYC:17682 $EAAC:C9 FF CMP #$FF A:FF X:FF Y:A2 P:A5 SP:FB PPU:179,155 CYC:17685 $EAAE:F0 02 BEQ $EAB2 A:FF X:FF Y:A2 P:nvUbdIZC SP:FB PPU:185,155 CYC:17687 $EAB2:C8 INY A:FF X:FF Y:A2 P:nvUbdIZC SP:FB PPU:194,155 CYC:17690 $EAB3:A9 37 LDA #$37 A:FF X:FF Y:A3 P:A5 SP:FB PPU:200,155 CYC:17692 $EAB5:85 47 STA $47 = #$FF A:37 X:FF Y:A3 P:25 SP:FB PPU:206,155 CYC:17694 $EAB7:20 54 FA JSR $FA54 A:37 X:FF Y:A3 P:25 SP:FB PPU:215,155 CYC:17697 $FA54:24 01 BIT $01 = #$FF A:37 X:FF Y:A3 P:25 SP:F9 PPU:233,155 CYC:17703 $FA56:A9 F0 LDA #$F0 A:37 X:FF Y:A3 P:E5 SP:F9 PPU:242,155 CYC:17706 $FA58:60 RTS A:F0 X:FF Y:A3 P:E5 SP:F9 PPU:248,155 CYC:17708 $EABA:D7 48 *DCP $48,X @ 47 = #$37 A:F0 X:FF Y:A3 P:E5 SP:FB PPU:266,155 CYC:17714 $EABC:EA NOP A:F0 X:FF Y:A3 P:E5 SP:FB PPU:284,155 CYC:17720 $EABD:EA NOP A:F0 X:FF Y:A3 P:E5 SP:FB PPU:290,155 CYC:17722 $EABE:EA NOP A:F0 X:FF Y:A3 P:E5 SP:FB PPU:296,155 CYC:17724 $EABF:EA NOP A:F0 X:FF Y:A3 P:E5 SP:FB PPU:302,155 CYC:17726 $EAC0:20 59 FA JSR $FA59 A:F0 X:FF Y:A3 P:E5 SP:FB PPU:308,155 CYC:17728 $FA59:50 0A BVC $FA65 A:F0 X:FF Y:A3 P:E5 SP:F9 PPU:326,155 CYC:17734 $FA5B:F0 08 BEQ $FA65 A:F0 X:FF Y:A3 P:E5 SP:F9 PPU:332,155 CYC:17736 $FA5D:10 06 BPL $FA65 A:F0 X:FF Y:A3 P:E5 SP:F9 PPU:338,155 CYC:17738 $FA5F:90 04 BCC $FA65 A:F0 X:FF Y:A3 P:E5 SP:F9 PPU: 3,156 CYC:17740 $FA61:C9 F0 CMP #$F0 A:F0 X:FF Y:A3 P:E5 SP:F9 PPU: 9,156 CYC:17742 $FA63:F0 02 BEQ $FA67 A:F0 X:FF Y:A3 P:67 SP:F9 PPU: 15,156 CYC:17744 $FA67:60 RTS A:F0 X:FF Y:A3 P:67 SP:F9 PPU: 24,156 CYC:17747 $EAC3:A5 47 LDA $47 = #$36 A:F0 X:FF Y:A3 P:67 SP:FB PPU: 42,156 CYC:17753 $EAC5:C9 36 CMP #$36 A:36 X:FF Y:A3 P:65 SP:FB PPU: 51,156 CYC:17756 $EAC7:F0 02 BEQ $EACB A:36 X:FF Y:A3 P:67 SP:FB PPU: 57,156 CYC:17758 $EACB:A9 EB LDA #$EB A:36 X:FF Y:A3 P:67 SP:FB PPU: 66,156 CYC:17761 $EACD:8D 47 06 STA $0647 = #$36 A:EB X:FF Y:A3 P:E5 SP:FB PPU: 72,156 CYC:17763 $EAD0:A0 FF LDY #$FF A:EB X:FF Y:A3 P:E5 SP:FB PPU: 84,156 CYC:17767 $EAD2:20 31 FA JSR $FA31 A:EB X:FF Y:FF P:E5 SP:FB PPU: 90,156 CYC:17769 $FA31:24 01 BIT $01 = #$FF A:EB X:FF Y:FF P:E5 SP:F9 PPU:108,156 CYC:17775 $FA33:18 CLC A:EB X:FF Y:FF P:E5 SP:F9 PPU:117,156 CYC:17778 $FA34:A9 40 LDA #$40 A:EB X:FF Y:FF P:NVUbdIzc SP:F9 PPU:123,156 CYC:17780 $FA36:60 RTS A:40 X:FF Y:FF P:64 SP:F9 PPU:129,156 CYC:17782 $EAD5:DB 48 05 *DCP $0548,Y @ 0647 = #$EB A:40 X:FF Y:FF P:64 SP:FB PPU:147,156 CYC:17788 $EAD8:EA NOP A:40 X:FF Y:FF P:64 SP:FB PPU:168,156 CYC:17795 $EAD9:EA NOP A:40 X:FF Y:FF P:64 SP:FB PPU:174,156 CYC:17797 $EADA:08 PHP A:40 X:FF Y:FF P:64 SP:FB PPU:180,156 CYC:17799 $EADB:48 PHA A:40 X:FF Y:FF P:64 SP:FA PPU:189,156 CYC:17802 $EADC:A0 A4 LDY #$A4 A:40 X:FF Y:FF P:64 SP:F9 PPU:198,156 CYC:17805 $EADE:68 PLA A:40 X:FF Y:A4 P:NVUbdIzc SP:F9 PPU:204,156 CYC:17807 $EADF:28 PLP A:40 X:FF Y:A4 P:64 SP:FA PPU:216,156 CYC:17811 $EAE0:20 37 FA JSR $FA37 A:40 X:FF Y:A4 P:64 SP:FB PPU:228,156 CYC:17815 $FA37:50 2C BVC $FA65 A:40 X:FF Y:A4 P:64 SP:F9 PPU:246,156 CYC:17821 $FA39:B0 2A BCS $FA65 A:40 X:FF Y:A4 P:64 SP:F9 PPU:252,156 CYC:17823 $FA3B:30 28 BMI $FA65 A:40 X:FF Y:A4 P:64 SP:F9 PPU:258,156 CYC:17825 $FA3D:C9 40 CMP #$40 A:40 X:FF Y:A4 P:64 SP:F9 PPU:264,156 CYC:17827 $FA3F:D0 24 BNE $FA65 A:40 X:FF Y:A4 P:67 SP:F9 PPU:270,156 CYC:17829 $FA41:60 RTS A:40 X:FF Y:A4 P:67 SP:F9 PPU:276,156 CYC:17831 $EAE3:AD 47 06 LDA $0647 = #$EA A:40 X:FF Y:A4 P:67 SP:FB PPU:294,156 CYC:17837 $EAE6:C9 EA CMP #$EA A:EA X:FF Y:A4 P:E5 SP:FB PPU:306,156 CYC:17841 $EAE8:F0 02 BEQ $EAEC A:EA X:FF Y:A4 P:67 SP:FB PPU:312,156 CYC:17843 $EAEC:A0 FF LDY #$FF A:EA X:FF Y:A4 P:67 SP:FB PPU:321,156 CYC:17846 $EAEE:A9 00 LDA #$00 A:EA X:FF Y:FF P:E5 SP:FB PPU:327,156 CYC:17848 $EAF0:8D 47 06 STA $0647 = #$EA A:00 X:FF Y:FF P:67 SP:FB PPU:333,156 CYC:17850 $EAF3:20 42 FA JSR $FA42 A:00 X:FF Y:FF P:67 SP:FB PPU: 4,157 CYC:17854 $FA42:B8 CLV A:00 X:FF Y:FF P:67 SP:F9 PPU: 22,157 CYC:17860 $FA43:38 SEC A:00 X:FF Y:FF P:nvUbdIZC SP:F9 PPU: 28,157 CYC:17862 $FA44:A9 FF LDA #$FF A:00 X:FF Y:FF P:nvUbdIZC SP:F9 PPU: 34,157 CYC:17864 $FA46:60 RTS A:FF X:FF Y:FF P:A5 SP:F9 PPU: 40,157 CYC:17866 $EAF6:DB 48 05 *DCP $0548,Y @ 0647 = #$00 A:FF X:FF Y:FF P:A5 SP:FB PPU: 58,157 CYC:17872 $EAF9:EA NOP A:FF X:FF Y:FF P:nvUbdIZC SP:FB PPU: 79,157 CYC:17879 $EAFA:EA NOP A:FF X:FF Y:FF P:nvUbdIZC SP:FB PPU: 85,157 CYC:17881 $EAFB:08 PHP A:FF X:FF Y:FF P:nvUbdIZC SP:FB PPU: 91,157 CYC:17883 $EAFC:48 PHA A:FF X:FF Y:FF P:nvUbdIZC SP:FA PPU:100,157 CYC:17886 $EAFD:A0 A5 LDY #$A5 A:FF X:FF Y:FF P:nvUbdIZC SP:F9 PPU:109,157 CYC:17889 $EAFF:68 PLA A:FF X:FF Y:A5 P:A5 SP:F9 PPU:115,157 CYC:17891 $EB00:28 PLP A:FF X:FF Y:A5 P:A5 SP:FA PPU:127,157 CYC:17895 $EB01:20 47 FA JSR $FA47 A:FF X:FF Y:A5 P:nvUbdIZC SP:FB PPU:139,157 CYC:17899 $FA47:70 1C BVS $FA65 A:FF X:FF Y:A5 P:nvUbdIZC SP:F9 PPU:157,157 CYC:17905 $FA49:D0 1A BNE $FA65 A:FF X:FF Y:A5 P:nvUbdIZC SP:F9 PPU:163,157 CYC:17907 $FA4B:30 18 BMI $FA65 A:FF X:FF Y:A5 P:nvUbdIZC SP:F9 PPU:169,157 CYC:17909 $FA4D:90 16 BCC $FA65 A:FF X:FF Y:A5 P:nvUbdIZC SP:F9 PPU:175,157 CYC:17911 $FA4F:C9 FF CMP #$FF A:FF X:FF Y:A5 P:nvUbdIZC SP:F9 PPU:181,157 CYC:17913 $FA51:D0 12 BNE $FA65 A:FF X:FF Y:A5 P:nvUbdIZC SP:F9 PPU:187,157 CYC:17915 $FA53:60 RTS A:FF X:FF Y:A5 P:nvUbdIZC SP:F9 PPU:193,157 CYC:17917 $EB04:AD 47 06 LDA $0647 = #$FF A:FF X:FF Y:A5 P:nvUbdIZC SP:FB PPU:211,157 CYC:17923 $EB07:C9 FF CMP #$FF A:FF X:FF Y:A5 P:A5 SP:FB PPU:223,157 CYC:17927 $EB09:F0 02 BEQ $EB0D A:FF X:FF Y:A5 P:nvUbdIZC SP:FB PPU:229,157 CYC:17929 $EB0D:A0 FF LDY #$FF A:FF X:FF Y:A5 P:nvUbdIZC SP:FB PPU:238,157 CYC:17932 $EB0F:A9 37 LDA #$37 A:FF X:FF Y:FF P:A5 SP:FB PPU:244,157 CYC:17934 $EB11:8D 47 06 STA $0647 = #$FF A:37 X:FF Y:FF P:25 SP:FB PPU:250,157 CYC:17936 $EB14:20 54 FA JSR $FA54 A:37 X:FF Y:FF P:25 SP:FB PPU:262,157 CYC:17940 $FA54:24 01 BIT $01 = #$FF A:37 X:FF Y:FF P:25 SP:F9 PPU:280,157 CYC:17946 $FA56:A9 F0 LDA #$F0 A:37 X:FF Y:FF P:E5 SP:F9 PPU:289,157 CYC:17949 $FA58:60 RTS A:F0 X:FF Y:FF P:E5 SP:F9 PPU:295,157 CYC:17951 $EB17:DB 48 05 *DCP $0548,Y @ 0647 = #$37 A:F0 X:FF Y:FF P:E5 SP:FB PPU:313,157 CYC:17957 $EB1A:EA NOP A:F0 X:FF Y:FF P:E5 SP:FB PPU:334,157 CYC:17964 $EB1B:EA NOP A:F0 X:FF Y:FF P:E5 SP:FB PPU:340,157 CYC:17966 $EB1C:08 PHP A:F0 X:FF Y:FF P:E5 SP:FB PPU: 5,158 CYC:17968 $EB1D:48 PHA A:F0 X:FF Y:FF P:E5 SP:FA PPU: 14,158 CYC:17971 $EB1E:A0 A6 LDY #$A6 A:F0 X:FF Y:FF P:E5 SP:F9 PPU: 23,158 CYC:17974 $EB20:68 PLA A:F0 X:FF Y:A6 P:E5 SP:F9 PPU: 29,158 CYC:17976 $EB21:28 PLP A:F0 X:FF Y:A6 P:E5 SP:FA PPU: 41,158 CYC:17980 $EB22:20 59 FA JSR $FA59 A:F0 X:FF Y:A6 P:E5 SP:FB PPU: 53,158 CYC:17984 $FA59:50 0A BVC $FA65 A:F0 X:FF Y:A6 P:E5 SP:F9 PPU: 71,158 CYC:17990 $FA5B:F0 08 BEQ $FA65 A:F0 X:FF Y:A6 P:E5 SP:F9 PPU: 77,158 CYC:17992 $FA5D:10 06 BPL $FA65 A:F0 X:FF Y:A6 P:E5 SP:F9 PPU: 83,158 CYC:17994 $FA5F:90 04 BCC $FA65 A:F0 X:FF Y:A6 P:E5 SP:F9 PPU: 89,158 CYC:17996 $FA61:C9 F0 CMP #$F0 A:F0 X:FF Y:A6 P:E5 SP:F9 PPU: 95,158 CYC:17998 $FA63:F0 02 BEQ $FA67 A:F0 X:FF Y:A6 P:67 SP:F9 PPU:101,158 CYC:18000 $FA67:60 RTS A:F0 X:FF Y:A6 P:67 SP:F9 PPU:110,158 CYC:18003 $EB25:AD 47 06 LDA $0647 = #$36 A:F0 X:FF Y:A6 P:67 SP:FB PPU:128,158 CYC:18009 $EB28:C9 36 CMP #$36 A:36 X:FF Y:A6 P:65 SP:FB PPU:140,158 CYC:18013 $EB2A:F0 02 BEQ $EB2E A:36 X:FF Y:A6 P:67 SP:FB PPU:146,158 CYC:18015 $EB2E:A0 A7 LDY #$A7 A:36 X:FF Y:A6 P:67 SP:FB PPU:155,158 CYC:18018 $EB30:A2 FF LDX #$FF A:36 X:FF Y:A7 P:E5 SP:FB PPU:161,158 CYC:18020 $EB32:A9 EB LDA #$EB A:36 X:FF Y:A7 P:E5 SP:FB PPU:167,158 CYC:18022 $EB34:8D 47 06 STA $0647 = #$36 A:EB X:FF Y:A7 P:E5 SP:FB PPU:173,158 CYC:18024 $EB37:20 31 FA JSR $FA31 A:EB X:FF Y:A7 P:E5 SP:FB PPU:185,158 CYC:18028 $FA31:24 01 BIT $01 = #$FF A:EB X:FF Y:A7 P:E5 SP:F9 PPU:203,158 CYC:18034 $FA33:18 CLC A:EB X:FF Y:A7 P:E5 SP:F9 PPU:212,158 CYC:18037 $FA34:A9 40 LDA #$40 A:EB X:FF Y:A7 P:NVUbdIzc SP:F9 PPU:218,158 CYC:18039 $FA36:60 RTS A:40 X:FF Y:A7 P:64 SP:F9 PPU:224,158 CYC:18041 $EB3A:DF 48 05 *DCP $0548,X @ 0647 = #$EB A:40 X:FF Y:A7 P:64 SP:FB PPU:242,158 CYC:18047 $EB3D:EA NOP A:40 X:FF Y:A7 P:64 SP:FB PPU:263,158 CYC:18054 $EB3E:EA NOP A:40 X:FF Y:A7 P:64 SP:FB PPU:269,158 CYC:18056 $EB3F:EA NOP A:40 X:FF Y:A7 P:64 SP:FB PPU:275,158 CYC:18058 $EB40:EA NOP A:40 X:FF Y:A7 P:64 SP:FB PPU:281,158 CYC:18060 $EB41:20 37 FA JSR $FA37 A:40 X:FF Y:A7 P:64 SP:FB PPU:287,158 CYC:18062 $FA37:50 2C BVC $FA65 A:40 X:FF Y:A7 P:64 SP:F9 PPU:305,158 CYC:18068 $FA39:B0 2A BCS $FA65 A:40 X:FF Y:A7 P:64 SP:F9 PPU:311,158 CYC:18070 $FA3B:30 28 BMI $FA65 A:40 X:FF Y:A7 P:64 SP:F9 PPU:317,158 CYC:18072 $FA3D:C9 40 CMP #$40 A:40 X:FF Y:A7 P:64 SP:F9 PPU:323,158 CYC:18074 $FA3F:D0 24 BNE $FA65 A:40 X:FF Y:A7 P:67 SP:F9 PPU:329,158 CYC:18076 $FA41:60 RTS A:40 X:FF Y:A7 P:67 SP:F9 PPU:335,158 CYC:18078 $EB44:AD 47 06 LDA $0647 = #$EA A:40 X:FF Y:A7 P:67 SP:FB PPU: 12,159 CYC:18084 $EB47:C9 EA CMP #$EA A:EA X:FF Y:A7 P:E5 SP:FB PPU: 24,159 CYC:18088 $EB49:F0 02 BEQ $EB4D A:EA X:FF Y:A7 P:67 SP:FB PPU: 30,159 CYC:18090 $EB4D:C8 INY A:EA X:FF Y:A7 P:67 SP:FB PPU: 39,159 CYC:18093 $EB4E:A9 00 LDA #$00 A:EA X:FF Y:A8 P:E5 SP:FB PPU: 45,159 CYC:18095 $EB50:8D 47 06 STA $0647 = #$EA A:00 X:FF Y:A8 P:67 SP:FB PPU: 51,159 CYC:18097 $EB53:20 42 FA JSR $FA42 A:00 X:FF Y:A8 P:67 SP:FB PPU: 63,159 CYC:18101 $FA42:B8 CLV A:00 X:FF Y:A8 P:67 SP:F9 PPU: 81,159 CYC:18107 $FA43:38 SEC A:00 X:FF Y:A8 P:nvUbdIZC SP:F9 PPU: 87,159 CYC:18109 $FA44:A9 FF LDA #$FF A:00 X:FF Y:A8 P:nvUbdIZC SP:F9 PPU: 93,159 CYC:18111 $FA46:60 RTS A:FF X:FF Y:A8 P:A5 SP:F9 PPU: 99,159 CYC:18113 $EB56:DF 48 05 *DCP $0548,X @ 0647 = #$00 A:FF X:FF Y:A8 P:A5 SP:FB PPU:117,159 CYC:18119 $EB59:EA NOP A:FF X:FF Y:A8 P:nvUbdIZC SP:FB PPU:138,159 CYC:18126 $EB5A:EA NOP A:FF X:FF Y:A8 P:nvUbdIZC SP:FB PPU:144,159 CYC:18128 $EB5B:EA NOP A:FF X:FF Y:A8 P:nvUbdIZC SP:FB PPU:150,159 CYC:18130 $EB5C:EA NOP A:FF X:FF Y:A8 P:nvUbdIZC SP:FB PPU:156,159 CYC:18132 $EB5D:20 47 FA JSR $FA47 A:FF X:FF Y:A8 P:nvUbdIZC SP:FB PPU:162,159 CYC:18134 $FA47:70 1C BVS $FA65 A:FF X:FF Y:A8 P:nvUbdIZC SP:F9 PPU:180,159 CYC:18140 $FA49:D0 1A BNE $FA65 A:FF X:FF Y:A8 P:nvUbdIZC SP:F9 PPU:186,159 CYC:18142 $FA4B:30 18 BMI $FA65 A:FF X:FF Y:A8 P:nvUbdIZC SP:F9 PPU:192,159 CYC:18144 $FA4D:90 16 BCC $FA65 A:FF X:FF Y:A8 P:nvUbdIZC SP:F9 PPU:198,159 CYC:18146 $FA4F:C9 FF CMP #$FF A:FF X:FF Y:A8 P:nvUbdIZC SP:F9 PPU:204,159 CYC:18148 $FA51:D0 12 BNE $FA65 A:FF X:FF Y:A8 P:nvUbdIZC SP:F9 PPU:210,159 CYC:18150 $FA53:60 RTS A:FF X:FF Y:A8 P:nvUbdIZC SP:F9 PPU:216,159 CYC:18152 $EB60:AD 47 06 LDA $0647 = #$FF A:FF X:FF Y:A8 P:nvUbdIZC SP:FB PPU:234,159 CYC:18158 $EB63:C9 FF CMP #$FF A:FF X:FF Y:A8 P:A5 SP:FB PPU:246,159 CYC:18162 $EB65:F0 02 BEQ $EB69 A:FF X:FF Y:A8 P:nvUbdIZC SP:FB PPU:252,159 CYC:18164 $EB69:C8 INY A:FF X:FF Y:A8 P:nvUbdIZC SP:FB PPU:261,159 CYC:18167 $EB6A:A9 37 LDA #$37 A:FF X:FF Y:A9 P:A5 SP:FB PPU:267,159 CYC:18169 $EB6C:8D 47 06 STA $0647 = #$FF A:37 X:FF Y:A9 P:25 SP:FB PPU:273,159 CYC:18171 $EB6F:20 54 FA JSR $FA54 A:37 X:FF Y:A9 P:25 SP:FB PPU:285,159 CYC:18175 $FA54:24 01 BIT $01 = #$FF A:37 X:FF Y:A9 P:25 SP:F9 PPU:303,159 CYC:18181 $FA56:A9 F0 LDA #$F0 A:37 X:FF Y:A9 P:E5 SP:F9 PPU:312,159 CYC:18184 $FA58:60 RTS A:F0 X:FF Y:A9 P:E5 SP:F9 PPU:318,159 CYC:18186 $EB72:DF 48 05 *DCP $0548,X @ 0647 = #$37 A:F0 X:FF Y:A9 P:E5 SP:FB PPU:336,159 CYC:18192 $EB75:EA NOP A:F0 X:FF Y:A9 P:E5 SP:FB PPU: 16,160 CYC:18199 $EB76:EA NOP A:F0 X:FF Y:A9 P:E5 SP:FB PPU: 22,160 CYC:18201 $EB77:EA NOP A:F0 X:FF Y:A9 P:E5 SP:FB PPU: 28,160 CYC:18203 $EB78:EA NOP A:F0 X:FF Y:A9 P:E5 SP:FB PPU: 34,160 CYC:18205 $EB79:20 59 FA JSR $FA59 A:F0 X:FF Y:A9 P:E5 SP:FB PPU: 40,160 CYC:18207 $FA59:50 0A BVC $FA65 A:F0 X:FF Y:A9 P:E5 SP:F9 PPU: 58,160 CYC:18213 $FA5B:F0 08 BEQ $FA65 A:F0 X:FF Y:A9 P:E5 SP:F9 PPU: 64,160 CYC:18215 $FA5D:10 06 BPL $FA65 A:F0 X:FF Y:A9 P:E5 SP:F9 PPU: 70,160 CYC:18217 $FA5F:90 04 BCC $FA65 A:F0 X:FF Y:A9 P:E5 SP:F9 PPU: 76,160 CYC:18219 $FA61:C9 F0 CMP #$F0 A:F0 X:FF Y:A9 P:E5 SP:F9 PPU: 82,160 CYC:18221 $FA63:F0 02 BEQ $FA67 A:F0 X:FF Y:A9 P:67 SP:F9 PPU: 88,160 CYC:18223 $FA67:60 RTS A:F0 X:FF Y:A9 P:67 SP:F9 PPU: 97,160 CYC:18226 $EB7C:AD 47 06 LDA $0647 = #$36 A:F0 X:FF Y:A9 P:67 SP:FB PPU:115,160 CYC:18232 $EB7F:C9 36 CMP #$36 A:36 X:FF Y:A9 P:65 SP:FB PPU:127,160 CYC:18236 $EB81:F0 02 BEQ $EB85 A:36 X:FF Y:A9 P:67 SP:FB PPU:133,160 CYC:18238 $EB85:60 RTS A:36 X:FF Y:A9 P:67 SP:FB PPU:142,160 CYC:18241 $C63E:20 86 EB JSR $EB86 A:36 X:FF Y:A9 P:67 SP:FD PPU:160,160 CYC:18247 $EB86:A9 FF LDA #$FF A:36 X:FF Y:A9 P:67 SP:FB PPU:178,160 CYC:18253 $EB88:85 01 STA $01 = #$FF A:FF X:FF Y:A9 P:E5 SP:FB PPU:184,160 CYC:18255 $EB8A:A0 AA LDY #$AA A:FF X:FF Y:A9 P:E5 SP:FB PPU:193,160 CYC:18258 $EB8C:A2 02 LDX #$02 A:FF X:FF Y:AA P:E5 SP:FB PPU:199,160 CYC:18260 $EB8E:A9 47 LDA #$47 A:FF X:02 Y:AA P:65 SP:FB PPU:205,160 CYC:18262 $EB90:85 47 STA $47 = #$36 A:47 X:02 Y:AA P:65 SP:FB PPU:211,160 CYC:18264 $EB92:A9 06 LDA #$06 A:47 X:02 Y:AA P:65 SP:FB PPU:220,160 CYC:18267 $EB94:85 48 STA $48 = #$06 A:06 X:02 Y:AA P:65 SP:FB PPU:226,160 CYC:18269 $EB96:A9 EB LDA #$EB A:06 X:02 Y:AA P:65 SP:FB PPU:235,160 CYC:18272 $EB98:8D 47 06 STA $0647 = #$36 A:EB X:02 Y:AA P:E5 SP:FB PPU:241,160 CYC:18274 $EB9B:20 B1 FA JSR $FAB1 A:EB X:02 Y:AA P:E5 SP:FB PPU:253,160 CYC:18278 $FAB1:24 01 BIT $01 = #$FF A:EB X:02 Y:AA P:E5 SP:F9 PPU:271,160 CYC:18284 $FAB3:18 CLC A:EB X:02 Y:AA P:E5 SP:F9 PPU:280,160 CYC:18287 $FAB4:A9 40 LDA #$40 A:EB X:02 Y:AA P:NVUbdIzc SP:F9 PPU:286,160 CYC:18289 $FAB6:60 RTS A:40 X:02 Y:AA P:64 SP:F9 PPU:292,160 CYC:18291 $EB9E:E3 45 *ISB ($45,X) @ 47 = #$0647 = EB A:40 X:02 Y:AA P:64 SP:FB PPU:310,160 CYC:18297 $EBA0:EA NOP A:53 X:02 Y:AA P:nvUbdIzc SP:FB PPU:334,160 CYC:18305 $EBA1:EA NOP A:53 X:02 Y:AA P:nvUbdIzc SP:FB PPU:340,160 CYC:18307 $EBA2:EA NOP A:53 X:02 Y:AA P:nvUbdIzc SP:FB PPU: 5,161 CYC:18309 $EBA3:EA NOP A:53 X:02 Y:AA P:nvUbdIzc SP:FB PPU: 11,161 CYC:18311 $EBA4:20 B7 FA JSR $FAB7 A:53 X:02 Y:AA P:nvUbdIzc SP:FB PPU: 17,161 CYC:18313 $FAB7:70 2D BVS $FAE6 A:53 X:02 Y:AA P:nvUbdIzc SP:F9 PPU: 35,161 CYC:18319 $FAB9:B0 2B BCS $FAE6 A:53 X:02 Y:AA P:nvUbdIzc SP:F9 PPU: 41,161 CYC:18321 $FABB:30 29 BMI $FAE6 A:53 X:02 Y:AA P:nvUbdIzc SP:F9 PPU: 47,161 CYC:18323 $FABD:C9 53 CMP #$53 A:53 X:02 Y:AA P:nvUbdIzc SP:F9 PPU: 53,161 CYC:18325 $FABF:D0 25 BNE $FAE6 A:53 X:02 Y:AA P:nvUbdIZC SP:F9 PPU: 59,161 CYC:18327 $FAC1:60 RTS A:53 X:02 Y:AA P:nvUbdIZC SP:F9 PPU: 65,161 CYC:18329 $EBA7:AD 47 06 LDA $0647 = #$EC A:53 X:02 Y:AA P:nvUbdIZC SP:FB PPU: 83,161 CYC:18335 $EBAA:C9 EC CMP #$EC A:EC X:02 Y:AA P:A5 SP:FB PPU: 95,161 CYC:18339 $EBAC:F0 02 BEQ $EBB0 A:EC X:02 Y:AA P:nvUbdIZC SP:FB PPU:101,161 CYC:18341 $EBB0:C8 INY A:EC X:02 Y:AA P:nvUbdIZC SP:FB PPU:110,161 CYC:18344 $EBB1:A9 FF LDA #$FF A:EC X:02 Y:AB P:A5 SP:FB PPU:116,161 CYC:18346 $EBB3:8D 47 06 STA $0647 = #$EC A:FF X:02 Y:AB P:A5 SP:FB PPU:122,161 CYC:18348 $EBB6:20 C2 FA JSR $FAC2 A:FF X:02 Y:AB P:A5 SP:FB PPU:134,161 CYC:18352 $FAC2:B8 CLV A:FF X:02 Y:AB P:A5 SP:F9 PPU:152,161 CYC:18358 $FAC3:38 SEC A:FF X:02 Y:AB P:A5 SP:F9 PPU:158,161 CYC:18360 $FAC4:A9 FF LDA #$FF A:FF X:02 Y:AB P:A5 SP:F9 PPU:164,161 CYC:18362 $FAC6:60 RTS A:FF X:02 Y:AB P:A5 SP:F9 PPU:170,161 CYC:18364 $EBB9:E3 45 *ISB ($45,X) @ 47 = #$0647 = FF A:FF X:02 Y:AB P:A5 SP:FB PPU:188,161 CYC:18370 $EBBB:EA NOP A:FF X:02 Y:AB P:A5 SP:FB PPU:212,161 CYC:18378 $EBBC:EA NOP A:FF X:02 Y:AB P:A5 SP:FB PPU:218,161 CYC:18380 $EBBD:EA NOP A:FF X:02 Y:AB P:A5 SP:FB PPU:224,161 CYC:18382 $EBBE:EA NOP A:FF X:02 Y:AB P:A5 SP:FB PPU:230,161 CYC:18384 $EBBF:20 C7 FA JSR $FAC7 A:FF X:02 Y:AB P:A5 SP:FB PPU:236,161 CYC:18386 $FAC7:70 1D BVS $FAE6 A:FF X:02 Y:AB P:A5 SP:F9 PPU:254,161 CYC:18392 $FAC9:F0 1B BEQ $FAE6 A:FF X:02 Y:AB P:A5 SP:F9 PPU:260,161 CYC:18394 $FACB:10 19 BPL $FAE6 A:FF X:02 Y:AB P:A5 SP:F9 PPU:266,161 CYC:18396 $FACD:90 17 BCC $FAE6 A:FF X:02 Y:AB P:A5 SP:F9 PPU:272,161 CYC:18398 $FACF:C9 FF CMP #$FF A:FF X:02 Y:AB P:A5 SP:F9 PPU:278,161 CYC:18400 $FAD1:D0 13 BNE $FAE6 A:FF X:02 Y:AB P:nvUbdIZC SP:F9 PPU:284,161 CYC:18402 $FAD3:60 RTS A:FF X:02 Y:AB P:nvUbdIZC SP:F9 PPU:290,161 CYC:18404 $EBC2:AD 47 06 LDA $0647 = #$00 A:FF X:02 Y:AB P:nvUbdIZC SP:FB PPU:308,161 CYC:18410 $EBC5:C9 00 CMP #$00 A:00 X:02 Y:AB P:nvUbdIZC SP:FB PPU:320,161 CYC:18414 $EBC7:F0 02 BEQ $EBCB A:00 X:02 Y:AB P:nvUbdIZC SP:FB PPU:326,161 CYC:18416 $EBCB:C8 INY A:00 X:02 Y:AB P:nvUbdIZC SP:FB PPU:335,161 CYC:18419 $EBCC:A9 37 LDA #$37 A:00 X:02 Y:AC P:A5 SP:FB PPU: 0,162 CYC:18421 $EBCE:8D 47 06 STA $0647 = #$00 A:37 X:02 Y:AC P:25 SP:FB PPU: 6,162 CYC:18423 $EBD1:20 D4 FA JSR $FAD4 A:37 X:02 Y:AC P:25 SP:FB PPU: 18,162 CYC:18427 $FAD4:24 01 BIT $01 = #$FF A:37 X:02 Y:AC P:25 SP:F9 PPU: 36,162 CYC:18433 $FAD6:38 SEC A:37 X:02 Y:AC P:E5 SP:F9 PPU: 45,162 CYC:18436 $FAD7:A9 F0 LDA #$F0 A:37 X:02 Y:AC P:E5 SP:F9 PPU: 51,162 CYC:18438 $FAD9:60 RTS A:F0 X:02 Y:AC P:E5 SP:F9 PPU: 57,162 CYC:18440 $EBD4:E3 45 *ISB ($45,X) @ 47 = #$0647 = 37 A:F0 X:02 Y:AC P:E5 SP:FB PPU: 75,162 CYC:18446 $EBD6:EA NOP A:B8 X:02 Y:AC P:A5 SP:FB PPU: 99,162 CYC:18454 $EBD7:EA NOP A:B8 X:02 Y:AC P:A5 SP:FB PPU:105,162 CYC:18456 $EBD8:EA NOP A:B8 X:02 Y:AC P:A5 SP:FB PPU:111,162 CYC:18458 $EBD9:EA NOP A:B8 X:02 Y:AC P:A5 SP:FB PPU:117,162 CYC:18460 $EBDA:20 DA FA JSR $FADA A:B8 X:02 Y:AC P:A5 SP:FB PPU:123,162 CYC:18462 $FADA:70 0A BVS $FAE6 A:B8 X:02 Y:AC P:A5 SP:F9 PPU:141,162 CYC:18468 $FADC:F0 08 BEQ $FAE6 A:B8 X:02 Y:AC P:A5 SP:F9 PPU:147,162 CYC:18470 $FADE:10 06 BPL $FAE6 A:B8 X:02 Y:AC P:A5 SP:F9 PPU:153,162 CYC:18472 $FAE0:90 04 BCC $FAE6 A:B8 X:02 Y:AC P:A5 SP:F9 PPU:159,162 CYC:18474 $FAE2:C9 B8 CMP #$B8 A:B8 X:02 Y:AC P:A5 SP:F9 PPU:165,162 CYC:18476 $FAE4:F0 02 BEQ $FAE8 A:B8 X:02 Y:AC P:nvUbdIZC SP:F9 PPU:171,162 CYC:18478 $FAE8:60 RTS A:B8 X:02 Y:AC P:nvUbdIZC SP:F9 PPU:180,162 CYC:18481 $EBDD:AD 47 06 LDA $0647 = #$38 A:B8 X:02 Y:AC P:nvUbdIZC SP:FB PPU:198,162 CYC:18487 $EBE0:C9 38 CMP #$38 A:38 X:02 Y:AC P:25 SP:FB PPU:210,162 CYC:18491 $EBE2:F0 02 BEQ $EBE6 A:38 X:02 Y:AC P:nvUbdIZC SP:FB PPU:216,162 CYC:18493 $EBE6:C8 INY A:38 X:02 Y:AC P:nvUbdIZC SP:FB PPU:225,162 CYC:18496 $EBE7:A9 EB LDA #$EB A:38 X:02 Y:AD P:A5 SP:FB PPU:231,162 CYC:18498 $EBE9:85 47 STA $47 = #$47 A:EB X:02 Y:AD P:A5 SP:FB PPU:237,162 CYC:18500 $EBEB:20 B1 FA JSR $FAB1 A:EB X:02 Y:AD P:A5 SP:FB PPU:246,162 CYC:18503 $FAB1:24 01 BIT $01 = #$FF A:EB X:02 Y:AD P:A5 SP:F9 PPU:264,162 CYC:18509 $FAB3:18 CLC A:EB X:02 Y:AD P:E5 SP:F9 PPU:273,162 CYC:18512 $FAB4:A9 40 LDA #$40 A:EB X:02 Y:AD P:NVUbdIzc SP:F9 PPU:279,162 CYC:18514 $FAB6:60 RTS A:40 X:02 Y:AD P:64 SP:F9 PPU:285,162 CYC:18516 $EBEE:E7 47 *ISB $47 = #$EB A:40 X:02 Y:AD P:64 SP:FB PPU:303,162 CYC:18522 $EBF0:EA NOP A:53 X:02 Y:AD P:nvUbdIzc SP:FB PPU:318,162 CYC:18527 $EBF1:EA NOP A:53 X:02 Y:AD P:nvUbdIzc SP:FB PPU:324,162 CYC:18529 $EBF2:EA NOP A:53 X:02 Y:AD P:nvUbdIzc SP:FB PPU:330,162 CYC:18531 $EBF3:EA NOP A:53 X:02 Y:AD P:nvUbdIzc SP:FB PPU:336,162 CYC:18533 $EBF4:20 B7 FA JSR $FAB7 A:53 X:02 Y:AD P:nvUbdIzc SP:FB PPU: 1,163 CYC:18535 $FAB7:70 2D BVS $FAE6 A:53 X:02 Y:AD P:nvUbdIzc SP:F9 PPU: 19,163 CYC:18541 $FAB9:B0 2B BCS $FAE6 A:53 X:02 Y:AD P:nvUbdIzc SP:F9 PPU: 25,163 CYC:18543 $FABB:30 29 BMI $FAE6 A:53 X:02 Y:AD P:nvUbdIzc SP:F9 PPU: 31,163 CYC:18545 $FABD:C9 53 CMP #$53 A:53 X:02 Y:AD P:nvUbdIzc SP:F9 PPU: 37,163 CYC:18547 $FABF:D0 25 BNE $FAE6 A:53 X:02 Y:AD P:nvUbdIZC SP:F9 PPU: 43,163 CYC:18549 $FAC1:60 RTS A:53 X:02 Y:AD P:nvUbdIZC SP:F9 PPU: 49,163 CYC:18551 $EBF7:A5 47 LDA $47 = #$EC A:53 X:02 Y:AD P:nvUbdIZC SP:FB PPU: 67,163 CYC:18557 $EBF9:C9 EC CMP #$EC A:EC X:02 Y:AD P:A5 SP:FB PPU: 76,163 CYC:18560 $EBFB:F0 02 BEQ $EBFF A:EC X:02 Y:AD P:nvUbdIZC SP:FB PPU: 82,163 CYC:18562 $EBFF:C8 INY A:EC X:02 Y:AD P:nvUbdIZC SP:FB PPU: 91,163 CYC:18565 $EC00:A9 FF LDA #$FF A:EC X:02 Y:AE P:A5 SP:FB PPU: 97,163 CYC:18567 $EC02:85 47 STA $47 = #$EC A:FF X:02 Y:AE P:A5 SP:FB PPU:103,163 CYC:18569 $EC04:20 C2 FA JSR $FAC2 A:FF X:02 Y:AE P:A5 SP:FB PPU:112,163 CYC:18572 $FAC2:B8 CLV A:FF X:02 Y:AE P:A5 SP:F9 PPU:130,163 CYC:18578 $FAC3:38 SEC A:FF X:02 Y:AE P:A5 SP:F9 PPU:136,163 CYC:18580 $FAC4:A9 FF LDA #$FF A:FF X:02 Y:AE P:A5 SP:F9 PPU:142,163 CYC:18582 $FAC6:60 RTS A:FF X:02 Y:AE P:A5 SP:F9 PPU:148,163 CYC:18584 $EC07:E7 47 *ISB $47 = #$FF A:FF X:02 Y:AE P:A5 SP:FB PPU:166,163 CYC:18590 $EC09:EA NOP A:FF X:02 Y:AE P:A5 SP:FB PPU:181,163 CYC:18595 $EC0A:EA NOP A:FF X:02 Y:AE P:A5 SP:FB PPU:187,163 CYC:18597 $EC0B:EA NOP A:FF X:02 Y:AE P:A5 SP:FB PPU:193,163 CYC:18599 $EC0C:EA NOP A:FF X:02 Y:AE P:A5 SP:FB PPU:199,163 CYC:18601 $EC0D:20 C7 FA JSR $FAC7 A:FF X:02 Y:AE P:A5 SP:FB PPU:205,163 CYC:18603 $FAC7:70 1D BVS $FAE6 A:FF X:02 Y:AE P:A5 SP:F9 PPU:223,163 CYC:18609 $FAC9:F0 1B BEQ $FAE6 A:FF X:02 Y:AE P:A5 SP:F9 PPU:229,163 CYC:18611 $FACB:10 19 BPL $FAE6 A:FF X:02 Y:AE P:A5 SP:F9 PPU:235,163 CYC:18613 $FACD:90 17 BCC $FAE6 A:FF X:02 Y:AE P:A5 SP:F9 PPU:241,163 CYC:18615 $FACF:C9 FF CMP #$FF A:FF X:02 Y:AE P:A5 SP:F9 PPU:247,163 CYC:18617 $FAD1:D0 13 BNE $FAE6 A:FF X:02 Y:AE P:nvUbdIZC SP:F9 PPU:253,163 CYC:18619 $FAD3:60 RTS A:FF X:02 Y:AE P:nvUbdIZC SP:F9 PPU:259,163 CYC:18621 $EC10:A5 47 LDA $47 = #$00 A:FF X:02 Y:AE P:nvUbdIZC SP:FB PPU:277,163 CYC:18627 $EC12:C9 00 CMP #$00 A:00 X:02 Y:AE P:nvUbdIZC SP:FB PPU:286,163 CYC:18630 $EC14:F0 02 BEQ $EC18 A:00 X:02 Y:AE P:nvUbdIZC SP:FB PPU:292,163 CYC:18632 $EC18:C8 INY A:00 X:02 Y:AE P:nvUbdIZC SP:FB PPU:301,163 CYC:18635 $EC19:A9 37 LDA #$37 A:00 X:02 Y:AF P:A5 SP:FB PPU:307,163 CYC:18637 $EC1B:85 47 STA $47 = #$00 A:37 X:02 Y:AF P:25 SP:FB PPU:313,163 CYC:18639 $EC1D:20 D4 FA JSR $FAD4 A:37 X:02 Y:AF P:25 SP:FB PPU:322,163 CYC:18642 $FAD4:24 01 BIT $01 = #$FF A:37 X:02 Y:AF P:25 SP:F9 PPU:340,163 CYC:18648 $FAD6:38 SEC A:37 X:02 Y:AF P:E5 SP:F9 PPU: 8,164 CYC:18651 $FAD7:A9 F0 LDA #$F0 A:37 X:02 Y:AF P:E5 SP:F9 PPU: 14,164 CYC:18653 $FAD9:60 RTS A:F0 X:02 Y:AF P:E5 SP:F9 PPU: 20,164 CYC:18655 $EC20:E7 47 *ISB $47 = #$37 A:F0 X:02 Y:AF P:E5 SP:FB PPU: 38,164 CYC:18661 $EC22:EA NOP A:B8 X:02 Y:AF P:A5 SP:FB PPU: 53,164 CYC:18666 $EC23:EA NOP A:B8 X:02 Y:AF P:A5 SP:FB PPU: 59,164 CYC:18668 $EC24:EA NOP A:B8 X:02 Y:AF P:A5 SP:FB PPU: 65,164 CYC:18670 $EC25:EA NOP A:B8 X:02 Y:AF P:A5 SP:FB PPU: 71,164 CYC:18672 $EC26:20 DA FA JSR $FADA A:B8 X:02 Y:AF P:A5 SP:FB PPU: 77,164 CYC:18674 $FADA:70 0A BVS $FAE6 A:B8 X:02 Y:AF P:A5 SP:F9 PPU: 95,164 CYC:18680 $FADC:F0 08 BEQ $FAE6 A:B8 X:02 Y:AF P:A5 SP:F9 PPU:101,164 CYC:18682 $FADE:10 06 BPL $FAE6 A:B8 X:02 Y:AF P:A5 SP:F9 PPU:107,164 CYC:18684 $FAE0:90 04 BCC $FAE6 A:B8 X:02 Y:AF P:A5 SP:F9 PPU:113,164 CYC:18686 $FAE2:C9 B8 CMP #$B8 A:B8 X:02 Y:AF P:A5 SP:F9 PPU:119,164 CYC:18688 $FAE4:F0 02 BEQ $FAE8 A:B8 X:02 Y:AF P:nvUbdIZC SP:F9 PPU:125,164 CYC:18690 $FAE8:60 RTS A:B8 X:02 Y:AF P:nvUbdIZC SP:F9 PPU:134,164 CYC:18693 $EC29:A5 47 LDA $47 = #$38 A:B8 X:02 Y:AF P:nvUbdIZC SP:FB PPU:152,164 CYC:18699 $EC2B:C9 38 CMP #$38 A:38 X:02 Y:AF P:25 SP:FB PPU:161,164 CYC:18702 $EC2D:F0 02 BEQ $EC31 A:38 X:02 Y:AF P:nvUbdIZC SP:FB PPU:167,164 CYC:18704 $EC31:C8 INY A:38 X:02 Y:AF P:nvUbdIZC SP:FB PPU:176,164 CYC:18707 $EC32:A9 EB LDA #$EB A:38 X:02 Y:B0 P:A5 SP:FB PPU:182,164 CYC:18709 $EC34:8D 47 06 STA $0647 = #$38 A:EB X:02 Y:B0 P:A5 SP:FB PPU:188,164 CYC:18711 $EC37:20 B1 FA JSR $FAB1 A:EB X:02 Y:B0 P:A5 SP:FB PPU:200,164 CYC:18715 $FAB1:24 01 BIT $01 = #$FF A:EB X:02 Y:B0 P:A5 SP:F9 PPU:218,164 CYC:18721 $FAB3:18 CLC A:EB X:02 Y:B0 P:E5 SP:F9 PPU:227,164 CYC:18724 $FAB4:A9 40 LDA #$40 A:EB X:02 Y:B0 P:NVUbdIzc SP:F9 PPU:233,164 CYC:18726 $FAB6:60 RTS A:40 X:02 Y:B0 P:64 SP:F9 PPU:239,164 CYC:18728 $EC3A:EF 47 06 *ISB $0647 = #$EB A:40 X:02 Y:B0 P:64 SP:FB PPU:257,164 CYC:18734 $EC3D:EA NOP A:53 X:02 Y:B0 P:nvUbdIzc SP:FB PPU:275,164 CYC:18740 $EC3E:EA NOP A:53 X:02 Y:B0 P:nvUbdIzc SP:FB PPU:281,164 CYC:18742 $EC3F:EA NOP A:53 X:02 Y:B0 P:nvUbdIzc SP:FB PPU:287,164 CYC:18744 $EC40:EA NOP A:53 X:02 Y:B0 P:nvUbdIzc SP:FB PPU:293,164 CYC:18746 $EC41:20 B7 FA JSR $FAB7 A:53 X:02 Y:B0 P:nvUbdIzc SP:FB PPU:299,164 CYC:18748 $FAB7:70 2D BVS $FAE6 A:53 X:02 Y:B0 P:nvUbdIzc SP:F9 PPU:317,164 CYC:18754 $FAB9:B0 2B BCS $FAE6 A:53 X:02 Y:B0 P:nvUbdIzc SP:F9 PPU:323,164 CYC:18756 $FABB:30 29 BMI $FAE6 A:53 X:02 Y:B0 P:nvUbdIzc SP:F9 PPU:329,164 CYC:18758 $FABD:C9 53 CMP #$53 A:53 X:02 Y:B0 P:nvUbdIzc SP:F9 PPU:335,164 CYC:18760 $FABF:D0 25 BNE $FAE6 A:53 X:02 Y:B0 P:nvUbdIZC SP:F9 PPU: 0,165 CYC:18762 $FAC1:60 RTS A:53 X:02 Y:B0 P:nvUbdIZC SP:F9 PPU: 6,165 CYC:18764 $EC44:AD 47 06 LDA $0647 = #$EC A:53 X:02 Y:B0 P:nvUbdIZC SP:FB PPU: 24,165 CYC:18770 $EC47:C9 EC CMP #$EC A:EC X:02 Y:B0 P:A5 SP:FB PPU: 36,165 CYC:18774 $EC49:F0 02 BEQ $EC4D A:EC X:02 Y:B0 P:nvUbdIZC SP:FB PPU: 42,165 CYC:18776 $EC4D:C8 INY A:EC X:02 Y:B0 P:nvUbdIZC SP:FB PPU: 51,165 CYC:18779 $EC4E:A9 FF LDA #$FF A:EC X:02 Y:B1 P:A5 SP:FB PPU: 57,165 CYC:18781 $EC50:8D 47 06 STA $0647 = #$EC A:FF X:02 Y:B1 P:A5 SP:FB PPU: 63,165 CYC:18783 $EC53:20 C2 FA JSR $FAC2 A:FF X:02 Y:B1 P:A5 SP:FB PPU: 75,165 CYC:18787 $FAC2:B8 CLV A:FF X:02 Y:B1 P:A5 SP:F9 PPU: 93,165 CYC:18793 $FAC3:38 SEC A:FF X:02 Y:B1 P:A5 SP:F9 PPU: 99,165 CYC:18795 $FAC4:A9 FF LDA #$FF A:FF X:02 Y:B1 P:A5 SP:F9 PPU:105,165 CYC:18797 $FAC6:60 RTS A:FF X:02 Y:B1 P:A5 SP:F9 PPU:111,165 CYC:18799 $EC56:EF 47 06 *ISB $0647 = #$FF A:FF X:02 Y:B1 P:A5 SP:FB PPU:129,165 CYC:18805 $EC59:EA NOP A:FF X:02 Y:B1 P:A5 SP:FB PPU:147,165 CYC:18811 $EC5A:EA NOP A:FF X:02 Y:B1 P:A5 SP:FB PPU:153,165 CYC:18813 $EC5B:EA NOP A:FF X:02 Y:B1 P:A5 SP:FB PPU:159,165 CYC:18815 $EC5C:EA NOP A:FF X:02 Y:B1 P:A5 SP:FB PPU:165,165 CYC:18817 $EC5D:20 C7 FA JSR $FAC7 A:FF X:02 Y:B1 P:A5 SP:FB PPU:171,165 CYC:18819 $FAC7:70 1D BVS $FAE6 A:FF X:02 Y:B1 P:A5 SP:F9 PPU:189,165 CYC:18825 $FAC9:F0 1B BEQ $FAE6 A:FF X:02 Y:B1 P:A5 SP:F9 PPU:195,165 CYC:18827 $FACB:10 19 BPL $FAE6 A:FF X:02 Y:B1 P:A5 SP:F9 PPU:201,165 CYC:18829 $FACD:90 17 BCC $FAE6 A:FF X:02 Y:B1 P:A5 SP:F9 PPU:207,165 CYC:18831 $FACF:C9 FF CMP #$FF A:FF X:02 Y:B1 P:A5 SP:F9 PPU:213,165 CYC:18833 $FAD1:D0 13 BNE $FAE6 A:FF X:02 Y:B1 P:nvUbdIZC SP:F9 PPU:219,165 CYC:18835 $FAD3:60 RTS A:FF X:02 Y:B1 P:nvUbdIZC SP:F9 PPU:225,165 CYC:18837 $EC60:AD 47 06 LDA $0647 = #$00 A:FF X:02 Y:B1 P:nvUbdIZC SP:FB PPU:243,165 CYC:18843 $EC63:C9 00 CMP #$00 A:00 X:02 Y:B1 P:nvUbdIZC SP:FB PPU:255,165 CYC:18847 $EC65:F0 02 BEQ $EC69 A:00 X:02 Y:B1 P:nvUbdIZC SP:FB PPU:261,165 CYC:18849 $EC69:C8 INY A:00 X:02 Y:B1 P:nvUbdIZC SP:FB PPU:270,165 CYC:18852 $EC6A:A9 37 LDA #$37 A:00 X:02 Y:B2 P:A5 SP:FB PPU:276,165 CYC:18854 $EC6C:8D 47 06 STA $0647 = #$00 A:37 X:02 Y:B2 P:25 SP:FB PPU:282,165 CYC:18856 $EC6F:20 D4 FA JSR $FAD4 A:37 X:02 Y:B2 P:25 SP:FB PPU:294,165 CYC:18860 $FAD4:24 01 BIT $01 = #$FF A:37 X:02 Y:B2 P:25 SP:F9 PPU:312,165 CYC:18866 $FAD6:38 SEC A:37 X:02 Y:B2 P:E5 SP:F9 PPU:321,165 CYC:18869 $FAD7:A9 F0 LDA #$F0 A:37 X:02 Y:B2 P:E5 SP:F9 PPU:327,165 CYC:18871 $FAD9:60 RTS A:F0 X:02 Y:B2 P:E5 SP:F9 PPU:333,165 CYC:18873 $EC72:EF 47 06 *ISB $0647 = #$37 A:F0 X:02 Y:B2 P:E5 SP:FB PPU: 10,166 CYC:18879 $EC75:EA NOP A:B8 X:02 Y:B2 P:A5 SP:FB PPU: 28,166 CYC:18885 $EC76:EA NOP A:B8 X:02 Y:B2 P:A5 SP:FB PPU: 34,166 CYC:18887 $EC77:EA NOP A:B8 X:02 Y:B2 P:A5 SP:FB PPU: 40,166 CYC:18889 $EC78:EA NOP A:B8 X:02 Y:B2 P:A5 SP:FB PPU: 46,166 CYC:18891 $EC79:20 DA FA JSR $FADA A:B8 X:02 Y:B2 P:A5 SP:FB PPU: 52,166 CYC:18893 $FADA:70 0A BVS $FAE6 A:B8 X:02 Y:B2 P:A5 SP:F9 PPU: 70,166 CYC:18899 $FADC:F0 08 BEQ $FAE6 A:B8 X:02 Y:B2 P:A5 SP:F9 PPU: 76,166 CYC:18901 $FADE:10 06 BPL $FAE6 A:B8 X:02 Y:B2 P:A5 SP:F9 PPU: 82,166 CYC:18903 $FAE0:90 04 BCC $FAE6 A:B8 X:02 Y:B2 P:A5 SP:F9 PPU: 88,166 CYC:18905 $FAE2:C9 B8 CMP #$B8 A:B8 X:02 Y:B2 P:A5 SP:F9 PPU: 94,166 CYC:18907 $FAE4:F0 02 BEQ $FAE8 A:B8 X:02 Y:B2 P:nvUbdIZC SP:F9 PPU:100,166 CYC:18909 $FAE8:60 RTS A:B8 X:02 Y:B2 P:nvUbdIZC SP:F9 PPU:109,166 CYC:18912 $EC7C:AD 47 06 LDA $0647 = #$38 A:B8 X:02 Y:B2 P:nvUbdIZC SP:FB PPU:127,166 CYC:18918 $EC7F:C9 38 CMP #$38 A:38 X:02 Y:B2 P:25 SP:FB PPU:139,166 CYC:18922 $EC81:F0 02 BEQ $EC85 A:38 X:02 Y:B2 P:nvUbdIZC SP:FB PPU:145,166 CYC:18924 $EC85:A9 EB LDA #$EB A:38 X:02 Y:B2 P:nvUbdIZC SP:FB PPU:154,166 CYC:18927 $EC87:8D 47 06 STA $0647 = #$38 A:EB X:02 Y:B2 P:A5 SP:FB PPU:160,166 CYC:18929 $EC8A:A9 48 LDA #$48 A:EB X:02 Y:B2 P:A5 SP:FB PPU:172,166 CYC:18933 $EC8C:85 45 STA $45 = #$48 A:48 X:02 Y:B2 P:25 SP:FB PPU:178,166 CYC:18935 $EC8E:A9 05 LDA #$05 A:48 X:02 Y:B2 P:25 SP:FB PPU:187,166 CYC:18938 $EC90:85 46 STA $46 = #$05 A:05 X:02 Y:B2 P:25 SP:FB PPU:193,166 CYC:18940 $EC92:A0 FF LDY #$FF A:05 X:02 Y:B2 P:25 SP:FB PPU:202,166 CYC:18943 $EC94:20 B1 FA JSR $FAB1 A:05 X:02 Y:FF P:A5 SP:FB PPU:208,166 CYC:18945 $FAB1:24 01 BIT $01 = #$FF A:05 X:02 Y:FF P:A5 SP:F9 PPU:226,166 CYC:18951 $FAB3:18 CLC A:05 X:02 Y:FF P:E5 SP:F9 PPU:235,166 CYC:18954 $FAB4:A9 40 LDA #$40 A:05 X:02 Y:FF P:NVUbdIzc SP:F9 PPU:241,166 CYC:18956 $FAB6:60 RTS A:40 X:02 Y:FF P:64 SP:F9 PPU:247,166 CYC:18958 $EC97:F3 45 *ISB ($45),Y = #$0548 @ 0647 = EB A:40 X:02 Y:FF P:64 SP:FB PPU:265,166 CYC:18964 $EC99:EA NOP A:53 X:02 Y:FF P:nvUbdIzc SP:FB PPU:289,166 CYC:18972 $EC9A:EA NOP A:53 X:02 Y:FF P:nvUbdIzc SP:FB PPU:295,166 CYC:18974 $EC9B:08 PHP A:53 X:02 Y:FF P:nvUbdIzc SP:FB PPU:301,166 CYC:18976 $EC9C:48 PHA A:53 X:02 Y:FF P:nvUbdIzc SP:FA PPU:310,166 CYC:18979 $EC9D:A0 B3 LDY #$B3 A:53 X:02 Y:FF P:nvUbdIzc SP:F9 PPU:319,166 CYC:18982 $EC9F:68 PLA A:53 X:02 Y:B3 P:NvUbdIzc SP:F9 PPU:325,166 CYC:18984 $ECA0:28 PLP A:53 X:02 Y:B3 P:nvUbdIzc SP:FA PPU:337,166 CYC:18988 $ECA1:20 B7 FA JSR $FAB7 A:53 X:02 Y:B3 P:nvUbdIzc SP:FB PPU: 8,167 CYC:18992 $FAB7:70 2D BVS $FAE6 A:53 X:02 Y:B3 P:nvUbdIzc SP:F9 PPU: 26,167 CYC:18998 $FAB9:B0 2B BCS $FAE6 A:53 X:02 Y:B3 P:nvUbdIzc SP:F9 PPU: 32,167 CYC:19000 $FABB:30 29 BMI $FAE6 A:53 X:02 Y:B3 P:nvUbdIzc SP:F9 PPU: 38,167 CYC:19002 $FABD:C9 53 CMP #$53 A:53 X:02 Y:B3 P:nvUbdIzc SP:F9 PPU: 44,167 CYC:19004 $FABF:D0 25 BNE $FAE6 A:53 X:02 Y:B3 P:nvUbdIZC SP:F9 PPU: 50,167 CYC:19006 $FAC1:60 RTS A:53 X:02 Y:B3 P:nvUbdIZC SP:F9 PPU: 56,167 CYC:19008 $ECA4:AD 47 06 LDA $0647 = #$EC A:53 X:02 Y:B3 P:nvUbdIZC SP:FB PPU: 74,167 CYC:19014 $ECA7:C9 EC CMP #$EC A:EC X:02 Y:B3 P:A5 SP:FB PPU: 86,167 CYC:19018 $ECA9:F0 02 BEQ $ECAD A:EC X:02 Y:B3 P:nvUbdIZC SP:FB PPU: 92,167 CYC:19020 $ECAD:A0 FF LDY #$FF A:EC X:02 Y:B3 P:nvUbdIZC SP:FB PPU:101,167 CYC:19023 $ECAF:A9 FF LDA #$FF A:EC X:02 Y:FF P:A5 SP:FB PPU:107,167 CYC:19025 $ECB1:8D 47 06 STA $0647 = #$EC A:FF X:02 Y:FF P:A5 SP:FB PPU:113,167 CYC:19027 $ECB4:20 C2 FA JSR $FAC2 A:FF X:02 Y:FF P:A5 SP:FB PPU:125,167 CYC:19031 $FAC2:B8 CLV A:FF X:02 Y:FF P:A5 SP:F9 PPU:143,167 CYC:19037 $FAC3:38 SEC A:FF X:02 Y:FF P:A5 SP:F9 PPU:149,167 CYC:19039 $FAC4:A9 FF LDA #$FF A:FF X:02 Y:FF P:A5 SP:F9 PPU:155,167 CYC:19041 $FAC6:60 RTS A:FF X:02 Y:FF P:A5 SP:F9 PPU:161,167 CYC:19043 $ECB7:F3 45 *ISB ($45),Y = #$0548 @ 0647 = FF A:FF X:02 Y:FF P:A5 SP:FB PPU:179,167 CYC:19049 $ECB9:EA NOP A:FF X:02 Y:FF P:A5 SP:FB PPU:203,167 CYC:19057 $ECBA:EA NOP A:FF X:02 Y:FF P:A5 SP:FB PPU:209,167 CYC:19059 $ECBB:08 PHP A:FF X:02 Y:FF P:A5 SP:FB PPU:215,167 CYC:19061 $ECBC:48 PHA A:FF X:02 Y:FF P:A5 SP:FA PPU:224,167 CYC:19064 $ECBD:A0 B4 LDY #$B4 A:FF X:02 Y:FF P:A5 SP:F9 PPU:233,167 CYC:19067 $ECBF:68 PLA A:FF X:02 Y:B4 P:A5 SP:F9 PPU:239,167 CYC:19069 $ECC0:28 PLP A:FF X:02 Y:B4 P:A5 SP:FA PPU:251,167 CYC:19073 $ECC1:20 C7 FA JSR $FAC7 A:FF X:02 Y:B4 P:A5 SP:FB PPU:263,167 CYC:19077 $FAC7:70 1D BVS $FAE6 A:FF X:02 Y:B4 P:A5 SP:F9 PPU:281,167 CYC:19083 $FAC9:F0 1B BEQ $FAE6 A:FF X:02 Y:B4 P:A5 SP:F9 PPU:287,167 CYC:19085 $FACB:10 19 BPL $FAE6 A:FF X:02 Y:B4 P:A5 SP:F9 PPU:293,167 CYC:19087 $FACD:90 17 BCC $FAE6 A:FF X:02 Y:B4 P:A5 SP:F9 PPU:299,167 CYC:19089 $FACF:C9 FF CMP #$FF A:FF X:02 Y:B4 P:A5 SP:F9 PPU:305,167 CYC:19091 $FAD1:D0 13 BNE $FAE6 A:FF X:02 Y:B4 P:nvUbdIZC SP:F9 PPU:311,167 CYC:19093 $FAD3:60 RTS A:FF X:02 Y:B4 P:nvUbdIZC SP:F9 PPU:317,167 CYC:19095 $ECC4:AD 47 06 LDA $0647 = #$00 A:FF X:02 Y:B4 P:nvUbdIZC SP:FB PPU:335,167 CYC:19101 $ECC7:C9 00 CMP #$00 A:00 X:02 Y:B4 P:nvUbdIZC SP:FB PPU: 6,168 CYC:19105 $ECC9:F0 02 BEQ $ECCD A:00 X:02 Y:B4 P:nvUbdIZC SP:FB PPU: 12,168 CYC:19107 $ECCD:A0 FF LDY #$FF A:00 X:02 Y:B4 P:nvUbdIZC SP:FB PPU: 21,168 CYC:19110 $ECCF:A9 37 LDA #$37 A:00 X:02 Y:FF P:A5 SP:FB PPU: 27,168 CYC:19112 $ECD1:8D 47 06 STA $0647 = #$00 A:37 X:02 Y:FF P:25 SP:FB PPU: 33,168 CYC:19114 $ECD4:20 D4 FA JSR $FAD4 A:37 X:02 Y:FF P:25 SP:FB PPU: 45,168 CYC:19118 $FAD4:24 01 BIT $01 = #$FF A:37 X:02 Y:FF P:25 SP:F9 PPU: 63,168 CYC:19124 $FAD6:38 SEC A:37 X:02 Y:FF P:E5 SP:F9 PPU: 72,168 CYC:19127 $FAD7:A9 F0 LDA #$F0 A:37 X:02 Y:FF P:E5 SP:F9 PPU: 78,168 CYC:19129 $FAD9:60 RTS A:F0 X:02 Y:FF P:E5 SP:F9 PPU: 84,168 CYC:19131 $ECD7:F3 45 *ISB ($45),Y = #$0548 @ 0647 = 37 A:F0 X:02 Y:FF P:E5 SP:FB PPU:102,168 CYC:19137 $ECD9:EA NOP A:B8 X:02 Y:FF P:A5 SP:FB PPU:126,168 CYC:19145 $ECDA:EA NOP A:B8 X:02 Y:FF P:A5 SP:FB PPU:132,168 CYC:19147 $ECDB:08 PHP A:B8 X:02 Y:FF P:A5 SP:FB PPU:138,168 CYC:19149 $ECDC:48 PHA A:B8 X:02 Y:FF P:A5 SP:FA PPU:147,168 CYC:19152 $ECDD:A0 B5 LDY #$B5 A:B8 X:02 Y:FF P:A5 SP:F9 PPU:156,168 CYC:19155 $ECDF:68 PLA A:B8 X:02 Y:B5 P:A5 SP:F9 PPU:162,168 CYC:19157 $ECE0:28 PLP A:B8 X:02 Y:B5 P:A5 SP:FA PPU:174,168 CYC:19161 $ECE1:20 DA FA JSR $FADA A:B8 X:02 Y:B5 P:A5 SP:FB PPU:186,168 CYC:19165 $FADA:70 0A BVS $FAE6 A:B8 X:02 Y:B5 P:A5 SP:F9 PPU:204,168 CYC:19171 $FADC:F0 08 BEQ $FAE6 A:B8 X:02 Y:B5 P:A5 SP:F9 PPU:210,168 CYC:19173 $FADE:10 06 BPL $FAE6 A:B8 X:02 Y:B5 P:A5 SP:F9 PPU:216,168 CYC:19175 $FAE0:90 04 BCC $FAE6 A:B8 X:02 Y:B5 P:A5 SP:F9 PPU:222,168 CYC:19177 $FAE2:C9 B8 CMP #$B8 A:B8 X:02 Y:B5 P:A5 SP:F9 PPU:228,168 CYC:19179 $FAE4:F0 02 BEQ $FAE8 A:B8 X:02 Y:B5 P:nvUbdIZC SP:F9 PPU:234,168 CYC:19181 $FAE8:60 RTS A:B8 X:02 Y:B5 P:nvUbdIZC SP:F9 PPU:243,168 CYC:19184 $ECE4:AD 47 06 LDA $0647 = #$38 A:B8 X:02 Y:B5 P:nvUbdIZC SP:FB PPU:261,168 CYC:19190 $ECE7:C9 38 CMP #$38 A:38 X:02 Y:B5 P:25 SP:FB PPU:273,168 CYC:19194 $ECE9:F0 02 BEQ $ECED A:38 X:02 Y:B5 P:nvUbdIZC SP:FB PPU:279,168 CYC:19196 $ECED:A0 B6 LDY #$B6 A:38 X:02 Y:B5 P:nvUbdIZC SP:FB PPU:288,168 CYC:19199 $ECEF:A2 FF LDX #$FF A:38 X:02 Y:B6 P:A5 SP:FB PPU:294,168 CYC:19201 $ECF1:A9 EB LDA #$EB A:38 X:FF Y:B6 P:A5 SP:FB PPU:300,168 CYC:19203 $ECF3:85 47 STA $47 = #$38 A:EB X:FF Y:B6 P:A5 SP:FB PPU:306,168 CYC:19205 $ECF5:20 B1 FA JSR $FAB1 A:EB X:FF Y:B6 P:A5 SP:FB PPU:315,168 CYC:19208 $FAB1:24 01 BIT $01 = #$FF A:EB X:FF Y:B6 P:A5 SP:F9 PPU:333,168 CYC:19214 $FAB3:18 CLC A:EB X:FF Y:B6 P:E5 SP:F9 PPU: 1,169 CYC:19217 $FAB4:A9 40 LDA #$40 A:EB X:FF Y:B6 P:NVUbdIzc SP:F9 PPU: 7,169 CYC:19219 $FAB6:60 RTS A:40 X:FF Y:B6 P:64 SP:F9 PPU: 13,169 CYC:19221 $ECF8:F7 48 *ISB $48,X @ 47 = #$EB A:40 X:FF Y:B6 P:64 SP:FB PPU: 31,169 CYC:19227 $ECFA:EA NOP A:53 X:FF Y:B6 P:nvUbdIzc SP:FB PPU: 49,169 CYC:19233 $ECFB:EA NOP A:53 X:FF Y:B6 P:nvUbdIzc SP:FB PPU: 55,169 CYC:19235 $ECFC:EA NOP A:53 X:FF Y:B6 P:nvUbdIzc SP:FB PPU: 61,169 CYC:19237 $ECFD:EA NOP A:53 X:FF Y:B6 P:nvUbdIzc SP:FB PPU: 67,169 CYC:19239 $ECFE:20 B7 FA JSR $FAB7 A:53 X:FF Y:B6 P:nvUbdIzc SP:FB PPU: 73,169 CYC:19241 $FAB7:70 2D BVS $FAE6 A:53 X:FF Y:B6 P:nvUbdIzc SP:F9 PPU: 91,169 CYC:19247 $FAB9:B0 2B BCS $FAE6 A:53 X:FF Y:B6 P:nvUbdIzc SP:F9 PPU: 97,169 CYC:19249 $FABB:30 29 BMI $FAE6 A:53 X:FF Y:B6 P:nvUbdIzc SP:F9 PPU:103,169 CYC:19251 $FABD:C9 53 CMP #$53 A:53 X:FF Y:B6 P:nvUbdIzc SP:F9 PPU:109,169 CYC:19253 $FABF:D0 25 BNE $FAE6 A:53 X:FF Y:B6 P:nvUbdIZC SP:F9 PPU:115,169 CYC:19255 $FAC1:60 RTS A:53 X:FF Y:B6 P:nvUbdIZC SP:F9 PPU:121,169 CYC:19257 $ED01:A5 47 LDA $47 = #$EC A:53 X:FF Y:B6 P:nvUbdIZC SP:FB PPU:139,169 CYC:19263 $ED03:C9 EC CMP #$EC A:EC X:FF Y:B6 P:A5 SP:FB PPU:148,169 CYC:19266 $ED05:F0 02 BEQ $ED09 A:EC X:FF Y:B6 P:nvUbdIZC SP:FB PPU:154,169 CYC:19268 $ED09:C8 INY A:EC X:FF Y:B6 P:nvUbdIZC SP:FB PPU:163,169 CYC:19271 $ED0A:A9 FF LDA #$FF A:EC X:FF Y:B7 P:A5 SP:FB PPU:169,169 CYC:19273 $ED0C:85 47 STA $47 = #$EC A:FF X:FF Y:B7 P:A5 SP:FB PPU:175,169 CYC:19275 $ED0E:20 C2 FA JSR $FAC2 A:FF X:FF Y:B7 P:A5 SP:FB PPU:184,169 CYC:19278 $FAC2:B8 CLV A:FF X:FF Y:B7 P:A5 SP:F9 PPU:202,169 CYC:19284 $FAC3:38 SEC A:FF X:FF Y:B7 P:A5 SP:F9 PPU:208,169 CYC:19286 $FAC4:A9 FF LDA #$FF A:FF X:FF Y:B7 P:A5 SP:F9 PPU:214,169 CYC:19288 $FAC6:60 RTS A:FF X:FF Y:B7 P:A5 SP:F9 PPU:220,169 CYC:19290 $ED11:F7 48 *ISB $48,X @ 47 = #$FF A:FF X:FF Y:B7 P:A5 SP:FB PPU:238,169 CYC:19296 $ED13:EA NOP A:FF X:FF Y:B7 P:A5 SP:FB PPU:256,169 CYC:19302 $ED14:EA NOP A:FF X:FF Y:B7 P:A5 SP:FB PPU:262,169 CYC:19304 $ED15:EA NOP A:FF X:FF Y:B7 P:A5 SP:FB PPU:268,169 CYC:19306 $ED16:EA NOP A:FF X:FF Y:B7 P:A5 SP:FB PPU:274,169 CYC:19308 $ED17:20 C7 FA JSR $FAC7 A:FF X:FF Y:B7 P:A5 SP:FB PPU:280,169 CYC:19310 $FAC7:70 1D BVS $FAE6 A:FF X:FF Y:B7 P:A5 SP:F9 PPU:298,169 CYC:19316 $FAC9:F0 1B BEQ $FAE6 A:FF X:FF Y:B7 P:A5 SP:F9 PPU:304,169 CYC:19318 $FACB:10 19 BPL $FAE6 A:FF X:FF Y:B7 P:A5 SP:F9 PPU:310,169 CYC:19320 $FACD:90 17 BCC $FAE6 A:FF X:FF Y:B7 P:A5 SP:F9 PPU:316,169 CYC:19322 $FACF:C9 FF CMP #$FF A:FF X:FF Y:B7 P:A5 SP:F9 PPU:322,169 CYC:19324 $FAD1:D0 13 BNE $FAE6 A:FF X:FF Y:B7 P:nvUbdIZC SP:F9 PPU:328,169 CYC:19326 $FAD3:60 RTS A:FF X:FF Y:B7 P:nvUbdIZC SP:F9 PPU:334,169 CYC:19328 $ED1A:A5 47 LDA $47 = #$00 A:FF X:FF Y:B7 P:nvUbdIZC SP:FB PPU: 11,170 CYC:19334 $ED1C:C9 00 CMP #$00 A:00 X:FF Y:B7 P:nvUbdIZC SP:FB PPU: 20,170 CYC:19337 $ED1E:F0 02 BEQ $ED22 A:00 X:FF Y:B7 P:nvUbdIZC SP:FB PPU: 26,170 CYC:19339 $ED22:C8 INY A:00 X:FF Y:B7 P:nvUbdIZC SP:FB PPU: 35,170 CYC:19342 $ED23:A9 37 LDA #$37 A:00 X:FF Y:B8 P:A5 SP:FB PPU: 41,170 CYC:19344 $ED25:85 47 STA $47 = #$00 A:37 X:FF Y:B8 P:25 SP:FB PPU: 47,170 CYC:19346 $ED27:20 D4 FA JSR $FAD4 A:37 X:FF Y:B8 P:25 SP:FB PPU: 56,170 CYC:19349 $FAD4:24 01 BIT $01 = #$FF A:37 X:FF Y:B8 P:25 SP:F9 PPU: 74,170 CYC:19355 $FAD6:38 SEC A:37 X:FF Y:B8 P:E5 SP:F9 PPU: 83,170 CYC:19358 $FAD7:A9 F0 LDA #$F0 A:37 X:FF Y:B8 P:E5 SP:F9 PPU: 89,170 CYC:19360 $FAD9:60 RTS A:F0 X:FF Y:B8 P:E5 SP:F9 PPU: 95,170 CYC:19362 $ED2A:F7 48 *ISB $48,X @ 47 = #$37 A:F0 X:FF Y:B8 P:E5 SP:FB PPU:113,170 CYC:19368 $ED2C:EA NOP A:B8 X:FF Y:B8 P:A5 SP:FB PPU:131,170 CYC:19374 $ED2D:EA NOP A:B8 X:FF Y:B8 P:A5 SP:FB PPU:137,170 CYC:19376 $ED2E:EA NOP A:B8 X:FF Y:B8 P:A5 SP:FB PPU:143,170 CYC:19378 $ED2F:EA NOP A:B8 X:FF Y:B8 P:A5 SP:FB PPU:149,170 CYC:19380 $ED30:20 DA FA JSR $FADA A:B8 X:FF Y:B8 P:A5 SP:FB PPU:155,170 CYC:19382 $FADA:70 0A BVS $FAE6 A:B8 X:FF Y:B8 P:A5 SP:F9 PPU:173,170 CYC:19388 $FADC:F0 08 BEQ $FAE6 A:B8 X:FF Y:B8 P:A5 SP:F9 PPU:179,170 CYC:19390 $FADE:10 06 BPL $FAE6 A:B8 X:FF Y:B8 P:A5 SP:F9 PPU:185,170 CYC:19392 $FAE0:90 04 BCC $FAE6 A:B8 X:FF Y:B8 P:A5 SP:F9 PPU:191,170 CYC:19394 $FAE2:C9 B8 CMP #$B8 A:B8 X:FF Y:B8 P:A5 SP:F9 PPU:197,170 CYC:19396 $FAE4:F0 02 BEQ $FAE8 A:B8 X:FF Y:B8 P:nvUbdIZC SP:F9 PPU:203,170 CYC:19398 $FAE8:60 RTS A:B8 X:FF Y:B8 P:nvUbdIZC SP:F9 PPU:212,170 CYC:19401 $ED33:A5 47 LDA $47 = #$38 A:B8 X:FF Y:B8 P:nvUbdIZC SP:FB PPU:230,170 CYC:19407 $ED35:C9 38 CMP #$38 A:38 X:FF Y:B8 P:25 SP:FB PPU:239,170 CYC:19410 $ED37:F0 02 BEQ $ED3B A:38 X:FF Y:B8 P:nvUbdIZC SP:FB PPU:245,170 CYC:19412 $ED3B:A9 EB LDA #$EB A:38 X:FF Y:B8 P:nvUbdIZC SP:FB PPU:254,170 CYC:19415 $ED3D:8D 47 06 STA $0647 = #$38 A:EB X:FF Y:B8 P:A5 SP:FB PPU:260,170 CYC:19417 $ED40:A0 FF LDY #$FF A:EB X:FF Y:B8 P:A5 SP:FB PPU:272,170 CYC:19421 $ED42:20 B1 FA JSR $FAB1 A:EB X:FF Y:FF P:A5 SP:FB PPU:278,170 CYC:19423 $FAB1:24 01 BIT $01 = #$FF A:EB X:FF Y:FF P:A5 SP:F9 PPU:296,170 CYC:19429 $FAB3:18 CLC A:EB X:FF Y:FF P:E5 SP:F9 PPU:305,170 CYC:19432 $FAB4:A9 40 LDA #$40 A:EB X:FF Y:FF P:NVUbdIzc SP:F9 PPU:311,170 CYC:19434 $FAB6:60 RTS A:40 X:FF Y:FF P:64 SP:F9 PPU:317,170 CYC:19436 $ED45:FB 48 05 *ISB $0548,Y @ 0647 = #$EB A:40 X:FF Y:FF P:64 SP:FB PPU:335,170 CYC:19442 $ED48:EA NOP A:53 X:FF Y:FF P:nvUbdIzc SP:FB PPU: 15,171 CYC:19449 $ED49:EA NOP A:53 X:FF Y:FF P:nvUbdIzc SP:FB PPU: 21,171 CYC:19451 $ED4A:08 PHP A:53 X:FF Y:FF P:nvUbdIzc SP:FB PPU: 27,171 CYC:19453 $ED4B:48 PHA A:53 X:FF Y:FF P:nvUbdIzc SP:FA PPU: 36,171 CYC:19456 $ED4C:A0 B9 LDY #$B9 A:53 X:FF Y:FF P:nvUbdIzc SP:F9 PPU: 45,171 CYC:19459 $ED4E:68 PLA A:53 X:FF Y:B9 P:NvUbdIzc SP:F9 PPU: 51,171 CYC:19461 $ED4F:28 PLP A:53 X:FF Y:B9 P:nvUbdIzc SP:FA PPU: 63,171 CYC:19465 $ED50:20 B7 FA JSR $FAB7 A:53 X:FF Y:B9 P:nvUbdIzc SP:FB PPU: 75,171 CYC:19469 $FAB7:70 2D BVS $FAE6 A:53 X:FF Y:B9 P:nvUbdIzc SP:F9 PPU: 93,171 CYC:19475 $FAB9:B0 2B BCS $FAE6 A:53 X:FF Y:B9 P:nvUbdIzc SP:F9 PPU: 99,171 CYC:19477 $FABB:30 29 BMI $FAE6 A:53 X:FF Y:B9 P:nvUbdIzc SP:F9 PPU:105,171 CYC:19479 $FABD:C9 53 CMP #$53 A:53 X:FF Y:B9 P:nvUbdIzc SP:F9 PPU:111,171 CYC:19481 $FABF:D0 25 BNE $FAE6 A:53 X:FF Y:B9 P:nvUbdIZC SP:F9 PPU:117,171 CYC:19483 $FAC1:60 RTS A:53 X:FF Y:B9 P:nvUbdIZC SP:F9 PPU:123,171 CYC:19485 $ED53:AD 47 06 LDA $0647 = #$EC A:53 X:FF Y:B9 P:nvUbdIZC SP:FB PPU:141,171 CYC:19491 $ED56:C9 EC CMP #$EC A:EC X:FF Y:B9 P:A5 SP:FB PPU:153,171 CYC:19495 $ED58:F0 02 BEQ $ED5C A:EC X:FF Y:B9 P:nvUbdIZC SP:FB PPU:159,171 CYC:19497 $ED5C:A0 FF LDY #$FF A:EC X:FF Y:B9 P:nvUbdIZC SP:FB PPU:168,171 CYC:19500 $ED5E:A9 FF LDA #$FF A:EC X:FF Y:FF P:A5 SP:FB PPU:174,171 CYC:19502 $ED60:8D 47 06 STA $0647 = #$EC A:FF X:FF Y:FF P:A5 SP:FB PPU:180,171 CYC:19504 $ED63:20 C2 FA JSR $FAC2 A:FF X:FF Y:FF P:A5 SP:FB PPU:192,171 CYC:19508 $FAC2:B8 CLV A:FF X:FF Y:FF P:A5 SP:F9 PPU:210,171 CYC:19514 $FAC3:38 SEC A:FF X:FF Y:FF P:A5 SP:F9 PPU:216,171 CYC:19516 $FAC4:A9 FF LDA #$FF A:FF X:FF Y:FF P:A5 SP:F9 PPU:222,171 CYC:19518 $FAC6:60 RTS A:FF X:FF Y:FF P:A5 SP:F9 PPU:228,171 CYC:19520 $ED66:FB 48 05 *ISB $0548,Y @ 0647 = #$FF A:FF X:FF Y:FF P:A5 SP:FB PPU:246,171 CYC:19526 $ED69:EA NOP A:FF X:FF Y:FF P:A5 SP:FB PPU:267,171 CYC:19533 $ED6A:EA NOP A:FF X:FF Y:FF P:A5 SP:FB PPU:273,171 CYC:19535 $ED6B:08 PHP A:FF X:FF Y:FF P:A5 SP:FB PPU:279,171 CYC:19537 $ED6C:48 PHA A:FF X:FF Y:FF P:A5 SP:FA PPU:288,171 CYC:19540 $ED6D:A0 BA LDY #$BA A:FF X:FF Y:FF P:A5 SP:F9 PPU:297,171 CYC:19543 $ED6F:68 PLA A:FF X:FF Y:BA P:A5 SP:F9 PPU:303,171 CYC:19545 $ED70:28 PLP A:FF X:FF Y:BA P:A5 SP:FA PPU:315,171 CYC:19549 $ED71:20 C7 FA JSR $FAC7 A:FF X:FF Y:BA P:A5 SP:FB PPU:327,171 CYC:19553 $FAC7:70 1D BVS $FAE6 A:FF X:FF Y:BA P:A5 SP:F9 PPU: 4,172 CYC:19559 $FAC9:F0 1B BEQ $FAE6 A:FF X:FF Y:BA P:A5 SP:F9 PPU: 10,172 CYC:19561 $FACB:10 19 BPL $FAE6 A:FF X:FF Y:BA P:A5 SP:F9 PPU: 16,172 CYC:19563 $FACD:90 17 BCC $FAE6 A:FF X:FF Y:BA P:A5 SP:F9 PPU: 22,172 CYC:19565 $FACF:C9 FF CMP #$FF A:FF X:FF Y:BA P:A5 SP:F9 PPU: 28,172 CYC:19567 $FAD1:D0 13 BNE $FAE6 A:FF X:FF Y:BA P:nvUbdIZC SP:F9 PPU: 34,172 CYC:19569 $FAD3:60 RTS A:FF X:FF Y:BA P:nvUbdIZC SP:F9 PPU: 40,172 CYC:19571 $ED74:AD 47 06 LDA $0647 = #$00 A:FF X:FF Y:BA P:nvUbdIZC SP:FB PPU: 58,172 CYC:19577 $ED77:C9 00 CMP #$00 A:00 X:FF Y:BA P:nvUbdIZC SP:FB PPU: 70,172 CYC:19581 $ED79:F0 02 BEQ $ED7D A:00 X:FF Y:BA P:nvUbdIZC SP:FB PPU: 76,172 CYC:19583 $ED7D:A0 FF LDY #$FF A:00 X:FF Y:BA P:nvUbdIZC SP:FB PPU: 85,172 CYC:19586 $ED7F:A9 37 LDA #$37 A:00 X:FF Y:FF P:A5 SP:FB PPU: 91,172 CYC:19588 $ED81:8D 47 06 STA $0647 = #$00 A:37 X:FF Y:FF P:25 SP:FB PPU: 97,172 CYC:19590 $ED84:20 D4 FA JSR $FAD4 A:37 X:FF Y:FF P:25 SP:FB PPU:109,172 CYC:19594 $FAD4:24 01 BIT $01 = #$FF A:37 X:FF Y:FF P:25 SP:F9 PPU:127,172 CYC:19600 $FAD6:38 SEC A:37 X:FF Y:FF P:E5 SP:F9 PPU:136,172 CYC:19603 $FAD7:A9 F0 LDA #$F0 A:37 X:FF Y:FF P:E5 SP:F9 PPU:142,172 CYC:19605 $FAD9:60 RTS A:F0 X:FF Y:FF P:E5 SP:F9 PPU:148,172 CYC:19607 $ED87:FB 48 05 *ISB $0548,Y @ 0647 = #$37 A:F0 X:FF Y:FF P:E5 SP:FB PPU:166,172 CYC:19613 $ED8A:EA NOP A:B8 X:FF Y:FF P:A5 SP:FB PPU:187,172 CYC:19620 $ED8B:EA NOP A:B8 X:FF Y:FF P:A5 SP:FB PPU:193,172 CYC:19622 $ED8C:08 PHP A:B8 X:FF Y:FF P:A5 SP:FB PPU:199,172 CYC:19624 $ED8D:48 PHA A:B8 X:FF Y:FF P:A5 SP:FA PPU:208,172 CYC:19627 $ED8E:A0 BB LDY #$BB A:B8 X:FF Y:FF P:A5 SP:F9 PPU:217,172 CYC:19630 $ED90:68 PLA A:B8 X:FF Y:BB P:A5 SP:F9 PPU:223,172 CYC:19632 $ED91:28 PLP A:B8 X:FF Y:BB P:A5 SP:FA PPU:235,172 CYC:19636 $ED92:20 DA FA JSR $FADA A:B8 X:FF Y:BB P:A5 SP:FB PPU:247,172 CYC:19640 $FADA:70 0A BVS $FAE6 A:B8 X:FF Y:BB P:A5 SP:F9 PPU:265,172 CYC:19646 $FADC:F0 08 BEQ $FAE6 A:B8 X:FF Y:BB P:A5 SP:F9 PPU:271,172 CYC:19648 $FADE:10 06 BPL $FAE6 A:B8 X:FF Y:BB P:A5 SP:F9 PPU:277,172 CYC:19650 $FAE0:90 04 BCC $FAE6 A:B8 X:FF Y:BB P:A5 SP:F9 PPU:283,172 CYC:19652 $FAE2:C9 B8 CMP #$B8 A:B8 X:FF Y:BB P:A5 SP:F9 PPU:289,172 CYC:19654 $FAE4:F0 02 BEQ $FAE8 A:B8 X:FF Y:BB P:nvUbdIZC SP:F9 PPU:295,172 CYC:19656 $FAE8:60 RTS A:B8 X:FF Y:BB P:nvUbdIZC SP:F9 PPU:304,172 CYC:19659 $ED95:AD 47 06 LDA $0647 = #$38 A:B8 X:FF Y:BB P:nvUbdIZC SP:FB PPU:322,172 CYC:19665 $ED98:C9 38 CMP #$38 A:38 X:FF Y:BB P:25 SP:FB PPU:334,172 CYC:19669 $ED9A:F0 02 BEQ $ED9E A:38 X:FF Y:BB P:nvUbdIZC SP:FB PPU:340,172 CYC:19671 $ED9E:A0 BC LDY #$BC A:38 X:FF Y:BB P:nvUbdIZC SP:FB PPU: 8,173 CYC:19674 $EDA0:A2 FF LDX #$FF A:38 X:FF Y:BC P:A5 SP:FB PPU: 14,173 CYC:19676 $EDA2:A9 EB LDA #$EB A:38 X:FF Y:BC P:A5 SP:FB PPU: 20,173 CYC:19678 $EDA4:8D 47 06 STA $0647 = #$38 A:EB X:FF Y:BC P:A5 SP:FB PPU: 26,173 CYC:19680 $EDA7:20 B1 FA JSR $FAB1 A:EB X:FF Y:BC P:A5 SP:FB PPU: 38,173 CYC:19684 $FAB1:24 01 BIT $01 = #$FF A:EB X:FF Y:BC P:A5 SP:F9 PPU: 56,173 CYC:19690 $FAB3:18 CLC A:EB X:FF Y:BC P:E5 SP:F9 PPU: 65,173 CYC:19693 $FAB4:A9 40 LDA #$40 A:EB X:FF Y:BC P:NVUbdIzc SP:F9 PPU: 71,173 CYC:19695 $FAB6:60 RTS A:40 X:FF Y:BC P:64 SP:F9 PPU: 77,173 CYC:19697 $EDAA:FF 48 05 *ISB $0548,X @ 0647 = #$EB A:40 X:FF Y:BC P:64 SP:FB PPU: 95,173 CYC:19703 $EDAD:EA NOP A:53 X:FF Y:BC P:nvUbdIzc SP:FB PPU:116,173 CYC:19710 $EDAE:EA NOP A:53 X:FF Y:BC P:nvUbdIzc SP:FB PPU:122,173 CYC:19712 $EDAF:EA NOP A:53 X:FF Y:BC P:nvUbdIzc SP:FB PPU:128,173 CYC:19714 $EDB0:EA NOP A:53 X:FF Y:BC P:nvUbdIzc SP:FB PPU:134,173 CYC:19716 $EDB1:20 B7 FA JSR $FAB7 A:53 X:FF Y:BC P:nvUbdIzc SP:FB PPU:140,173 CYC:19718 $FAB7:70 2D BVS $FAE6 A:53 X:FF Y:BC P:nvUbdIzc SP:F9 PPU:158,173 CYC:19724 $FAB9:B0 2B BCS $FAE6 A:53 X:FF Y:BC P:nvUbdIzc SP:F9 PPU:164,173 CYC:19726 $FABB:30 29 BMI $FAE6 A:53 X:FF Y:BC P:nvUbdIzc SP:F9 PPU:170,173 CYC:19728 $FABD:C9 53 CMP #$53 A:53 X:FF Y:BC P:nvUbdIzc SP:F9 PPU:176,173 CYC:19730 $FABF:D0 25 BNE $FAE6 A:53 X:FF Y:BC P:nvUbdIZC SP:F9 PPU:182,173 CYC:19732 $FAC1:60 RTS A:53 X:FF Y:BC P:nvUbdIZC SP:F9 PPU:188,173 CYC:19734 $EDB4:AD 47 06 LDA $0647 = #$EC A:53 X:FF Y:BC P:nvUbdIZC SP:FB PPU:206,173 CYC:19740 $EDB7:C9 EC CMP #$EC A:EC X:FF Y:BC P:A5 SP:FB PPU:218,173 CYC:19744 $EDB9:F0 02 BEQ $EDBD A:EC X:FF Y:BC P:nvUbdIZC SP:FB PPU:224,173 CYC:19746 $EDBD:C8 INY A:EC X:FF Y:BC P:nvUbdIZC SP:FB PPU:233,173 CYC:19749 $EDBE:A9 FF LDA #$FF A:EC X:FF Y:BD P:A5 SP:FB PPU:239,173 CYC:19751 $EDC0:8D 47 06 STA $0647 = #$EC A:FF X:FF Y:BD P:A5 SP:FB PPU:245,173 CYC:19753 $EDC3:20 C2 FA JSR $FAC2 A:FF X:FF Y:BD P:A5 SP:FB PPU:257,173 CYC:19757 $FAC2:B8 CLV A:FF X:FF Y:BD P:A5 SP:F9 PPU:275,173 CYC:19763 $FAC3:38 SEC A:FF X:FF Y:BD P:A5 SP:F9 PPU:281,173 CYC:19765 $FAC4:A9 FF LDA #$FF A:FF X:FF Y:BD P:A5 SP:F9 PPU:287,173 CYC:19767 $FAC6:60 RTS A:FF X:FF Y:BD P:A5 SP:F9 PPU:293,173 CYC:19769 $EDC6:FF 48 05 *ISB $0548,X @ 0647 = #$FF A:FF X:FF Y:BD P:A5 SP:FB PPU:311,173 CYC:19775 $EDC9:EA NOP A:FF X:FF Y:BD P:A5 SP:FB PPU:332,173 CYC:19782 $EDCA:EA NOP A:FF X:FF Y:BD P:A5 SP:FB PPU:338,173 CYC:19784 $EDCB:EA NOP A:FF X:FF Y:BD P:A5 SP:FB PPU: 3,174 CYC:19786 $EDCC:EA NOP A:FF X:FF Y:BD P:A5 SP:FB PPU: 9,174 CYC:19788 $EDCD:20 C7 FA JSR $FAC7 A:FF X:FF Y:BD P:A5 SP:FB PPU: 15,174 CYC:19790 $FAC7:70 1D BVS $FAE6 A:FF X:FF Y:BD P:A5 SP:F9 PPU: 33,174 CYC:19796 $FAC9:F0 1B BEQ $FAE6 A:FF X:FF Y:BD P:A5 SP:F9 PPU: 39,174 CYC:19798 $FACB:10 19 BPL $FAE6 A:FF X:FF Y:BD P:A5 SP:F9 PPU: 45,174 CYC:19800 $FACD:90 17 BCC $FAE6 A:FF X:FF Y:BD P:A5 SP:F9 PPU: 51,174 CYC:19802 $FACF:C9 FF CMP #$FF A:FF X:FF Y:BD P:A5 SP:F9 PPU: 57,174 CYC:19804 $FAD1:D0 13 BNE $FAE6 A:FF X:FF Y:BD P:nvUbdIZC SP:F9 PPU: 63,174 CYC:19806 $FAD3:60 RTS A:FF X:FF Y:BD P:nvUbdIZC SP:F9 PPU: 69,174 CYC:19808 $EDD0:AD 47 06 LDA $0647 = #$00 A:FF X:FF Y:BD P:nvUbdIZC SP:FB PPU: 87,174 CYC:19814 $EDD3:C9 00 CMP #$00 A:00 X:FF Y:BD P:nvUbdIZC SP:FB PPU: 99,174 CYC:19818 $EDD5:F0 02 BEQ $EDD9 A:00 X:FF Y:BD P:nvUbdIZC SP:FB PPU:105,174 CYC:19820 $EDD9:C8 INY A:00 X:FF Y:BD P:nvUbdIZC SP:FB PPU:114,174 CYC:19823 $EDDA:A9 37 LDA #$37 A:00 X:FF Y:BE P:A5 SP:FB PPU:120,174 CYC:19825 $EDDC:8D 47 06 STA $0647 = #$00 A:37 X:FF Y:BE P:25 SP:FB PPU:126,174 CYC:19827 $EDDF:20 D4 FA JSR $FAD4 A:37 X:FF Y:BE P:25 SP:FB PPU:138,174 CYC:19831 $FAD4:24 01 BIT $01 = #$FF A:37 X:FF Y:BE P:25 SP:F9 PPU:156,174 CYC:19837 $FAD6:38 SEC A:37 X:FF Y:BE P:E5 SP:F9 PPU:165,174 CYC:19840 $FAD7:A9 F0 LDA #$F0 A:37 X:FF Y:BE P:E5 SP:F9 PPU:171,174 CYC:19842 $FAD9:60 RTS A:F0 X:FF Y:BE P:E5 SP:F9 PPU:177,174 CYC:19844 $EDE2:FF 48 05 *ISB $0548,X @ 0647 = #$37 A:F0 X:FF Y:BE P:E5 SP:FB PPU:195,174 CYC:19850 $EDE5:EA NOP A:B8 X:FF Y:BE P:A5 SP:FB PPU:216,174 CYC:19857 $EDE6:EA NOP A:B8 X:FF Y:BE P:A5 SP:FB PPU:222,174 CYC:19859 $EDE7:EA NOP A:B8 X:FF Y:BE P:A5 SP:FB PPU:228,174 CYC:19861 $EDE8:EA NOP A:B8 X:FF Y:BE P:A5 SP:FB PPU:234,174 CYC:19863 $EDE9:20 DA FA JSR $FADA A:B8 X:FF Y:BE P:A5 SP:FB PPU:240,174 CYC:19865 $FADA:70 0A BVS $FAE6 A:B8 X:FF Y:BE P:A5 SP:F9 PPU:258,174 CYC:19871 $FADC:F0 08 BEQ $FAE6 A:B8 X:FF Y:BE P:A5 SP:F9 PPU:264,174 CYC:19873 $FADE:10 06 BPL $FAE6 A:B8 X:FF Y:BE P:A5 SP:F9 PPU:270,174 CYC:19875 $FAE0:90 04 BCC $FAE6 A:B8 X:FF Y:BE P:A5 SP:F9 PPU:276,174 CYC:19877 $FAE2:C9 B8 CMP #$B8 A:B8 X:FF Y:BE P:A5 SP:F9 PPU:282,174 CYC:19879 $FAE4:F0 02 BEQ $FAE8 A:B8 X:FF Y:BE P:nvUbdIZC SP:F9 PPU:288,174 CYC:19881 $FAE8:60 RTS A:B8 X:FF Y:BE P:nvUbdIZC SP:F9 PPU:297,174 CYC:19884 $EDEC:AD 47 06 LDA $0647 = #$38 A:B8 X:FF Y:BE P:nvUbdIZC SP:FB PPU:315,174 CYC:19890 $EDEF:C9 38 CMP #$38 A:38 X:FF Y:BE P:25 SP:FB PPU:327,174 CYC:19894 $EDF1:F0 02 BEQ $EDF5 A:38 X:FF Y:BE P:nvUbdIZC SP:FB PPU:333,174 CYC:19896 $EDF5:60 RTS A:38 X:FF Y:BE P:nvUbdIZC SP:FB PPU: 1,175 CYC:19899 $C641:20 F6 ED JSR $EDF6 A:38 X:FF Y:BE P:nvUbdIZC SP:FD PPU: 19,175 CYC:19905 $EDF6:A9 FF LDA #$FF A:38 X:FF Y:BE P:nvUbdIZC SP:FB PPU: 37,175 CYC:19911 $EDF8:85 01 STA $01 = #$FF A:FF X:FF Y:BE P:A5 SP:FB PPU: 43,175 CYC:19913 $EDFA:A0 BF LDY #$BF A:FF X:FF Y:BE P:A5 SP:FB PPU: 52,175 CYC:19916 $EDFC:A2 02 LDX #$02 A:FF X:FF Y:BF P:A5 SP:FB PPU: 58,175 CYC:19918 $EDFE:A9 47 LDA #$47 A:FF X:02 Y:BF P:25 SP:FB PPU: 64,175 CYC:19920 $EE00:85 47 STA $47 = #$38 A:47 X:02 Y:BF P:25 SP:FB PPU: 70,175 CYC:19922 $EE02:A9 06 LDA #$06 A:47 X:02 Y:BF P:25 SP:FB PPU: 79,175 CYC:19925 $EE04:85 48 STA $48 = #$06 A:06 X:02 Y:BF P:25 SP:FB PPU: 85,175 CYC:19927 $EE06:A9 A5 LDA #$A5 A:06 X:02 Y:BF P:25 SP:FB PPU: 94,175 CYC:19930 $EE08:8D 47 06 STA $0647 = #$38 A:A5 X:02 Y:BF P:A5 SP:FB PPU:100,175 CYC:19932 $EE0B:20 7B FA JSR $FA7B A:A5 X:02 Y:BF P:A5 SP:FB PPU:112,175 CYC:19936 $FA7B:24 01 BIT $01 = #$FF A:A5 X:02 Y:BF P:A5 SP:F9 PPU:130,175 CYC:19942 $FA7D:18 CLC A:A5 X:02 Y:BF P:E5 SP:F9 PPU:139,175 CYC:19945 $FA7E:A9 B3 LDA #$B3 A:A5 X:02 Y:BF P:NVUbdIzc SP:F9 PPU:145,175 CYC:19947 $FA80:60 RTS A:B3 X:02 Y:BF P:NVUbdIzc SP:F9 PPU:151,175 CYC:19949 $EE0E:03 45 *SLO ($45,X) @ 47 = #$0647 = A5 A:B3 X:02 Y:BF P:NVUbdIzc SP:FB PPU:169,175 CYC:19955 $EE10:EA NOP A:FB X:02 Y:BF P:E5 SP:FB PPU:193,175 CYC:19963 $EE11:EA NOP A:FB X:02 Y:BF P:E5 SP:FB PPU:199,175 CYC:19965 $EE12:EA NOP A:FB X:02 Y:BF P:E5 SP:FB PPU:205,175 CYC:19967 $EE13:EA NOP A:FB X:02 Y:BF P:E5 SP:FB PPU:211,175 CYC:19969 $EE14:20 81 FA JSR $FA81 A:FB X:02 Y:BF P:E5 SP:FB PPU:217,175 CYC:19971 $FA81:50 63 BVC $FAE6 A:FB X:02 Y:BF P:E5 SP:F9 PPU:235,175 CYC:19977 $FA83:90 61 BCC $FAE6 A:FB X:02 Y:BF P:E5 SP:F9 PPU:241,175 CYC:19979 $FA85:10 5F BPL $FAE6 A:FB X:02 Y:BF P:E5 SP:F9 PPU:247,175 CYC:19981 $FA87:C9 FB CMP #$FB A:FB X:02 Y:BF P:E5 SP:F9 PPU:253,175 CYC:19983 $FA89:D0 5B BNE $FAE6 A:FB X:02 Y:BF P:67 SP:F9 PPU:259,175 CYC:19985 $FA8B:60 RTS A:FB X:02 Y:BF P:67 SP:F9 PPU:265,175 CYC:19987 $EE17:AD 47 06 LDA $0647 = #$4A A:FB X:02 Y:BF P:67 SP:FB PPU:283,175 CYC:19993 $EE1A:C9 4A CMP #$4A A:4A X:02 Y:BF P:65 SP:FB PPU:295,175 CYC:19997 $EE1C:F0 02 BEQ $EE20 A:4A X:02 Y:BF P:67 SP:FB PPU:301,175 CYC:19999 $EE20:C8 INY A:4A X:02 Y:BF P:67 SP:FB PPU:310,175 CYC:20002 $EE21:A9 29 LDA #$29 A:4A X:02 Y:C0 P:E5 SP:FB PPU:316,175 CYC:20004 $EE23:8D 47 06 STA $0647 = #$4A A:29 X:02 Y:C0 P:65 SP:FB PPU:322,175 CYC:20006 $EE26:20 8C FA JSR $FA8C A:29 X:02 Y:C0 P:65 SP:FB PPU:334,175 CYC:20010 $FA8C:B8 CLV A:29 X:02 Y:C0 P:65 SP:F9 PPU: 11,176 CYC:20016 $FA8D:18 CLC A:29 X:02 Y:C0 P:25 SP:F9 PPU: 17,176 CYC:20018 $FA8E:A9 C3 LDA #$C3 A:29 X:02 Y:C0 P:nvUbdIzc SP:F9 PPU: 23,176 CYC:20020 $FA90:60 RTS A:C3 X:02 Y:C0 P:NvUbdIzc SP:F9 PPU: 29,176 CYC:20022 $EE29:03 45 *SLO ($45,X) @ 47 = #$0647 = 29 A:C3 X:02 Y:C0 P:NvUbdIzc SP:FB PPU: 47,176 CYC:20028 $EE2B:EA NOP A:D3 X:02 Y:C0 P:NvUbdIzc SP:FB PPU: 71,176 CYC:20036 $EE2C:EA NOP A:D3 X:02 Y:C0 P:NvUbdIzc SP:FB PPU: 77,176 CYC:20038 $EE2D:EA NOP A:D3 X:02 Y:C0 P:NvUbdIzc SP:FB PPU: 83,176 CYC:20040 $EE2E:EA NOP A:D3 X:02 Y:C0 P:NvUbdIzc SP:FB PPU: 89,176 CYC:20042 $EE2F:20 91 FA JSR $FA91 A:D3 X:02 Y:C0 P:NvUbdIzc SP:FB PPU: 95,176 CYC:20044 $FA91:70 53 BVS $FAE6 A:D3 X:02 Y:C0 P:NvUbdIzc SP:F9 PPU:113,176 CYC:20050 $FA93:F0 51 BEQ $FAE6 A:D3 X:02 Y:C0 P:NvUbdIzc SP:F9 PPU:119,176 CYC:20052 $FA95:10 4F BPL $FAE6 A:D3 X:02 Y:C0 P:NvUbdIzc SP:F9 PPU:125,176 CYC:20054 $FA97:B0 4D BCS $FAE6 A:D3 X:02 Y:C0 P:NvUbdIzc SP:F9 PPU:131,176 CYC:20056 $FA99:C9 D3 CMP #$D3 A:D3 X:02 Y:C0 P:NvUbdIzc SP:F9 PPU:137,176 CYC:20058 $FA9B:D0 49 BNE $FAE6 A:D3 X:02 Y:C0 P:nvUbdIZC SP:F9 PPU:143,176 CYC:20060 $FA9D:60 RTS A:D3 X:02 Y:C0 P:nvUbdIZC SP:F9 PPU:149,176 CYC:20062 $EE32:AD 47 06 LDA $0647 = #$52 A:D3 X:02 Y:C0 P:nvUbdIZC SP:FB PPU:167,176 CYC:20068 $EE35:C9 52 CMP #$52 A:52 X:02 Y:C0 P:25 SP:FB PPU:179,176 CYC:20072 $EE37:F0 02 BEQ $EE3B A:52 X:02 Y:C0 P:nvUbdIZC SP:FB PPU:185,176 CYC:20074 $EE3B:C8 INY A:52 X:02 Y:C0 P:nvUbdIZC SP:FB PPU:194,176 CYC:20077 $EE3C:A9 37 LDA #$37 A:52 X:02 Y:C1 P:A5 SP:FB PPU:200,176 CYC:20079 $EE3E:8D 47 06 STA $0647 = #$52 A:37 X:02 Y:C1 P:25 SP:FB PPU:206,176 CYC:20081 $EE41:20 9E FA JSR $FA9E A:37 X:02 Y:C1 P:25 SP:FB PPU:218,176 CYC:20085 $FA9E:24 01 BIT $01 = #$FF A:37 X:02 Y:C1 P:25 SP:F9 PPU:236,176 CYC:20091 $FAA0:38 SEC A:37 X:02 Y:C1 P:E5 SP:F9 PPU:245,176 CYC:20094 $FAA1:A9 10 LDA #$10 A:37 X:02 Y:C1 P:E5 SP:F9 PPU:251,176 CYC:20096 $FAA3:60 RTS A:10 X:02 Y:C1 P:65 SP:F9 PPU:257,176 CYC:20098 $EE44:03 45 *SLO ($45,X) @ 47 = #$0647 = 37 A:10 X:02 Y:C1 P:65 SP:FB PPU:275,176 CYC:20104 $EE46:EA NOP A:7E X:02 Y:C1 P:64 SP:FB PPU:299,176 CYC:20112 $EE47:EA NOP A:7E X:02 Y:C1 P:64 SP:FB PPU:305,176 CYC:20114 $EE48:EA NOP A:7E X:02 Y:C1 P:64 SP:FB PPU:311,176 CYC:20116 $EE49:EA NOP A:7E X:02 Y:C1 P:64 SP:FB PPU:317,176 CYC:20118 $EE4A:20 A4 FA JSR $FAA4 A:7E X:02 Y:C1 P:64 SP:FB PPU:323,176 CYC:20120 $FAA4:50 40 BVC $FAE6 A:7E X:02 Y:C1 P:64 SP:F9 PPU: 0,177 CYC:20126 $FAA6:F0 3E BEQ $FAE6 A:7E X:02 Y:C1 P:64 SP:F9 PPU: 6,177 CYC:20128 $FAA8:30 3C BMI $FAE6 A:7E X:02 Y:C1 P:64 SP:F9 PPU: 12,177 CYC:20130 $FAAA:B0 3A BCS $FAE6 A:7E X:02 Y:C1 P:64 SP:F9 PPU: 18,177 CYC:20132 $FAAC:C9 7E CMP #$7E A:7E X:02 Y:C1 P:64 SP:F9 PPU: 24,177 CYC:20134 $FAAE:D0 36 BNE $FAE6 A:7E X:02 Y:C1 P:67 SP:F9 PPU: 30,177 CYC:20136 $FAB0:60 RTS A:7E X:02 Y:C1 P:67 SP:F9 PPU: 36,177 CYC:20138 $EE4D:AD 47 06 LDA $0647 = #$6E A:7E X:02 Y:C1 P:67 SP:FB PPU: 54,177 CYC:20144 $EE50:C9 6E CMP #$6E A:6E X:02 Y:C1 P:65 SP:FB PPU: 66,177 CYC:20148 $EE52:F0 02 BEQ $EE56 A:6E X:02 Y:C1 P:67 SP:FB PPU: 72,177 CYC:20150 $EE56:C8 INY A:6E X:02 Y:C1 P:67 SP:FB PPU: 81,177 CYC:20153 $EE57:A9 A5 LDA #$A5 A:6E X:02 Y:C2 P:E5 SP:FB PPU: 87,177 CYC:20155 $EE59:85 47 STA $47 = #$47 A:A5 X:02 Y:C2 P:E5 SP:FB PPU: 93,177 CYC:20157 $EE5B:20 7B FA JSR $FA7B A:A5 X:02 Y:C2 P:E5 SP:FB PPU:102,177 CYC:20160 $FA7B:24 01 BIT $01 = #$FF A:A5 X:02 Y:C2 P:E5 SP:F9 PPU:120,177 CYC:20166 $FA7D:18 CLC A:A5 X:02 Y:C2 P:E5 SP:F9 PPU:129,177 CYC:20169 $FA7E:A9 B3 LDA #$B3 A:A5 X:02 Y:C2 P:NVUbdIzc SP:F9 PPU:135,177 CYC:20171 $FA80:60 RTS A:B3 X:02 Y:C2 P:NVUbdIzc SP:F9 PPU:141,177 CYC:20173 $EE5E:07 47 *SLO $47 = #$A5 A:B3 X:02 Y:C2 P:NVUbdIzc SP:FB PPU:159,177 CYC:20179 $EE60:EA NOP A:FB X:02 Y:C2 P:E5 SP:FB PPU:174,177 CYC:20184 $EE61:EA NOP A:FB X:02 Y:C2 P:E5 SP:FB PPU:180,177 CYC:20186 $EE62:EA NOP A:FB X:02 Y:C2 P:E5 SP:FB PPU:186,177 CYC:20188 $EE63:EA NOP A:FB X:02 Y:C2 P:E5 SP:FB PPU:192,177 CYC:20190 $EE64:20 81 FA JSR $FA81 A:FB X:02 Y:C2 P:E5 SP:FB PPU:198,177 CYC:20192 $FA81:50 63 BVC $FAE6 A:FB X:02 Y:C2 P:E5 SP:F9 PPU:216,177 CYC:20198 $FA83:90 61 BCC $FAE6 A:FB X:02 Y:C2 P:E5 SP:F9 PPU:222,177 CYC:20200 $FA85:10 5F BPL $FAE6 A:FB X:02 Y:C2 P:E5 SP:F9 PPU:228,177 CYC:20202 $FA87:C9 FB CMP #$FB A:FB X:02 Y:C2 P:E5 SP:F9 PPU:234,177 CYC:20204 $FA89:D0 5B BNE $FAE6 A:FB X:02 Y:C2 P:67 SP:F9 PPU:240,177 CYC:20206 $FA8B:60 RTS A:FB X:02 Y:C2 P:67 SP:F9 PPU:246,177 CYC:20208 $EE67:A5 47 LDA $47 = #$4A A:FB X:02 Y:C2 P:67 SP:FB PPU:264,177 CYC:20214 $EE69:C9 4A CMP #$4A A:4A X:02 Y:C2 P:65 SP:FB PPU:273,177 CYC:20217 $EE6B:F0 02 BEQ $EE6F A:4A X:02 Y:C2 P:67 SP:FB PPU:279,177 CYC:20219 $EE6F:C8 INY A:4A X:02 Y:C2 P:67 SP:FB PPU:288,177 CYC:20222 $EE70:A9 29 LDA #$29 A:4A X:02 Y:C3 P:E5 SP:FB PPU:294,177 CYC:20224 $EE72:85 47 STA $47 = #$4A A:29 X:02 Y:C3 P:65 SP:FB PPU:300,177 CYC:20226 $EE74:20 8C FA JSR $FA8C A:29 X:02 Y:C3 P:65 SP:FB PPU:309,177 CYC:20229 $FA8C:B8 CLV A:29 X:02 Y:C3 P:65 SP:F9 PPU:327,177 CYC:20235 $FA8D:18 CLC A:29 X:02 Y:C3 P:25 SP:F9 PPU:333,177 CYC:20237 $FA8E:A9 C3 LDA #$C3 A:29 X:02 Y:C3 P:nvUbdIzc SP:F9 PPU:339,177 CYC:20239 $FA90:60 RTS A:C3 X:02 Y:C3 P:NvUbdIzc SP:F9 PPU: 4,178 CYC:20241 $EE77:07 47 *SLO $47 = #$29 A:C3 X:02 Y:C3 P:NvUbdIzc SP:FB PPU: 22,178 CYC:20247 $EE79:EA NOP A:D3 X:02 Y:C3 P:NvUbdIzc SP:FB PPU: 37,178 CYC:20252 $EE7A:EA NOP A:D3 X:02 Y:C3 P:NvUbdIzc SP:FB PPU: 43,178 CYC:20254 $EE7B:EA NOP A:D3 X:02 Y:C3 P:NvUbdIzc SP:FB PPU: 49,178 CYC:20256 $EE7C:EA NOP A:D3 X:02 Y:C3 P:NvUbdIzc SP:FB PPU: 55,178 CYC:20258 $EE7D:20 91 FA JSR $FA91 A:D3 X:02 Y:C3 P:NvUbdIzc SP:FB PPU: 61,178 CYC:20260 $FA91:70 53 BVS $FAE6 A:D3 X:02 Y:C3 P:NvUbdIzc SP:F9 PPU: 79,178 CYC:20266 $FA93:F0 51 BEQ $FAE6 A:D3 X:02 Y:C3 P:NvUbdIzc SP:F9 PPU: 85,178 CYC:20268 $FA95:10 4F BPL $FAE6 A:D3 X:02 Y:C3 P:NvUbdIzc SP:F9 PPU: 91,178 CYC:20270 $FA97:B0 4D BCS $FAE6 A:D3 X:02 Y:C3 P:NvUbdIzc SP:F9 PPU: 97,178 CYC:20272 $FA99:C9 D3 CMP #$D3 A:D3 X:02 Y:C3 P:NvUbdIzc SP:F9 PPU:103,178 CYC:20274 $FA9B:D0 49 BNE $FAE6 A:D3 X:02 Y:C3 P:nvUbdIZC SP:F9 PPU:109,178 CYC:20276 $FA9D:60 RTS A:D3 X:02 Y:C3 P:nvUbdIZC SP:F9 PPU:115,178 CYC:20278 $EE80:A5 47 LDA $47 = #$52 A:D3 X:02 Y:C3 P:nvUbdIZC SP:FB PPU:133,178 CYC:20284 $EE82:C9 52 CMP #$52 A:52 X:02 Y:C3 P:25 SP:FB PPU:142,178 CYC:20287 $EE84:F0 02 BEQ $EE88 A:52 X:02 Y:C3 P:nvUbdIZC SP:FB PPU:148,178 CYC:20289 $EE88:C8 INY A:52 X:02 Y:C3 P:nvUbdIZC SP:FB PPU:157,178 CYC:20292 $EE89:A9 37 LDA #$37 A:52 X:02 Y:C4 P:A5 SP:FB PPU:163,178 CYC:20294 $EE8B:85 47 STA $47 = #$52 A:37 X:02 Y:C4 P:25 SP:FB PPU:169,178 CYC:20296 $EE8D:20 9E FA JSR $FA9E A:37 X:02 Y:C4 P:25 SP:FB PPU:178,178 CYC:20299 $FA9E:24 01 BIT $01 = #$FF A:37 X:02 Y:C4 P:25 SP:F9 PPU:196,178 CYC:20305 $FAA0:38 SEC A:37 X:02 Y:C4 P:E5 SP:F9 PPU:205,178 CYC:20308 $FAA1:A9 10 LDA #$10 A:37 X:02 Y:C4 P:E5 SP:F9 PPU:211,178 CYC:20310 $FAA3:60 RTS A:10 X:02 Y:C4 P:65 SP:F9 PPU:217,178 CYC:20312 $EE90:07 47 *SLO $47 = #$37 A:10 X:02 Y:C4 P:65 SP:FB PPU:235,178 CYC:20318 $EE92:EA NOP A:7E X:02 Y:C4 P:64 SP:FB PPU:250,178 CYC:20323 $EE93:EA NOP A:7E X:02 Y:C4 P:64 SP:FB PPU:256,178 CYC:20325 $EE94:EA NOP A:7E X:02 Y:C4 P:64 SP:FB PPU:262,178 CYC:20327 $EE95:EA NOP A:7E X:02 Y:C4 P:64 SP:FB PPU:268,178 CYC:20329 $EE96:20 A4 FA JSR $FAA4 A:7E X:02 Y:C4 P:64 SP:FB PPU:274,178 CYC:20331 $FAA4:50 40 BVC $FAE6 A:7E X:02 Y:C4 P:64 SP:F9 PPU:292,178 CYC:20337 $FAA6:F0 3E BEQ $FAE6 A:7E X:02 Y:C4 P:64 SP:F9 PPU:298,178 CYC:20339 $FAA8:30 3C BMI $FAE6 A:7E X:02 Y:C4 P:64 SP:F9 PPU:304,178 CYC:20341 $FAAA:B0 3A BCS $FAE6 A:7E X:02 Y:C4 P:64 SP:F9 PPU:310,178 CYC:20343 $FAAC:C9 7E CMP #$7E A:7E X:02 Y:C4 P:64 SP:F9 PPU:316,178 CYC:20345 $FAAE:D0 36 BNE $FAE6 A:7E X:02 Y:C4 P:67 SP:F9 PPU:322,178 CYC:20347 $FAB0:60 RTS A:7E X:02 Y:C4 P:67 SP:F9 PPU:328,178 CYC:20349 $EE99:A5 47 LDA $47 = #$6E A:7E X:02 Y:C4 P:67 SP:FB PPU: 5,179 CYC:20355 $EE9B:C9 6E CMP #$6E A:6E X:02 Y:C4 P:65 SP:FB PPU: 14,179 CYC:20358 $EE9D:F0 02 BEQ $EEA1 A:6E X:02 Y:C4 P:67 SP:FB PPU: 20,179 CYC:20360 $EEA1:C8 INY A:6E X:02 Y:C4 P:67 SP:FB PPU: 29,179 CYC:20363 $EEA2:A9 A5 LDA #$A5 A:6E X:02 Y:C5 P:E5 SP:FB PPU: 35,179 CYC:20365 $EEA4:8D 47 06 STA $0647 = #$6E A:A5 X:02 Y:C5 P:E5 SP:FB PPU: 41,179 CYC:20367 $EEA7:20 7B FA JSR $FA7B A:A5 X:02 Y:C5 P:E5 SP:FB PPU: 53,179 CYC:20371 $FA7B:24 01 BIT $01 = #$FF A:A5 X:02 Y:C5 P:E5 SP:F9 PPU: 71,179 CYC:20377 $FA7D:18 CLC A:A5 X:02 Y:C5 P:E5 SP:F9 PPU: 80,179 CYC:20380 $FA7E:A9 B3 LDA #$B3 A:A5 X:02 Y:C5 P:NVUbdIzc SP:F9 PPU: 86,179 CYC:20382 $FA80:60 RTS A:B3 X:02 Y:C5 P:NVUbdIzc SP:F9 PPU: 92,179 CYC:20384 $EEAA:0F 47 06 *SLO $0647 = #$A5 A:B3 X:02 Y:C5 P:NVUbdIzc SP:FB PPU:110,179 CYC:20390 $EEAD:EA NOP A:FB X:02 Y:C5 P:E5 SP:FB PPU:128,179 CYC:20396 $EEAE:EA NOP A:FB X:02 Y:C5 P:E5 SP:FB PPU:134,179 CYC:20398 $EEAF:EA NOP A:FB X:02 Y:C5 P:E5 SP:FB PPU:140,179 CYC:20400 $EEB0:EA NOP A:FB X:02 Y:C5 P:E5 SP:FB PPU:146,179 CYC:20402 $EEB1:20 81 FA JSR $FA81 A:FB X:02 Y:C5 P:E5 SP:FB PPU:152,179 CYC:20404 $FA81:50 63 BVC $FAE6 A:FB X:02 Y:C5 P:E5 SP:F9 PPU:170,179 CYC:20410 $FA83:90 61 BCC $FAE6 A:FB X:02 Y:C5 P:E5 SP:F9 PPU:176,179 CYC:20412 $FA85:10 5F BPL $FAE6 A:FB X:02 Y:C5 P:E5 SP:F9 PPU:182,179 CYC:20414 $FA87:C9 FB CMP #$FB A:FB X:02 Y:C5 P:E5 SP:F9 PPU:188,179 CYC:20416 $FA89:D0 5B BNE $FAE6 A:FB X:02 Y:C5 P:67 SP:F9 PPU:194,179 CYC:20418 $FA8B:60 RTS A:FB X:02 Y:C5 P:67 SP:F9 PPU:200,179 CYC:20420 $EEB4:AD 47 06 LDA $0647 = #$4A A:FB X:02 Y:C5 P:67 SP:FB PPU:218,179 CYC:20426 $EEB7:C9 4A CMP #$4A A:4A X:02 Y:C5 P:65 SP:FB PPU:230,179 CYC:20430 $EEB9:F0 02 BEQ $EEBD A:4A X:02 Y:C5 P:67 SP:FB PPU:236,179 CYC:20432 $EEBD:C8 INY A:4A X:02 Y:C5 P:67 SP:FB PPU:245,179 CYC:20435 $EEBE:A9 29 LDA #$29 A:4A X:02 Y:C6 P:E5 SP:FB PPU:251,179 CYC:20437 $EEC0:8D 47 06 STA $0647 = #$4A A:29 X:02 Y:C6 P:65 SP:FB PPU:257,179 CYC:20439 $EEC3:20 8C FA JSR $FA8C A:29 X:02 Y:C6 P:65 SP:FB PPU:269,179 CYC:20443 $FA8C:B8 CLV A:29 X:02 Y:C6 P:65 SP:F9 PPU:287,179 CYC:20449 $FA8D:18 CLC A:29 X:02 Y:C6 P:25 SP:F9 PPU:293,179 CYC:20451 $FA8E:A9 C3 LDA #$C3 A:29 X:02 Y:C6 P:nvUbdIzc SP:F9 PPU:299,179 CYC:20453 $FA90:60 RTS A:C3 X:02 Y:C6 P:NvUbdIzc SP:F9 PPU:305,179 CYC:20455 $EEC6:0F 47 06 *SLO $0647 = #$29 A:C3 X:02 Y:C6 P:NvUbdIzc SP:FB PPU:323,179 CYC:20461 $EEC9:EA NOP A:D3 X:02 Y:C6 P:NvUbdIzc SP:FB PPU: 0,180 CYC:20467 $EECA:EA NOP A:D3 X:02 Y:C6 P:NvUbdIzc SP:FB PPU: 6,180 CYC:20469 $EECB:EA NOP A:D3 X:02 Y:C6 P:NvUbdIzc SP:FB PPU: 12,180 CYC:20471 $EECC:EA NOP A:D3 X:02 Y:C6 P:NvUbdIzc SP:FB PPU: 18,180 CYC:20473 $EECD:20 91 FA JSR $FA91 A:D3 X:02 Y:C6 P:NvUbdIzc SP:FB PPU: 24,180 CYC:20475 $FA91:70 53 BVS $FAE6 A:D3 X:02 Y:C6 P:NvUbdIzc SP:F9 PPU: 42,180 CYC:20481 $FA93:F0 51 BEQ $FAE6 A:D3 X:02 Y:C6 P:NvUbdIzc SP:F9 PPU: 48,180 CYC:20483 $FA95:10 4F BPL $FAE6 A:D3 X:02 Y:C6 P:NvUbdIzc SP:F9 PPU: 54,180 CYC:20485 $FA97:B0 4D BCS $FAE6 A:D3 X:02 Y:C6 P:NvUbdIzc SP:F9 PPU: 60,180 CYC:20487 $FA99:C9 D3 CMP #$D3 A:D3 X:02 Y:C6 P:NvUbdIzc SP:F9 PPU: 66,180 CYC:20489 $FA9B:D0 49 BNE $FAE6 A:D3 X:02 Y:C6 P:nvUbdIZC SP:F9 PPU: 72,180 CYC:20491 $FA9D:60 RTS A:D3 X:02 Y:C6 P:nvUbdIZC SP:F9 PPU: 78,180 CYC:20493 $EED0:AD 47 06 LDA $0647 = #$52 A:D3 X:02 Y:C6 P:nvUbdIZC SP:FB PPU: 96,180 CYC:20499 $EED3:C9 52 CMP #$52 A:52 X:02 Y:C6 P:25 SP:FB PPU:108,180 CYC:20503 $EED5:F0 02 BEQ $EED9 A:52 X:02 Y:C6 P:nvUbdIZC SP:FB PPU:114,180 CYC:20505 $EED9:C8 INY A:52 X:02 Y:C6 P:nvUbdIZC SP:FB PPU:123,180 CYC:20508 $EEDA:A9 37 LDA #$37 A:52 X:02 Y:C7 P:A5 SP:FB PPU:129,180 CYC:20510 $EEDC:8D 47 06 STA $0647 = #$52 A:37 X:02 Y:C7 P:25 SP:FB PPU:135,180 CYC:20512 $EEDF:20 9E FA JSR $FA9E A:37 X:02 Y:C7 P:25 SP:FB PPU:147,180 CYC:20516 $FA9E:24 01 BIT $01 = #$FF A:37 X:02 Y:C7 P:25 SP:F9 PPU:165,180 CYC:20522 $FAA0:38 SEC A:37 X:02 Y:C7 P:E5 SP:F9 PPU:174,180 CYC:20525 $FAA1:A9 10 LDA #$10 A:37 X:02 Y:C7 P:E5 SP:F9 PPU:180,180 CYC:20527 $FAA3:60 RTS A:10 X:02 Y:C7 P:65 SP:F9 PPU:186,180 CYC:20529 $EEE2:0F 47 06 *SLO $0647 = #$37 A:10 X:02 Y:C7 P:65 SP:FB PPU:204,180 CYC:20535 $EEE5:EA NOP A:7E X:02 Y:C7 P:64 SP:FB PPU:222,180 CYC:20541 $EEE6:EA NOP A:7E X:02 Y:C7 P:64 SP:FB PPU:228,180 CYC:20543 $EEE7:EA NOP A:7E X:02 Y:C7 P:64 SP:FB PPU:234,180 CYC:20545 $EEE8:EA NOP A:7E X:02 Y:C7 P:64 SP:FB PPU:240,180 CYC:20547 $EEE9:20 A4 FA JSR $FAA4 A:7E X:02 Y:C7 P:64 SP:FB PPU:246,180 CYC:20549 $FAA4:50 40 BVC $FAE6 A:7E X:02 Y:C7 P:64 SP:F9 PPU:264,180 CYC:20555 $FAA6:F0 3E BEQ $FAE6 A:7E X:02 Y:C7 P:64 SP:F9 PPU:270,180 CYC:20557 $FAA8:30 3C BMI $FAE6 A:7E X:02 Y:C7 P:64 SP:F9 PPU:276,180 CYC:20559 $FAAA:B0 3A BCS $FAE6 A:7E X:02 Y:C7 P:64 SP:F9 PPU:282,180 CYC:20561 $FAAC:C9 7E CMP #$7E A:7E X:02 Y:C7 P:64 SP:F9 PPU:288,180 CYC:20563 $FAAE:D0 36 BNE $FAE6 A:7E X:02 Y:C7 P:67 SP:F9 PPU:294,180 CYC:20565 $FAB0:60 RTS A:7E X:02 Y:C7 P:67 SP:F9 PPU:300,180 CYC:20567 $EEEC:AD 47 06 LDA $0647 = #$6E A:7E X:02 Y:C7 P:67 SP:FB PPU:318,180 CYC:20573 $EEEF:C9 6E CMP #$6E A:6E X:02 Y:C7 P:65 SP:FB PPU:330,180 CYC:20577 $EEF1:F0 02 BEQ $EEF5 A:6E X:02 Y:C7 P:67 SP:FB PPU:336,180 CYC:20579 $EEF5:A9 A5 LDA #$A5 A:6E X:02 Y:C7 P:67 SP:FB PPU: 4,181 CYC:20582 $EEF7:8D 47 06 STA $0647 = #$6E A:A5 X:02 Y:C7 P:E5 SP:FB PPU: 10,181 CYC:20584 $EEFA:A9 48 LDA #$48 A:A5 X:02 Y:C7 P:E5 SP:FB PPU: 22,181 CYC:20588 $EEFC:85 45 STA $45 = #$48 A:48 X:02 Y:C7 P:65 SP:FB PPU: 28,181 CYC:20590 $EEFE:A9 05 LDA #$05 A:48 X:02 Y:C7 P:65 SP:FB PPU: 37,181 CYC:20593 $EF00:85 46 STA $46 = #$05 A:05 X:02 Y:C7 P:65 SP:FB PPU: 43,181 CYC:20595 $EF02:A0 FF LDY #$FF A:05 X:02 Y:C7 P:65 SP:FB PPU: 52,181 CYC:20598 $EF04:20 7B FA JSR $FA7B A:05 X:02 Y:FF P:E5 SP:FB PPU: 58,181 CYC:20600 $FA7B:24 01 BIT $01 = #$FF A:05 X:02 Y:FF P:E5 SP:F9 PPU: 76,181 CYC:20606 $FA7D:18 CLC A:05 X:02 Y:FF P:E5 SP:F9 PPU: 85,181 CYC:20609 $FA7E:A9 B3 LDA #$B3 A:05 X:02 Y:FF P:NVUbdIzc SP:F9 PPU: 91,181 CYC:20611 $FA80:60 RTS A:B3 X:02 Y:FF P:NVUbdIzc SP:F9 PPU: 97,181 CYC:20613 $EF07:13 45 *SLO ($45),Y = #$0548 @ 0647 = A5 A:B3 X:02 Y:FF P:NVUbdIzc SP:FB PPU:115,181 CYC:20619 $EF09:EA NOP A:FB X:02 Y:FF P:E5 SP:FB PPU:139,181 CYC:20627 $EF0A:EA NOP A:FB X:02 Y:FF P:E5 SP:FB PPU:145,181 CYC:20629 $EF0B:08 PHP A:FB X:02 Y:FF P:E5 SP:FB PPU:151,181 CYC:20631 $EF0C:48 PHA A:FB X:02 Y:FF P:E5 SP:FA PPU:160,181 CYC:20634 $EF0D:A0 C8 LDY #$C8 A:FB X:02 Y:FF P:E5 SP:F9 PPU:169,181 CYC:20637 $EF0F:68 PLA A:FB X:02 Y:C8 P:E5 SP:F9 PPU:175,181 CYC:20639 $EF10:28 PLP A:FB X:02 Y:C8 P:E5 SP:FA PPU:187,181 CYC:20643 $EF11:20 81 FA JSR $FA81 A:FB X:02 Y:C8 P:E5 SP:FB PPU:199,181 CYC:20647 $FA81:50 63 BVC $FAE6 A:FB X:02 Y:C8 P:E5 SP:F9 PPU:217,181 CYC:20653 $FA83:90 61 BCC $FAE6 A:FB X:02 Y:C8 P:E5 SP:F9 PPU:223,181 CYC:20655 $FA85:10 5F BPL $FAE6 A:FB X:02 Y:C8 P:E5 SP:F9 PPU:229,181 CYC:20657 $FA87:C9 FB CMP #$FB A:FB X:02 Y:C8 P:E5 SP:F9 PPU:235,181 CYC:20659 $FA89:D0 5B BNE $FAE6 A:FB X:02 Y:C8 P:67 SP:F9 PPU:241,181 CYC:20661 $FA8B:60 RTS A:FB X:02 Y:C8 P:67 SP:F9 PPU:247,181 CYC:20663 $EF14:AD 47 06 LDA $0647 = #$4A A:FB X:02 Y:C8 P:67 SP:FB PPU:265,181 CYC:20669 $EF17:C9 4A CMP #$4A A:4A X:02 Y:C8 P:65 SP:FB PPU:277,181 CYC:20673 $EF19:F0 02 BEQ $EF1D A:4A X:02 Y:C8 P:67 SP:FB PPU:283,181 CYC:20675 $EF1D:A0 FF LDY #$FF A:4A X:02 Y:C8 P:67 SP:FB PPU:292,181 CYC:20678 $EF1F:A9 29 LDA #$29 A:4A X:02 Y:FF P:E5 SP:FB PPU:298,181 CYC:20680 $EF21:8D 47 06 STA $0647 = #$4A A:29 X:02 Y:FF P:65 SP:FB PPU:304,181 CYC:20682 $EF24:20 8C FA JSR $FA8C A:29 X:02 Y:FF P:65 SP:FB PPU:316,181 CYC:20686 $FA8C:B8 CLV A:29 X:02 Y:FF P:65 SP:F9 PPU:334,181 CYC:20692 $FA8D:18 CLC A:29 X:02 Y:FF P:25 SP:F9 PPU:340,181 CYC:20694 $FA8E:A9 C3 LDA #$C3 A:29 X:02 Y:FF P:nvUbdIzc SP:F9 PPU: 5,182 CYC:20696 $FA90:60 RTS A:C3 X:02 Y:FF P:NvUbdIzc SP:F9 PPU: 11,182 CYC:20698 $EF27:13 45 *SLO ($45),Y = #$0548 @ 0647 = 29 A:C3 X:02 Y:FF P:NvUbdIzc SP:FB PPU: 29,182 CYC:20704 $EF29:EA NOP A:D3 X:02 Y:FF P:NvUbdIzc SP:FB PPU: 53,182 CYC:20712 $EF2A:EA NOP A:D3 X:02 Y:FF P:NvUbdIzc SP:FB PPU: 59,182 CYC:20714 $EF2B:08 PHP A:D3 X:02 Y:FF P:NvUbdIzc SP:FB PPU: 65,182 CYC:20716 $EF2C:48 PHA A:D3 X:02 Y:FF P:NvUbdIzc SP:FA PPU: 74,182 CYC:20719 $EF2D:A0 C9 LDY #$C9 A:D3 X:02 Y:FF P:NvUbdIzc SP:F9 PPU: 83,182 CYC:20722 $EF2F:68 PLA A:D3 X:02 Y:C9 P:NvUbdIzc SP:F9 PPU: 89,182 CYC:20724 $EF30:28 PLP A:D3 X:02 Y:C9 P:NvUbdIzc SP:FA PPU:101,182 CYC:20728 $EF31:20 91 FA JSR $FA91 A:D3 X:02 Y:C9 P:NvUbdIzc SP:FB PPU:113,182 CYC:20732 $FA91:70 53 BVS $FAE6 A:D3 X:02 Y:C9 P:NvUbdIzc SP:F9 PPU:131,182 CYC:20738 $FA93:F0 51 BEQ $FAE6 A:D3 X:02 Y:C9 P:NvUbdIzc SP:F9 PPU:137,182 CYC:20740 $FA95:10 4F BPL $FAE6 A:D3 X:02 Y:C9 P:NvUbdIzc SP:F9 PPU:143,182 CYC:20742 $FA97:B0 4D BCS $FAE6 A:D3 X:02 Y:C9 P:NvUbdIzc SP:F9 PPU:149,182 CYC:20744 $FA99:C9 D3 CMP #$D3 A:D3 X:02 Y:C9 P:NvUbdIzc SP:F9 PPU:155,182 CYC:20746 $FA9B:D0 49 BNE $FAE6 A:D3 X:02 Y:C9 P:nvUbdIZC SP:F9 PPU:161,182 CYC:20748 $FA9D:60 RTS A:D3 X:02 Y:C9 P:nvUbdIZC SP:F9 PPU:167,182 CYC:20750 $EF34:AD 47 06 LDA $0647 = #$52 A:D3 X:02 Y:C9 P:nvUbdIZC SP:FB PPU:185,182 CYC:20756 $EF37:C9 52 CMP #$52 A:52 X:02 Y:C9 P:25 SP:FB PPU:197,182 CYC:20760 $EF39:F0 02 BEQ $EF3D A:52 X:02 Y:C9 P:nvUbdIZC SP:FB PPU:203,182 CYC:20762 $EF3D:A0 FF LDY #$FF A:52 X:02 Y:C9 P:nvUbdIZC SP:FB PPU:212,182 CYC:20765 $EF3F:A9 37 LDA #$37 A:52 X:02 Y:FF P:A5 SP:FB PPU:218,182 CYC:20767 $EF41:8D 47 06 STA $0647 = #$52 A:37 X:02 Y:FF P:25 SP:FB PPU:224,182 CYC:20769 $EF44:20 9E FA JSR $FA9E A:37 X:02 Y:FF P:25 SP:FB PPU:236,182 CYC:20773 $FA9E:24 01 BIT $01 = #$FF A:37 X:02 Y:FF P:25 SP:F9 PPU:254,182 CYC:20779 $FAA0:38 SEC A:37 X:02 Y:FF P:E5 SP:F9 PPU:263,182 CYC:20782 $FAA1:A9 10 LDA #$10 A:37 X:02 Y:FF P:E5 SP:F9 PPU:269,182 CYC:20784 $FAA3:60 RTS A:10 X:02 Y:FF P:65 SP:F9 PPU:275,182 CYC:20786 $EF47:13 45 *SLO ($45),Y = #$0548 @ 0647 = 37 A:10 X:02 Y:FF P:65 SP:FB PPU:293,182 CYC:20792 $EF49:EA NOP A:7E X:02 Y:FF P:64 SP:FB PPU:317,182 CYC:20800 $EF4A:EA NOP A:7E X:02 Y:FF P:64 SP:FB PPU:323,182 CYC:20802 $EF4B:08 PHP A:7E X:02 Y:FF P:64 SP:FB PPU:329,182 CYC:20804 $EF4C:48 PHA A:7E X:02 Y:FF P:64 SP:FA PPU:338,182 CYC:20807 $EF4D:A0 CA LDY #$CA A:7E X:02 Y:FF P:64 SP:F9 PPU: 6,183 CYC:20810 $EF4F:68 PLA A:7E X:02 Y:CA P:NVUbdIzc SP:F9 PPU: 12,183 CYC:20812 $EF50:28 PLP A:7E X:02 Y:CA P:64 SP:FA PPU: 24,183 CYC:20816 $EF51:20 A4 FA JSR $FAA4 A:7E X:02 Y:CA P:64 SP:FB PPU: 36,183 CYC:20820 $FAA4:50 40 BVC $FAE6 A:7E X:02 Y:CA P:64 SP:F9 PPU: 54,183 CYC:20826 $FAA6:F0 3E BEQ $FAE6 A:7E X:02 Y:CA P:64 SP:F9 PPU: 60,183 CYC:20828 $FAA8:30 3C BMI $FAE6 A:7E X:02 Y:CA P:64 SP:F9 PPU: 66,183 CYC:20830 $FAAA:B0 3A BCS $FAE6 A:7E X:02 Y:CA P:64 SP:F9 PPU: 72,183 CYC:20832 $FAAC:C9 7E CMP #$7E A:7E X:02 Y:CA P:64 SP:F9 PPU: 78,183 CYC:20834 $FAAE:D0 36 BNE $FAE6 A:7E X:02 Y:CA P:67 SP:F9 PPU: 84,183 CYC:20836 $FAB0:60 RTS A:7E X:02 Y:CA P:67 SP:F9 PPU: 90,183 CYC:20838 $EF54:AD 47 06 LDA $0647 = #$6E A:7E X:02 Y:CA P:67 SP:FB PPU:108,183 CYC:20844 $EF57:C9 6E CMP #$6E A:6E X:02 Y:CA P:65 SP:FB PPU:120,183 CYC:20848 $EF59:F0 02 BEQ $EF5D A:6E X:02 Y:CA P:67 SP:FB PPU:126,183 CYC:20850 $EF5D:A0 CB LDY #$CB A:6E X:02 Y:CA P:67 SP:FB PPU:135,183 CYC:20853 $EF5F:A2 FF LDX #$FF A:6E X:02 Y:CB P:E5 SP:FB PPU:141,183 CYC:20855 $EF61:A9 A5 LDA #$A5 A:6E X:FF Y:CB P:E5 SP:FB PPU:147,183 CYC:20857 $EF63:85 47 STA $47 = #$6E A:A5 X:FF Y:CB P:E5 SP:FB PPU:153,183 CYC:20859 $EF65:20 7B FA JSR $FA7B A:A5 X:FF Y:CB P:E5 SP:FB PPU:162,183 CYC:20862 $FA7B:24 01 BIT $01 = #$FF A:A5 X:FF Y:CB P:E5 SP:F9 PPU:180,183 CYC:20868 $FA7D:18 CLC A:A5 X:FF Y:CB P:E5 SP:F9 PPU:189,183 CYC:20871 $FA7E:A9 B3 LDA #$B3 A:A5 X:FF Y:CB P:NVUbdIzc SP:F9 PPU:195,183 CYC:20873 $FA80:60 RTS A:B3 X:FF Y:CB P:NVUbdIzc SP:F9 PPU:201,183 CYC:20875 $EF68:17 48 *SLO $48,X @ 47 = #$A5 A:B3 X:FF Y:CB P:NVUbdIzc SP:FB PPU:219,183 CYC:20881 $EF6A:EA NOP A:FB X:FF Y:CB P:E5 SP:FB PPU:237,183 CYC:20887 $EF6B:EA NOP A:FB X:FF Y:CB P:E5 SP:FB PPU:243,183 CYC:20889 $EF6C:EA NOP A:FB X:FF Y:CB P:E5 SP:FB PPU:249,183 CYC:20891 $EF6D:EA NOP A:FB X:FF Y:CB P:E5 SP:FB PPU:255,183 CYC:20893 $EF6E:20 81 FA JSR $FA81 A:FB X:FF Y:CB P:E5 SP:FB PPU:261,183 CYC:20895 $FA81:50 63 BVC $FAE6 A:FB X:FF Y:CB P:E5 SP:F9 PPU:279,183 CYC:20901 $FA83:90 61 BCC $FAE6 A:FB X:FF Y:CB P:E5 SP:F9 PPU:285,183 CYC:20903 $FA85:10 5F BPL $FAE6 A:FB X:FF Y:CB P:E5 SP:F9 PPU:291,183 CYC:20905 $FA87:C9 FB CMP #$FB A:FB X:FF Y:CB P:E5 SP:F9 PPU:297,183 CYC:20907 $FA89:D0 5B BNE $FAE6 A:FB X:FF Y:CB P:67 SP:F9 PPU:303,183 CYC:20909 $FA8B:60 RTS A:FB X:FF Y:CB P:67 SP:F9 PPU:309,183 CYC:20911 $EF71:A5 47 LDA $47 = #$4A A:FB X:FF Y:CB P:67 SP:FB PPU:327,183 CYC:20917 $EF73:C9 4A CMP #$4A A:4A X:FF Y:CB P:65 SP:FB PPU:336,183 CYC:20920 $EF75:F0 02 BEQ $EF79 A:4A X:FF Y:CB P:67 SP:FB PPU: 1,184 CYC:20922 $EF79:C8 INY A:4A X:FF Y:CB P:67 SP:FB PPU: 10,184 CYC:20925 $EF7A:A9 29 LDA #$29 A:4A X:FF Y:CC P:E5 SP:FB PPU: 16,184 CYC:20927 $EF7C:85 47 STA $47 = #$4A A:29 X:FF Y:CC P:65 SP:FB PPU: 22,184 CYC:20929 $EF7E:20 8C FA JSR $FA8C A:29 X:FF Y:CC P:65 SP:FB PPU: 31,184 CYC:20932 $FA8C:B8 CLV A:29 X:FF Y:CC P:65 SP:F9 PPU: 49,184 CYC:20938 $FA8D:18 CLC A:29 X:FF Y:CC P:25 SP:F9 PPU: 55,184 CYC:20940 $FA8E:A9 C3 LDA #$C3 A:29 X:FF Y:CC P:nvUbdIzc SP:F9 PPU: 61,184 CYC:20942 $FA90:60 RTS A:C3 X:FF Y:CC P:NvUbdIzc SP:F9 PPU: 67,184 CYC:20944 $EF81:17 48 *SLO $48,X @ 47 = #$29 A:C3 X:FF Y:CC P:NvUbdIzc SP:FB PPU: 85,184 CYC:20950 $EF83:EA NOP A:D3 X:FF Y:CC P:NvUbdIzc SP:FB PPU:103,184 CYC:20956 $EF84:EA NOP A:D3 X:FF Y:CC P:NvUbdIzc SP:FB PPU:109,184 CYC:20958 $EF85:EA NOP A:D3 X:FF Y:CC P:NvUbdIzc SP:FB PPU:115,184 CYC:20960 $EF86:EA NOP A:D3 X:FF Y:CC P:NvUbdIzc SP:FB PPU:121,184 CYC:20962 $EF87:20 91 FA JSR $FA91 A:D3 X:FF Y:CC P:NvUbdIzc SP:FB PPU:127,184 CYC:20964 $FA91:70 53 BVS $FAE6 A:D3 X:FF Y:CC P:NvUbdIzc SP:F9 PPU:145,184 CYC:20970 $FA93:F0 51 BEQ $FAE6 A:D3 X:FF Y:CC P:NvUbdIzc SP:F9 PPU:151,184 CYC:20972 $FA95:10 4F BPL $FAE6 A:D3 X:FF Y:CC P:NvUbdIzc SP:F9 PPU:157,184 CYC:20974 $FA97:B0 4D BCS $FAE6 A:D3 X:FF Y:CC P:NvUbdIzc SP:F9 PPU:163,184 CYC:20976 $FA99:C9 D3 CMP #$D3 A:D3 X:FF Y:CC P:NvUbdIzc SP:F9 PPU:169,184 CYC:20978 $FA9B:D0 49 BNE $FAE6 A:D3 X:FF Y:CC P:nvUbdIZC SP:F9 PPU:175,184 CYC:20980 $FA9D:60 RTS A:D3 X:FF Y:CC P:nvUbdIZC SP:F9 PPU:181,184 CYC:20982 $EF8A:A5 47 LDA $47 = #$52 A:D3 X:FF Y:CC P:nvUbdIZC SP:FB PPU:199,184 CYC:20988 $EF8C:C9 52 CMP #$52 A:52 X:FF Y:CC P:25 SP:FB PPU:208,184 CYC:20991 $EF8E:F0 02 BEQ $EF92 A:52 X:FF Y:CC P:nvUbdIZC SP:FB PPU:214,184 CYC:20993 $EF92:C8 INY A:52 X:FF Y:CC P:nvUbdIZC SP:FB PPU:223,184 CYC:20996 $EF93:A9 37 LDA #$37 A:52 X:FF Y:CD P:A5 SP:FB PPU:229,184 CYC:20998 $EF95:85 47 STA $47 = #$52 A:37 X:FF Y:CD P:25 SP:FB PPU:235,184 CYC:21000 $EF97:20 9E FA JSR $FA9E A:37 X:FF Y:CD P:25 SP:FB PPU:244,184 CYC:21003 $FA9E:24 01 BIT $01 = #$FF A:37 X:FF Y:CD P:25 SP:F9 PPU:262,184 CYC:21009 $FAA0:38 SEC A:37 X:FF Y:CD P:E5 SP:F9 PPU:271,184 CYC:21012 $FAA1:A9 10 LDA #$10 A:37 X:FF Y:CD P:E5 SP:F9 PPU:277,184 CYC:21014 $FAA3:60 RTS A:10 X:FF Y:CD P:65 SP:F9 PPU:283,184 CYC:21016 $EF9A:17 48 *SLO $48,X @ 47 = #$37 A:10 X:FF Y:CD P:65 SP:FB PPU:301,184 CYC:21022 $EF9C:EA NOP A:7E X:FF Y:CD P:64 SP:FB PPU:319,184 CYC:21028 $EF9D:EA NOP A:7E X:FF Y:CD P:64 SP:FB PPU:325,184 CYC:21030 $EF9E:EA NOP A:7E X:FF Y:CD P:64 SP:FB PPU:331,184 CYC:21032 $EF9F:EA NOP A:7E X:FF Y:CD P:64 SP:FB PPU:337,184 CYC:21034 $EFA0:20 A4 FA JSR $FAA4 A:7E X:FF Y:CD P:64 SP:FB PPU: 2,185 CYC:21036 $FAA4:50 40 BVC $FAE6 A:7E X:FF Y:CD P:64 SP:F9 PPU: 20,185 CYC:21042 $FAA6:F0 3E BEQ $FAE6 A:7E X:FF Y:CD P:64 SP:F9 PPU: 26,185 CYC:21044 $FAA8:30 3C BMI $FAE6 A:7E X:FF Y:CD P:64 SP:F9 PPU: 32,185 CYC:21046 $FAAA:B0 3A BCS $FAE6 A:7E X:FF Y:CD P:64 SP:F9 PPU: 38,185 CYC:21048 $FAAC:C9 7E CMP #$7E A:7E X:FF Y:CD P:64 SP:F9 PPU: 44,185 CYC:21050 $FAAE:D0 36 BNE $FAE6 A:7E X:FF Y:CD P:67 SP:F9 PPU: 50,185 CYC:21052 $FAB0:60 RTS A:7E X:FF Y:CD P:67 SP:F9 PPU: 56,185 CYC:21054 $EFA3:A5 47 LDA $47 = #$6E A:7E X:FF Y:CD P:67 SP:FB PPU: 74,185 CYC:21060 $EFA5:C9 6E CMP #$6E A:6E X:FF Y:CD P:65 SP:FB PPU: 83,185 CYC:21063 $EFA7:F0 02 BEQ $EFAB A:6E X:FF Y:CD P:67 SP:FB PPU: 89,185 CYC:21065 $EFAB:A9 A5 LDA #$A5 A:6E X:FF Y:CD P:67 SP:FB PPU: 98,185 CYC:21068 $EFAD:8D 47 06 STA $0647 = #$6E A:A5 X:FF Y:CD P:E5 SP:FB PPU:104,185 CYC:21070 $EFB0:A0 FF LDY #$FF A:A5 X:FF Y:CD P:E5 SP:FB PPU:116,185 CYC:21074 $EFB2:20 7B FA JSR $FA7B A:A5 X:FF Y:FF P:E5 SP:FB PPU:122,185 CYC:21076 $FA7B:24 01 BIT $01 = #$FF A:A5 X:FF Y:FF P:E5 SP:F9 PPU:140,185 CYC:21082 $FA7D:18 CLC A:A5 X:FF Y:FF P:E5 SP:F9 PPU:149,185 CYC:21085 $FA7E:A9 B3 LDA #$B3 A:A5 X:FF Y:FF P:NVUbdIzc SP:F9 PPU:155,185 CYC:21087 $FA80:60 RTS A:B3 X:FF Y:FF P:NVUbdIzc SP:F9 PPU:161,185 CYC:21089 $EFB5:1B 48 05 *SLO $0548,Y @ 0647 = #$A5 A:B3 X:FF Y:FF P:NVUbdIzc SP:FB PPU:179,185 CYC:21095 $EFB8:EA NOP A:FB X:FF Y:FF P:E5 SP:FB PPU:200,185 CYC:21102 $EFB9:EA NOP A:FB X:FF Y:FF P:E5 SP:FB PPU:206,185 CYC:21104 $EFBA:08 PHP A:FB X:FF Y:FF P:E5 SP:FB PPU:212,185 CYC:21106 $EFBB:48 PHA A:FB X:FF Y:FF P:E5 SP:FA PPU:221,185 CYC:21109 $EFBC:A0 CE LDY #$CE A:FB X:FF Y:FF P:E5 SP:F9 PPU:230,185 CYC:21112 $EFBE:68 PLA A:FB X:FF Y:CE P:E5 SP:F9 PPU:236,185 CYC:21114 $EFBF:28 PLP A:FB X:FF Y:CE P:E5 SP:FA PPU:248,185 CYC:21118 $EFC0:20 81 FA JSR $FA81 A:FB X:FF Y:CE P:E5 SP:FB PPU:260,185 CYC:21122 $FA81:50 63 BVC $FAE6 A:FB X:FF Y:CE P:E5 SP:F9 PPU:278,185 CYC:21128 $FA83:90 61 BCC $FAE6 A:FB X:FF Y:CE P:E5 SP:F9 PPU:284,185 CYC:21130 $FA85:10 5F BPL $FAE6 A:FB X:FF Y:CE P:E5 SP:F9 PPU:290,185 CYC:21132 $FA87:C9 FB CMP #$FB A:FB X:FF Y:CE P:E5 SP:F9 PPU:296,185 CYC:21134 $FA89:D0 5B BNE $FAE6 A:FB X:FF Y:CE P:67 SP:F9 PPU:302,185 CYC:21136 $FA8B:60 RTS A:FB X:FF Y:CE P:67 SP:F9 PPU:308,185 CYC:21138 $EFC3:AD 47 06 LDA $0647 = #$4A A:FB X:FF Y:CE P:67 SP:FB PPU:326,185 CYC:21144 $EFC6:C9 4A CMP #$4A A:4A X:FF Y:CE P:65 SP:FB PPU:338,185 CYC:21148 $EFC8:F0 02 BEQ $EFCC A:4A X:FF Y:CE P:67 SP:FB PPU: 3,186 CYC:21150 $EFCC:A0 FF LDY #$FF A:4A X:FF Y:CE P:67 SP:FB PPU: 12,186 CYC:21153 $EFCE:A9 29 LDA #$29 A:4A X:FF Y:FF P:E5 SP:FB PPU: 18,186 CYC:21155 $EFD0:8D 47 06 STA $0647 = #$4A A:29 X:FF Y:FF P:65 SP:FB PPU: 24,186 CYC:21157 $EFD3:20 8C FA JSR $FA8C A:29 X:FF Y:FF P:65 SP:FB PPU: 36,186 CYC:21161 $FA8C:B8 CLV A:29 X:FF Y:FF P:65 SP:F9 PPU: 54,186 CYC:21167 $FA8D:18 CLC A:29 X:FF Y:FF P:25 SP:F9 PPU: 60,186 CYC:21169 $FA8E:A9 C3 LDA #$C3 A:29 X:FF Y:FF P:nvUbdIzc SP:F9 PPU: 66,186 CYC:21171 $FA90:60 RTS A:C3 X:FF Y:FF P:NvUbdIzc SP:F9 PPU: 72,186 CYC:21173 $EFD6:1B 48 05 *SLO $0548,Y @ 0647 = #$29 A:C3 X:FF Y:FF P:NvUbdIzc SP:FB PPU: 90,186 CYC:21179 $EFD9:EA NOP A:D3 X:FF Y:FF P:NvUbdIzc SP:FB PPU:111,186 CYC:21186 $EFDA:EA NOP A:D3 X:FF Y:FF P:NvUbdIzc SP:FB PPU:117,186 CYC:21188 $EFDB:08 PHP A:D3 X:FF Y:FF P:NvUbdIzc SP:FB PPU:123,186 CYC:21190 $EFDC:48 PHA A:D3 X:FF Y:FF P:NvUbdIzc SP:FA PPU:132,186 CYC:21193 $EFDD:A0 CF LDY #$CF A:D3 X:FF Y:FF P:NvUbdIzc SP:F9 PPU:141,186 CYC:21196 $EFDF:68 PLA A:D3 X:FF Y:CF P:NvUbdIzc SP:F9 PPU:147,186 CYC:21198 $EFE0:28 PLP A:D3 X:FF Y:CF P:NvUbdIzc SP:FA PPU:159,186 CYC:21202 $EFE1:20 91 FA JSR $FA91 A:D3 X:FF Y:CF P:NvUbdIzc SP:FB PPU:171,186 CYC:21206 $FA91:70 53 BVS $FAE6 A:D3 X:FF Y:CF P:NvUbdIzc SP:F9 PPU:189,186 CYC:21212 $FA93:F0 51 BEQ $FAE6 A:D3 X:FF Y:CF P:NvUbdIzc SP:F9 PPU:195,186 CYC:21214 $FA95:10 4F BPL $FAE6 A:D3 X:FF Y:CF P:NvUbdIzc SP:F9 PPU:201,186 CYC:21216 $FA97:B0 4D BCS $FAE6 A:D3 X:FF Y:CF P:NvUbdIzc SP:F9 PPU:207,186 CYC:21218 $FA99:C9 D3 CMP #$D3 A:D3 X:FF Y:CF P:NvUbdIzc SP:F9 PPU:213,186 CYC:21220 $FA9B:D0 49 BNE $FAE6 A:D3 X:FF Y:CF P:nvUbdIZC SP:F9 PPU:219,186 CYC:21222 $FA9D:60 RTS A:D3 X:FF Y:CF P:nvUbdIZC SP:F9 PPU:225,186 CYC:21224 $EFE4:AD 47 06 LDA $0647 = #$52 A:D3 X:FF Y:CF P:nvUbdIZC SP:FB PPU:243,186 CYC:21230 $EFE7:C9 52 CMP #$52 A:52 X:FF Y:CF P:25 SP:FB PPU:255,186 CYC:21234 $EFE9:F0 02 BEQ $EFED A:52 X:FF Y:CF P:nvUbdIZC SP:FB PPU:261,186 CYC:21236 $EFED:A0 FF LDY #$FF A:52 X:FF Y:CF P:nvUbdIZC SP:FB PPU:270,186 CYC:21239 $EFEF:A9 37 LDA #$37 A:52 X:FF Y:FF P:A5 SP:FB PPU:276,186 CYC:21241 $EFF1:8D 47 06 STA $0647 = #$52 A:37 X:FF Y:FF P:25 SP:FB PPU:282,186 CYC:21243 $EFF4:20 9E FA JSR $FA9E A:37 X:FF Y:FF P:25 SP:FB PPU:294,186 CYC:21247 $FA9E:24 01 BIT $01 = #$FF A:37 X:FF Y:FF P:25 SP:F9 PPU:312,186 CYC:21253 $FAA0:38 SEC A:37 X:FF Y:FF P:E5 SP:F9 PPU:321,186 CYC:21256 $FAA1:A9 10 LDA #$10 A:37 X:FF Y:FF P:E5 SP:F9 PPU:327,186 CYC:21258 $FAA3:60 RTS A:10 X:FF Y:FF P:65 SP:F9 PPU:333,186 CYC:21260 $EFF7:1B 48 05 *SLO $0548,Y @ 0647 = #$37 A:10 X:FF Y:FF P:65 SP:FB PPU: 10,187 CYC:21266 $EFFA:EA NOP A:7E X:FF Y:FF P:64 SP:FB PPU: 31,187 CYC:21273 $EFFB:EA NOP A:7E X:FF Y:FF P:64 SP:FB PPU: 37,187 CYC:21275 $EFFC:08 PHP A:7E X:FF Y:FF P:64 SP:FB PPU: 43,187 CYC:21277 $EFFD:48 PHA A:7E X:FF Y:FF P:64 SP:FA PPU: 52,187 CYC:21280 $EFFE:A0 D0 LDY #$D0 A:7E X:FF Y:FF P:64 SP:F9 PPU: 61,187 CYC:21283 $F000:68 PLA A:7E X:FF Y:D0 P:NVUbdIzc SP:F9 PPU: 67,187 CYC:21285 $F001:28 PLP A:7E X:FF Y:D0 P:64 SP:FA PPU: 79,187 CYC:21289 $F002:20 A4 FA JSR $FAA4 A:7E X:FF Y:D0 P:64 SP:FB PPU: 91,187 CYC:21293 $FAA4:50 40 BVC $FAE6 A:7E X:FF Y:D0 P:64 SP:F9 PPU:109,187 CYC:21299 $FAA6:F0 3E BEQ $FAE6 A:7E X:FF Y:D0 P:64 SP:F9 PPU:115,187 CYC:21301 $FAA8:30 3C BMI $FAE6 A:7E X:FF Y:D0 P:64 SP:F9 PPU:121,187 CYC:21303 $FAAA:B0 3A BCS $FAE6 A:7E X:FF Y:D0 P:64 SP:F9 PPU:127,187 CYC:21305 $FAAC:C9 7E CMP #$7E A:7E X:FF Y:D0 P:64 SP:F9 PPU:133,187 CYC:21307 $FAAE:D0 36 BNE $FAE6 A:7E X:FF Y:D0 P:67 SP:F9 PPU:139,187 CYC:21309 $FAB0:60 RTS A:7E X:FF Y:D0 P:67 SP:F9 PPU:145,187 CYC:21311 $F005:AD 47 06 LDA $0647 = #$6E A:7E X:FF Y:D0 P:67 SP:FB PPU:163,187 CYC:21317 $F008:C9 6E CMP #$6E A:6E X:FF Y:D0 P:65 SP:FB PPU:175,187 CYC:21321 $F00A:F0 02 BEQ $F00E A:6E X:FF Y:D0 P:67 SP:FB PPU:181,187 CYC:21323 $F00E:A0 D1 LDY #$D1 A:6E X:FF Y:D0 P:67 SP:FB PPU:190,187 CYC:21326 $F010:A2 FF LDX #$FF A:6E X:FF Y:D1 P:E5 SP:FB PPU:196,187 CYC:21328 $F012:A9 A5 LDA #$A5 A:6E X:FF Y:D1 P:E5 SP:FB PPU:202,187 CYC:21330 $F014:8D 47 06 STA $0647 = #$6E A:A5 X:FF Y:D1 P:E5 SP:FB PPU:208,187 CYC:21332 $F017:20 7B FA JSR $FA7B A:A5 X:FF Y:D1 P:E5 SP:FB PPU:220,187 CYC:21336 $FA7B:24 01 BIT $01 = #$FF A:A5 X:FF Y:D1 P:E5 SP:F9 PPU:238,187 CYC:21342 $FA7D:18 CLC A:A5 X:FF Y:D1 P:E5 SP:F9 PPU:247,187 CYC:21345 $FA7E:A9 B3 LDA #$B3 A:A5 X:FF Y:D1 P:NVUbdIzc SP:F9 PPU:253,187 CYC:21347 $FA80:60 RTS A:B3 X:FF Y:D1 P:NVUbdIzc SP:F9 PPU:259,187 CYC:21349 $F01A:1F 48 05 *SLO $0548,X @ 0647 = #$A5 A:B3 X:FF Y:D1 P:NVUbdIzc SP:FB PPU:277,187 CYC:21355 $F01D:EA NOP A:FB X:FF Y:D1 P:E5 SP:FB PPU:298,187 CYC:21362 $F01E:EA NOP A:FB X:FF Y:D1 P:E5 SP:FB PPU:304,187 CYC:21364 $F01F:EA NOP A:FB X:FF Y:D1 P:E5 SP:FB PPU:310,187 CYC:21366 $F020:EA NOP A:FB X:FF Y:D1 P:E5 SP:FB PPU:316,187 CYC:21368 $F021:20 81 FA JSR $FA81 A:FB X:FF Y:D1 P:E5 SP:FB PPU:322,187 CYC:21370 $FA81:50 63 BVC $FAE6 A:FB X:FF Y:D1 P:E5 SP:F9 PPU:340,187 CYC:21376 $FA83:90 61 BCC $FAE6 A:FB X:FF Y:D1 P:E5 SP:F9 PPU: 5,188 CYC:21378 $FA85:10 5F BPL $FAE6 A:FB X:FF Y:D1 P:E5 SP:F9 PPU: 11,188 CYC:21380 $FA87:C9 FB CMP #$FB A:FB X:FF Y:D1 P:E5 SP:F9 PPU: 17,188 CYC:21382 $FA89:D0 5B BNE $FAE6 A:FB X:FF Y:D1 P:67 SP:F9 PPU: 23,188 CYC:21384 $FA8B:60 RTS A:FB X:FF Y:D1 P:67 SP:F9 PPU: 29,188 CYC:21386 $F024:AD 47 06 LDA $0647 = #$4A A:FB X:FF Y:D1 P:67 SP:FB PPU: 47,188 CYC:21392 $F027:C9 4A CMP #$4A A:4A X:FF Y:D1 P:65 SP:FB PPU: 59,188 CYC:21396 $F029:F0 02 BEQ $F02D A:4A X:FF Y:D1 P:67 SP:FB PPU: 65,188 CYC:21398 $F02D:C8 INY A:4A X:FF Y:D1 P:67 SP:FB PPU: 74,188 CYC:21401 $F02E:A9 29 LDA #$29 A:4A X:FF Y:D2 P:E5 SP:FB PPU: 80,188 CYC:21403 $F030:8D 47 06 STA $0647 = #$4A A:29 X:FF Y:D2 P:65 SP:FB PPU: 86,188 CYC:21405 $F033:20 8C FA JSR $FA8C A:29 X:FF Y:D2 P:65 SP:FB PPU: 98,188 CYC:21409 $FA8C:B8 CLV A:29 X:FF Y:D2 P:65 SP:F9 PPU:116,188 CYC:21415 $FA8D:18 CLC A:29 X:FF Y:D2 P:25 SP:F9 PPU:122,188 CYC:21417 $FA8E:A9 C3 LDA #$C3 A:29 X:FF Y:D2 P:nvUbdIzc SP:F9 PPU:128,188 CYC:21419 $FA90:60 RTS A:C3 X:FF Y:D2 P:NvUbdIzc SP:F9 PPU:134,188 CYC:21421 $F036:1F 48 05 *SLO $0548,X @ 0647 = #$29 A:C3 X:FF Y:D2 P:NvUbdIzc SP:FB PPU:152,188 CYC:21427 $F039:EA NOP A:D3 X:FF Y:D2 P:NvUbdIzc SP:FB PPU:173,188 CYC:21434 $F03A:EA NOP A:D3 X:FF Y:D2 P:NvUbdIzc SP:FB PPU:179,188 CYC:21436 $F03B:EA NOP A:D3 X:FF Y:D2 P:NvUbdIzc SP:FB PPU:185,188 CYC:21438 $F03C:EA NOP A:D3 X:FF Y:D2 P:NvUbdIzc SP:FB PPU:191,188 CYC:21440 $F03D:20 91 FA JSR $FA91 A:D3 X:FF Y:D2 P:NvUbdIzc SP:FB PPU:197,188 CYC:21442 $FA91:70 53 BVS $FAE6 A:D3 X:FF Y:D2 P:NvUbdIzc SP:F9 PPU:215,188 CYC:21448 $FA93:F0 51 BEQ $FAE6 A:D3 X:FF Y:D2 P:NvUbdIzc SP:F9 PPU:221,188 CYC:21450 $FA95:10 4F BPL $FAE6 A:D3 X:FF Y:D2 P:NvUbdIzc SP:F9 PPU:227,188 CYC:21452 $FA97:B0 4D BCS $FAE6 A:D3 X:FF Y:D2 P:NvUbdIzc SP:F9 PPU:233,188 CYC:21454 $FA99:C9 D3 CMP #$D3 A:D3 X:FF Y:D2 P:NvUbdIzc SP:F9 PPU:239,188 CYC:21456 $FA9B:D0 49 BNE $FAE6 A:D3 X:FF Y:D2 P:nvUbdIZC SP:F9 PPU:245,188 CYC:21458 $FA9D:60 RTS A:D3 X:FF Y:D2 P:nvUbdIZC SP:F9 PPU:251,188 CYC:21460 $F040:AD 47 06 LDA $0647 = #$52 A:D3 X:FF Y:D2 P:nvUbdIZC SP:FB PPU:269,188 CYC:21466 $F043:C9 52 CMP #$52 A:52 X:FF Y:D2 P:25 SP:FB PPU:281,188 CYC:21470 $F045:F0 02 BEQ $F049 A:52 X:FF Y:D2 P:nvUbdIZC SP:FB PPU:287,188 CYC:21472 $F049:C8 INY A:52 X:FF Y:D2 P:nvUbdIZC SP:FB PPU:296,188 CYC:21475 $F04A:A9 37 LDA #$37 A:52 X:FF Y:D3 P:A5 SP:FB PPU:302,188 CYC:21477 $F04C:8D 47 06 STA $0647 = #$52 A:37 X:FF Y:D3 P:25 SP:FB PPU:308,188 CYC:21479 $F04F:20 9E FA JSR $FA9E A:37 X:FF Y:D3 P:25 SP:FB PPU:320,188 CYC:21483 $FA9E:24 01 BIT $01 = #$FF A:37 X:FF Y:D3 P:25 SP:F9 PPU:338,188 CYC:21489 $FAA0:38 SEC A:37 X:FF Y:D3 P:E5 SP:F9 PPU: 6,189 CYC:21492 $FAA1:A9 10 LDA #$10 A:37 X:FF Y:D3 P:E5 SP:F9 PPU: 12,189 CYC:21494 $FAA3:60 RTS A:10 X:FF Y:D3 P:65 SP:F9 PPU: 18,189 CYC:21496 $F052:1F 48 05 *SLO $0548,X @ 0647 = #$37 A:10 X:FF Y:D3 P:65 SP:FB PPU: 36,189 CYC:21502 $F055:EA NOP A:7E X:FF Y:D3 P:64 SP:FB PPU: 57,189 CYC:21509 $F056:EA NOP A:7E X:FF Y:D3 P:64 SP:FB PPU: 63,189 CYC:21511 $F057:EA NOP A:7E X:FF Y:D3 P:64 SP:FB PPU: 69,189 CYC:21513 $F058:EA NOP A:7E X:FF Y:D3 P:64 SP:FB PPU: 75,189 CYC:21515 $F059:20 A4 FA JSR $FAA4 A:7E X:FF Y:D3 P:64 SP:FB PPU: 81,189 CYC:21517 $FAA4:50 40 BVC $FAE6 A:7E X:FF Y:D3 P:64 SP:F9 PPU: 99,189 CYC:21523 $FAA6:F0 3E BEQ $FAE6 A:7E X:FF Y:D3 P:64 SP:F9 PPU:105,189 CYC:21525 $FAA8:30 3C BMI $FAE6 A:7E X:FF Y:D3 P:64 SP:F9 PPU:111,189 CYC:21527 $FAAA:B0 3A BCS $FAE6 A:7E X:FF Y:D3 P:64 SP:F9 PPU:117,189 CYC:21529 $FAAC:C9 7E CMP #$7E A:7E X:FF Y:D3 P:64 SP:F9 PPU:123,189 CYC:21531 $FAAE:D0 36 BNE $FAE6 A:7E X:FF Y:D3 P:67 SP:F9 PPU:129,189 CYC:21533 $FAB0:60 RTS A:7E X:FF Y:D3 P:67 SP:F9 PPU:135,189 CYC:21535 $F05C:AD 47 06 LDA $0647 = #$6E A:7E X:FF Y:D3 P:67 SP:FB PPU:153,189 CYC:21541 $F05F:C9 6E CMP #$6E A:6E X:FF Y:D3 P:65 SP:FB PPU:165,189 CYC:21545 $F061:F0 02 BEQ $F065 A:6E X:FF Y:D3 P:67 SP:FB PPU:171,189 CYC:21547 $F065:60 RTS A:6E X:FF Y:D3 P:67 SP:FB PPU:180,189 CYC:21550 $C644:20 66 F0 JSR $F066 A:6E X:FF Y:D3 P:67 SP:FD PPU:198,189 CYC:21556 $F066:A9 FF LDA #$FF A:6E X:FF Y:D3 P:67 SP:FB PPU:216,189 CYC:21562 $F068:85 01 STA $01 = #$FF A:FF X:FF Y:D3 P:E5 SP:FB PPU:222,189 CYC:21564 $F06A:A0 D4 LDY #$D4 A:FF X:FF Y:D3 P:E5 SP:FB PPU:231,189 CYC:21567 $F06C:A2 02 LDX #$02 A:FF X:FF Y:D4 P:E5 SP:FB PPU:237,189 CYC:21569 $F06E:A9 47 LDA #$47 A:FF X:02 Y:D4 P:65 SP:FB PPU:243,189 CYC:21571 $F070:85 47 STA $47 = #$6E A:47 X:02 Y:D4 P:65 SP:FB PPU:249,189 CYC:21573 $F072:A9 06 LDA #$06 A:47 X:02 Y:D4 P:65 SP:FB PPU:258,189 CYC:21576 $F074:85 48 STA $48 = #$06 A:06 X:02 Y:D4 P:65 SP:FB PPU:264,189 CYC:21578 $F076:A9 A5 LDA #$A5 A:06 X:02 Y:D4 P:65 SP:FB PPU:273,189 CYC:21581 $F078:8D 47 06 STA $0647 = #$6E A:A5 X:02 Y:D4 P:E5 SP:FB PPU:279,189 CYC:21583 $F07B:20 53 FB JSR $FB53 A:A5 X:02 Y:D4 P:E5 SP:FB PPU:291,189 CYC:21587 $FB53:24 01 BIT $01 = #$FF A:A5 X:02 Y:D4 P:E5 SP:F9 PPU:309,189 CYC:21593 $FB55:18 CLC A:A5 X:02 Y:D4 P:E5 SP:F9 PPU:318,189 CYC:21596 $FB56:A9 B3 LDA #$B3 A:A5 X:02 Y:D4 P:NVUbdIzc SP:F9 PPU:324,189 CYC:21598 $FB58:60 RTS A:B3 X:02 Y:D4 P:NVUbdIzc SP:F9 PPU:330,189 CYC:21600 $F07E:23 45 *RLA ($45,X) @ 47 = #$0647 = A5 A:B3 X:02 Y:D4 P:NVUbdIzc SP:FB PPU: 7,190 CYC:21606 $F080:EA NOP A:02 X:02 Y:D4 P:65 SP:FB PPU: 31,190 CYC:21614 $F081:EA NOP A:02 X:02 Y:D4 P:65 SP:FB PPU: 37,190 CYC:21616 $F082:EA NOP A:02 X:02 Y:D4 P:65 SP:FB PPU: 43,190 CYC:21618 $F083:EA NOP A:02 X:02 Y:D4 P:65 SP:FB PPU: 49,190 CYC:21620 $F084:20 59 FB JSR $FB59 A:02 X:02 Y:D4 P:65 SP:FB PPU: 55,190 CYC:21622 $FB59:50 1A BVC $FB75 A:02 X:02 Y:D4 P:65 SP:F9 PPU: 73,190 CYC:21628 $FB5B:90 18 BCC $FB75 A:02 X:02 Y:D4 P:65 SP:F9 PPU: 79,190 CYC:21630 $FB5D:30 16 BMI $FB75 A:02 X:02 Y:D4 P:65 SP:F9 PPU: 85,190 CYC:21632 $FB5F:C9 02 CMP #$02 A:02 X:02 Y:D4 P:65 SP:F9 PPU: 91,190 CYC:21634 $FB61:D0 12 BNE $FB75 A:02 X:02 Y:D4 P:67 SP:F9 PPU: 97,190 CYC:21636 $FB63:60 RTS A:02 X:02 Y:D4 P:67 SP:F9 PPU:103,190 CYC:21638 $F087:AD 47 06 LDA $0647 = #$4A A:02 X:02 Y:D4 P:67 SP:FB PPU:121,190 CYC:21644 $F08A:C9 4A CMP #$4A A:4A X:02 Y:D4 P:65 SP:FB PPU:133,190 CYC:21648 $F08C:F0 02 BEQ $F090 A:4A X:02 Y:D4 P:67 SP:FB PPU:139,190 CYC:21650 $F090:C8 INY A:4A X:02 Y:D4 P:67 SP:FB PPU:148,190 CYC:21653 $F091:A9 29 LDA #$29 A:4A X:02 Y:D5 P:E5 SP:FB PPU:154,190 CYC:21655 $F093:8D 47 06 STA $0647 = #$4A A:29 X:02 Y:D5 P:65 SP:FB PPU:160,190 CYC:21657 $F096:20 64 FB JSR $FB64 A:29 X:02 Y:D5 P:65 SP:FB PPU:172,190 CYC:21661 $FB64:B8 CLV A:29 X:02 Y:D5 P:65 SP:F9 PPU:190,190 CYC:21667 $FB65:18 CLC A:29 X:02 Y:D5 P:25 SP:F9 PPU:196,190 CYC:21669 $FB66:A9 42 LDA #$42 A:29 X:02 Y:D5 P:nvUbdIzc SP:F9 PPU:202,190 CYC:21671 $FB68:60 RTS A:42 X:02 Y:D5 P:nvUbdIzc SP:F9 PPU:208,190 CYC:21673 $F099:23 45 *RLA ($45,X) @ 47 = #$0647 = 29 A:42 X:02 Y:D5 P:nvUbdIzc SP:FB PPU:226,190 CYC:21679 $F09B:EA NOP A:42 X:02 Y:D5 P:nvUbdIzc SP:FB PPU:250,190 CYC:21687 $F09C:EA NOP A:42 X:02 Y:D5 P:nvUbdIzc SP:FB PPU:256,190 CYC:21689 $F09D:EA NOP A:42 X:02 Y:D5 P:nvUbdIzc SP:FB PPU:262,190 CYC:21691 $F09E:EA NOP A:42 X:02 Y:D5 P:nvUbdIzc SP:FB PPU:268,190 CYC:21693 $F09F:20 69 FB JSR $FB69 A:42 X:02 Y:D5 P:nvUbdIzc SP:FB PPU:274,190 CYC:21695 $FB69:70 0A BVS $FB75 A:42 X:02 Y:D5 P:nvUbdIzc SP:F9 PPU:292,190 CYC:21701 $FB6B:F0 08 BEQ $FB75 A:42 X:02 Y:D5 P:nvUbdIzc SP:F9 PPU:298,190 CYC:21703 $FB6D:30 06 BMI $FB75 A:42 X:02 Y:D5 P:nvUbdIzc SP:F9 PPU:304,190 CYC:21705 $FB6F:B0 04 BCS $FB75 A:42 X:02 Y:D5 P:nvUbdIzc SP:F9 PPU:310,190 CYC:21707 $FB71:C9 42 CMP #$42 A:42 X:02 Y:D5 P:nvUbdIzc SP:F9 PPU:316,190 CYC:21709 $FB73:F0 02 BEQ $FB77 A:42 X:02 Y:D5 P:nvUbdIZC SP:F9 PPU:322,190 CYC:21711 $FB77:60 RTS A:42 X:02 Y:D5 P:nvUbdIZC SP:F9 PPU:331,190 CYC:21714 $F0A2:AD 47 06 LDA $0647 = #$52 A:42 X:02 Y:D5 P:nvUbdIZC SP:FB PPU: 8,191 CYC:21720 $F0A5:C9 52 CMP #$52 A:52 X:02 Y:D5 P:25 SP:FB PPU: 20,191 CYC:21724 $F0A7:F0 02 BEQ $F0AB A:52 X:02 Y:D5 P:nvUbdIZC SP:FB PPU: 26,191 CYC:21726 $F0AB:C8 INY A:52 X:02 Y:D5 P:nvUbdIZC SP:FB PPU: 35,191 CYC:21729 $F0AC:A9 37 LDA #$37 A:52 X:02 Y:D6 P:A5 SP:FB PPU: 41,191 CYC:21731 $F0AE:8D 47 06 STA $0647 = #$52 A:37 X:02 Y:D6 P:25 SP:FB PPU: 47,191 CYC:21733 $F0B1:20 68 FA JSR $FA68 A:37 X:02 Y:D6 P:25 SP:FB PPU: 59,191 CYC:21737 $FA68:24 01 BIT $01 = #$FF A:37 X:02 Y:D6 P:25 SP:F9 PPU: 77,191 CYC:21743 $FA6A:38 SEC A:37 X:02 Y:D6 P:E5 SP:F9 PPU: 86,191 CYC:21746 $FA6B:A9 75 LDA #$75 A:37 X:02 Y:D6 P:E5 SP:F9 PPU: 92,191 CYC:21748 $FA6D:60 RTS A:75 X:02 Y:D6 P:65 SP:F9 PPU: 98,191 CYC:21750 $F0B4:23 45 *RLA ($45,X) @ 47 = #$0647 = 37 A:75 X:02 Y:D6 P:65 SP:FB PPU:116,191 CYC:21756 $F0B6:EA NOP A:65 X:02 Y:D6 P:64 SP:FB PPU:140,191 CYC:21764 $F0B7:EA NOP A:65 X:02 Y:D6 P:64 SP:FB PPU:146,191 CYC:21766 $F0B8:EA NOP A:65 X:02 Y:D6 P:64 SP:FB PPU:152,191 CYC:21768 $F0B9:EA NOP A:65 X:02 Y:D6 P:64 SP:FB PPU:158,191 CYC:21770 $F0BA:20 6E FA JSR $FA6E A:65 X:02 Y:D6 P:64 SP:FB PPU:164,191 CYC:21772 $FA6E:50 76 BVC $FAE6 A:65 X:02 Y:D6 P:64 SP:F9 PPU:182,191 CYC:21778 $FA70:F0 74 BEQ $FAE6 A:65 X:02 Y:D6 P:64 SP:F9 PPU:188,191 CYC:21780 $FA72:30 72 BMI $FAE6 A:65 X:02 Y:D6 P:64 SP:F9 PPU:194,191 CYC:21782 $FA74:B0 70 BCS $FAE6 A:65 X:02 Y:D6 P:64 SP:F9 PPU:200,191 CYC:21784 $FA76:C9 65 CMP #$65 A:65 X:02 Y:D6 P:64 SP:F9 PPU:206,191 CYC:21786 $FA78:D0 6C BNE $FAE6 A:65 X:02 Y:D6 P:67 SP:F9 PPU:212,191 CYC:21788 $FA7A:60 RTS A:65 X:02 Y:D6 P:67 SP:F9 PPU:218,191 CYC:21790 $F0BD:AD 47 06 LDA $0647 = #$6F A:65 X:02 Y:D6 P:67 SP:FB PPU:236,191 CYC:21796 $F0C0:C9 6F CMP #$6F A:6F X:02 Y:D6 P:65 SP:FB PPU:248,191 CYC:21800 $F0C2:F0 02 BEQ $F0C6 A:6F X:02 Y:D6 P:67 SP:FB PPU:254,191 CYC:21802 $F0C6:C8 INY A:6F X:02 Y:D6 P:67 SP:FB PPU:263,191 CYC:21805 $F0C7:A9 A5 LDA #$A5 A:6F X:02 Y:D7 P:E5 SP:FB PPU:269,191 CYC:21807 $F0C9:85 47 STA $47 = #$47 A:A5 X:02 Y:D7 P:E5 SP:FB PPU:275,191 CYC:21809 $F0CB:20 53 FB JSR $FB53 A:A5 X:02 Y:D7 P:E5 SP:FB PPU:284,191 CYC:21812 $FB53:24 01 BIT $01 = #$FF A:A5 X:02 Y:D7 P:E5 SP:F9 PPU:302,191 CYC:21818 $FB55:18 CLC A:A5 X:02 Y:D7 P:E5 SP:F9 PPU:311,191 CYC:21821 $FB56:A9 B3 LDA #$B3 A:A5 X:02 Y:D7 P:NVUbdIzc SP:F9 PPU:317,191 CYC:21823 $FB58:60 RTS A:B3 X:02 Y:D7 P:NVUbdIzc SP:F9 PPU:323,191 CYC:21825 $F0CE:27 47 *RLA $47 = #$A5 A:B3 X:02 Y:D7 P:NVUbdIzc SP:FB PPU: 0,192 CYC:21831 $F0D0:EA NOP A:02 X:02 Y:D7 P:65 SP:FB PPU: 15,192 CYC:21836 $F0D1:EA NOP A:02 X:02 Y:D7 P:65 SP:FB PPU: 21,192 CYC:21838 $F0D2:EA NOP A:02 X:02 Y:D7 P:65 SP:FB PPU: 27,192 CYC:21840 $F0D3:EA NOP A:02 X:02 Y:D7 P:65 SP:FB PPU: 33,192 CYC:21842 $F0D4:20 59 FB JSR $FB59 A:02 X:02 Y:D7 P:65 SP:FB PPU: 39,192 CYC:21844 $FB59:50 1A BVC $FB75 A:02 X:02 Y:D7 P:65 SP:F9 PPU: 57,192 CYC:21850 $FB5B:90 18 BCC $FB75 A:02 X:02 Y:D7 P:65 SP:F9 PPU: 63,192 CYC:21852 $FB5D:30 16 BMI $FB75 A:02 X:02 Y:D7 P:65 SP:F9 PPU: 69,192 CYC:21854 $FB5F:C9 02 CMP #$02 A:02 X:02 Y:D7 P:65 SP:F9 PPU: 75,192 CYC:21856 $FB61:D0 12 BNE $FB75 A:02 X:02 Y:D7 P:67 SP:F9 PPU: 81,192 CYC:21858 $FB63:60 RTS A:02 X:02 Y:D7 P:67 SP:F9 PPU: 87,192 CYC:21860 $F0D7:A5 47 LDA $47 = #$4A A:02 X:02 Y:D7 P:67 SP:FB PPU:105,192 CYC:21866 $F0D9:C9 4A CMP #$4A A:4A X:02 Y:D7 P:65 SP:FB PPU:114,192 CYC:21869 $F0DB:F0 02 BEQ $F0DF A:4A X:02 Y:D7 P:67 SP:FB PPU:120,192 CYC:21871 $F0DF:C8 INY A:4A X:02 Y:D7 P:67 SP:FB PPU:129,192 CYC:21874 $F0E0:A9 29 LDA #$29 A:4A X:02 Y:D8 P:E5 SP:FB PPU:135,192 CYC:21876 $F0E2:85 47 STA $47 = #$4A A:29 X:02 Y:D8 P:65 SP:FB PPU:141,192 CYC:21878 $F0E4:20 64 FB JSR $FB64 A:29 X:02 Y:D8 P:65 SP:FB PPU:150,192 CYC:21881 $FB64:B8 CLV A:29 X:02 Y:D8 P:65 SP:F9 PPU:168,192 CYC:21887 $FB65:18 CLC A:29 X:02 Y:D8 P:25 SP:F9 PPU:174,192 CYC:21889 $FB66:A9 42 LDA #$42 A:29 X:02 Y:D8 P:nvUbdIzc SP:F9 PPU:180,192 CYC:21891 $FB68:60 RTS A:42 X:02 Y:D8 P:nvUbdIzc SP:F9 PPU:186,192 CYC:21893 $F0E7:27 47 *RLA $47 = #$29 A:42 X:02 Y:D8 P:nvUbdIzc SP:FB PPU:204,192 CYC:21899 $F0E9:EA NOP A:42 X:02 Y:D8 P:nvUbdIzc SP:FB PPU:219,192 CYC:21904 $F0EA:EA NOP A:42 X:02 Y:D8 P:nvUbdIzc SP:FB PPU:225,192 CYC:21906 $F0EB:EA NOP A:42 X:02 Y:D8 P:nvUbdIzc SP:FB PPU:231,192 CYC:21908 $F0EC:EA NOP A:42 X:02 Y:D8 P:nvUbdIzc SP:FB PPU:237,192 CYC:21910 $F0ED:20 69 FB JSR $FB69 A:42 X:02 Y:D8 P:nvUbdIzc SP:FB PPU:243,192 CYC:21912 $FB69:70 0A BVS $FB75 A:42 X:02 Y:D8 P:nvUbdIzc SP:F9 PPU:261,192 CYC:21918 $FB6B:F0 08 BEQ $FB75 A:42 X:02 Y:D8 P:nvUbdIzc SP:F9 PPU:267,192 CYC:21920 $FB6D:30 06 BMI $FB75 A:42 X:02 Y:D8 P:nvUbdIzc SP:F9 PPU:273,192 CYC:21922 $FB6F:B0 04 BCS $FB75 A:42 X:02 Y:D8 P:nvUbdIzc SP:F9 PPU:279,192 CYC:21924 $FB71:C9 42 CMP #$42 A:42 X:02 Y:D8 P:nvUbdIzc SP:F9 PPU:285,192 CYC:21926 $FB73:F0 02 BEQ $FB77 A:42 X:02 Y:D8 P:nvUbdIZC SP:F9 PPU:291,192 CYC:21928 $FB77:60 RTS A:42 X:02 Y:D8 P:nvUbdIZC SP:F9 PPU:300,192 CYC:21931 $F0F0:A5 47 LDA $47 = #$52 A:42 X:02 Y:D8 P:nvUbdIZC SP:FB PPU:318,192 CYC:21937 $F0F2:C9 52 CMP #$52 A:52 X:02 Y:D8 P:25 SP:FB PPU:327,192 CYC:21940 $F0F4:F0 02 BEQ $F0F8 A:52 X:02 Y:D8 P:nvUbdIZC SP:FB PPU:333,192 CYC:21942 $F0F8:C8 INY A:52 X:02 Y:D8 P:nvUbdIZC SP:FB PPU: 1,193 CYC:21945 $F0F9:A9 37 LDA #$37 A:52 X:02 Y:D9 P:A5 SP:FB PPU: 7,193 CYC:21947 $F0FB:85 47 STA $47 = #$52 A:37 X:02 Y:D9 P:25 SP:FB PPU: 13,193 CYC:21949 $F0FD:20 68 FA JSR $FA68 A:37 X:02 Y:D9 P:25 SP:FB PPU: 22,193 CYC:21952 $FA68:24 01 BIT $01 = #$FF A:37 X:02 Y:D9 P:25 SP:F9 PPU: 40,193 CYC:21958 $FA6A:38 SEC A:37 X:02 Y:D9 P:E5 SP:F9 PPU: 49,193 CYC:21961 $FA6B:A9 75 LDA #$75 A:37 X:02 Y:D9 P:E5 SP:F9 PPU: 55,193 CYC:21963 $FA6D:60 RTS A:75 X:02 Y:D9 P:65 SP:F9 PPU: 61,193 CYC:21965 $F100:27 47 *RLA $47 = #$37 A:75 X:02 Y:D9 P:65 SP:FB PPU: 79,193 CYC:21971 $F102:EA NOP A:65 X:02 Y:D9 P:64 SP:FB PPU: 94,193 CYC:21976 $F103:EA NOP A:65 X:02 Y:D9 P:64 SP:FB PPU:100,193 CYC:21978 $F104:EA NOP A:65 X:02 Y:D9 P:64 SP:FB PPU:106,193 CYC:21980 $F105:EA NOP A:65 X:02 Y:D9 P:64 SP:FB PPU:112,193 CYC:21982 $F106:20 6E FA JSR $FA6E A:65 X:02 Y:D9 P:64 SP:FB PPU:118,193 CYC:21984 $FA6E:50 76 BVC $FAE6 A:65 X:02 Y:D9 P:64 SP:F9 PPU:136,193 CYC:21990 $FA70:F0 74 BEQ $FAE6 A:65 X:02 Y:D9 P:64 SP:F9 PPU:142,193 CYC:21992 $FA72:30 72 BMI $FAE6 A:65 X:02 Y:D9 P:64 SP:F9 PPU:148,193 CYC:21994 $FA74:B0 70 BCS $FAE6 A:65 X:02 Y:D9 P:64 SP:F9 PPU:154,193 CYC:21996 $FA76:C9 65 CMP #$65 A:65 X:02 Y:D9 P:64 SP:F9 PPU:160,193 CYC:21998 $FA78:D0 6C BNE $FAE6 A:65 X:02 Y:D9 P:67 SP:F9 PPU:166,193 CYC:22000 $FA7A:60 RTS A:65 X:02 Y:D9 P:67 SP:F9 PPU:172,193 CYC:22002 $F109:A5 47 LDA $47 = #$6F A:65 X:02 Y:D9 P:67 SP:FB PPU:190,193 CYC:22008 $F10B:C9 6F CMP #$6F A:6F X:02 Y:D9 P:65 SP:FB PPU:199,193 CYC:22011 $F10D:F0 02 BEQ $F111 A:6F X:02 Y:D9 P:67 SP:FB PPU:205,193 CYC:22013 $F111:C8 INY A:6F X:02 Y:D9 P:67 SP:FB PPU:214,193 CYC:22016 $F112:A9 A5 LDA #$A5 A:6F X:02 Y:DA P:E5 SP:FB PPU:220,193 CYC:22018 $F114:8D 47 06 STA $0647 = #$6F A:A5 X:02 Y:DA P:E5 SP:FB PPU:226,193 CYC:22020 $F117:20 53 FB JSR $FB53 A:A5 X:02 Y:DA P:E5 SP:FB PPU:238,193 CYC:22024 $FB53:24 01 BIT $01 = #$FF A:A5 X:02 Y:DA P:E5 SP:F9 PPU:256,193 CYC:22030 $FB55:18 CLC A:A5 X:02 Y:DA P:E5 SP:F9 PPU:265,193 CYC:22033 $FB56:A9 B3 LDA #$B3 A:A5 X:02 Y:DA P:NVUbdIzc SP:F9 PPU:271,193 CYC:22035 $FB58:60 RTS A:B3 X:02 Y:DA P:NVUbdIzc SP:F9 PPU:277,193 CYC:22037 $F11A:2F 47 06 *RLA $0647 = #$A5 A:B3 X:02 Y:DA P:NVUbdIzc SP:FB PPU:295,193 CYC:22043 $F11D:EA NOP A:02 X:02 Y:DA P:65 SP:FB PPU:313,193 CYC:22049 $F11E:EA NOP A:02 X:02 Y:DA P:65 SP:FB PPU:319,193 CYC:22051 $F11F:EA NOP A:02 X:02 Y:DA P:65 SP:FB PPU:325,193 CYC:22053 $F120:EA NOP A:02 X:02 Y:DA P:65 SP:FB PPU:331,193 CYC:22055 $F121:20 59 FB JSR $FB59 A:02 X:02 Y:DA P:65 SP:FB PPU:337,193 CYC:22057 $FB59:50 1A BVC $FB75 A:02 X:02 Y:DA P:65 SP:F9 PPU: 14,194 CYC:22063 $FB5B:90 18 BCC $FB75 A:02 X:02 Y:DA P:65 SP:F9 PPU: 20,194 CYC:22065 $FB5D:30 16 BMI $FB75 A:02 X:02 Y:DA P:65 SP:F9 PPU: 26,194 CYC:22067 $FB5F:C9 02 CMP #$02 A:02 X:02 Y:DA P:65 SP:F9 PPU: 32,194 CYC:22069 $FB61:D0 12 BNE $FB75 A:02 X:02 Y:DA P:67 SP:F9 PPU: 38,194 CYC:22071 $FB63:60 RTS A:02 X:02 Y:DA P:67 SP:F9 PPU: 44,194 CYC:22073 $F124:AD 47 06 LDA $0647 = #$4A A:02 X:02 Y:DA P:67 SP:FB PPU: 62,194 CYC:22079 $F127:C9 4A CMP #$4A A:4A X:02 Y:DA P:65 SP:FB PPU: 74,194 CYC:22083 $F129:F0 02 BEQ $F12D A:4A X:02 Y:DA P:67 SP:FB PPU: 80,194 CYC:22085 $F12D:C8 INY A:4A X:02 Y:DA P:67 SP:FB PPU: 89,194 CYC:22088 $F12E:A9 29 LDA #$29 A:4A X:02 Y:DB P:E5 SP:FB PPU: 95,194 CYC:22090 $F130:8D 47 06 STA $0647 = #$4A A:29 X:02 Y:DB P:65 SP:FB PPU:101,194 CYC:22092 $F133:20 64 FB JSR $FB64 A:29 X:02 Y:DB P:65 SP:FB PPU:113,194 CYC:22096 $FB64:B8 CLV A:29 X:02 Y:DB P:65 SP:F9 PPU:131,194 CYC:22102 $FB65:18 CLC A:29 X:02 Y:DB P:25 SP:F9 PPU:137,194 CYC:22104 $FB66:A9 42 LDA #$42 A:29 X:02 Y:DB P:nvUbdIzc SP:F9 PPU:143,194 CYC:22106 $FB68:60 RTS A:42 X:02 Y:DB P:nvUbdIzc SP:F9 PPU:149,194 CYC:22108 $F136:2F 47 06 *RLA $0647 = #$29 A:42 X:02 Y:DB P:nvUbdIzc SP:FB PPU:167,194 CYC:22114 $F139:EA NOP A:42 X:02 Y:DB P:nvUbdIzc SP:FB PPU:185,194 CYC:22120 $F13A:EA NOP A:42 X:02 Y:DB P:nvUbdIzc SP:FB PPU:191,194 CYC:22122 $F13B:EA NOP A:42 X:02 Y:DB P:nvUbdIzc SP:FB PPU:197,194 CYC:22124 $F13C:EA NOP A:42 X:02 Y:DB P:nvUbdIzc SP:FB PPU:203,194 CYC:22126 $F13D:20 69 FB JSR $FB69 A:42 X:02 Y:DB P:nvUbdIzc SP:FB PPU:209,194 CYC:22128 $FB69:70 0A BVS $FB75 A:42 X:02 Y:DB P:nvUbdIzc SP:F9 PPU:227,194 CYC:22134 $FB6B:F0 08 BEQ $FB75 A:42 X:02 Y:DB P:nvUbdIzc SP:F9 PPU:233,194 CYC:22136 $FB6D:30 06 BMI $FB75 A:42 X:02 Y:DB P:nvUbdIzc SP:F9 PPU:239,194 CYC:22138 $FB6F:B0 04 BCS $FB75 A:42 X:02 Y:DB P:nvUbdIzc SP:F9 PPU:245,194 CYC:22140 $FB71:C9 42 CMP #$42 A:42 X:02 Y:DB P:nvUbdIzc SP:F9 PPU:251,194 CYC:22142 $FB73:F0 02 BEQ $FB77 A:42 X:02 Y:DB P:nvUbdIZC SP:F9 PPU:257,194 CYC:22144 $FB77:60 RTS A:42 X:02 Y:DB P:nvUbdIZC SP:F9 PPU:266,194 CYC:22147 $F140:AD 47 06 LDA $0647 = #$52 A:42 X:02 Y:DB P:nvUbdIZC SP:FB PPU:284,194 CYC:22153 $F143:C9 52 CMP #$52 A:52 X:02 Y:DB P:25 SP:FB PPU:296,194 CYC:22157 $F145:F0 02 BEQ $F149 A:52 X:02 Y:DB P:nvUbdIZC SP:FB PPU:302,194 CYC:22159 $F149:C8 INY A:52 X:02 Y:DB P:nvUbdIZC SP:FB PPU:311,194 CYC:22162 $F14A:A9 37 LDA #$37 A:52 X:02 Y:DC P:A5 SP:FB PPU:317,194 CYC:22164 $F14C:8D 47 06 STA $0647 = #$52 A:37 X:02 Y:DC P:25 SP:FB PPU:323,194 CYC:22166 $F14F:20 68 FA JSR $FA68 A:37 X:02 Y:DC P:25 SP:FB PPU:335,194 CYC:22170 $FA68:24 01 BIT $01 = #$FF A:37 X:02 Y:DC P:25 SP:F9 PPU: 12,195 CYC:22176 $FA6A:38 SEC A:37 X:02 Y:DC P:E5 SP:F9 PPU: 21,195 CYC:22179 $FA6B:A9 75 LDA #$75 A:37 X:02 Y:DC P:E5 SP:F9 PPU: 27,195 CYC:22181 $FA6D:60 RTS A:75 X:02 Y:DC P:65 SP:F9 PPU: 33,195 CYC:22183 $F152:2F 47 06 *RLA $0647 = #$37 A:75 X:02 Y:DC P:65 SP:FB PPU: 51,195 CYC:22189 $F155:EA NOP A:65 X:02 Y:DC P:64 SP:FB PPU: 69,195 CYC:22195 $F156:EA NOP A:65 X:02 Y:DC P:64 SP:FB PPU: 75,195 CYC:22197 $F157:EA NOP A:65 X:02 Y:DC P:64 SP:FB PPU: 81,195 CYC:22199 $F158:EA NOP A:65 X:02 Y:DC P:64 SP:FB PPU: 87,195 CYC:22201 $F159:20 6E FA JSR $FA6E A:65 X:02 Y:DC P:64 SP:FB PPU: 93,195 CYC:22203 $FA6E:50 76 BVC $FAE6 A:65 X:02 Y:DC P:64 SP:F9 PPU:111,195 CYC:22209 $FA70:F0 74 BEQ $FAE6 A:65 X:02 Y:DC P:64 SP:F9 PPU:117,195 CYC:22211 $FA72:30 72 BMI $FAE6 A:65 X:02 Y:DC P:64 SP:F9 PPU:123,195 CYC:22213 $FA74:B0 70 BCS $FAE6 A:65 X:02 Y:DC P:64 SP:F9 PPU:129,195 CYC:22215 $FA76:C9 65 CMP #$65 A:65 X:02 Y:DC P:64 SP:F9 PPU:135,195 CYC:22217 $FA78:D0 6C BNE $FAE6 A:65 X:02 Y:DC P:67 SP:F9 PPU:141,195 CYC:22219 $FA7A:60 RTS A:65 X:02 Y:DC P:67 SP:F9 PPU:147,195 CYC:22221 $F15C:AD 47 06 LDA $0647 = #$6F A:65 X:02 Y:DC P:67 SP:FB PPU:165,195 CYC:22227 $F15F:C9 6F CMP #$6F A:6F X:02 Y:DC P:65 SP:FB PPU:177,195 CYC:22231 $F161:F0 02 BEQ $F165 A:6F X:02 Y:DC P:67 SP:FB PPU:183,195 CYC:22233 $F165:A9 A5 LDA #$A5 A:6F X:02 Y:DC P:67 SP:FB PPU:192,195 CYC:22236 $F167:8D 47 06 STA $0647 = #$6F A:A5 X:02 Y:DC P:E5 SP:FB PPU:198,195 CYC:22238 $F16A:A9 48 LDA #$48 A:A5 X:02 Y:DC P:E5 SP:FB PPU:210,195 CYC:22242 $F16C:85 45 STA $45 = #$48 A:48 X:02 Y:DC P:65 SP:FB PPU:216,195 CYC:22244 $F16E:A9 05 LDA #$05 A:48 X:02 Y:DC P:65 SP:FB PPU:225,195 CYC:22247 $F170:85 46 STA $46 = #$05 A:05 X:02 Y:DC P:65 SP:FB PPU:231,195 CYC:22249 $F172:A0 FF LDY #$FF A:05 X:02 Y:DC P:65 SP:FB PPU:240,195 CYC:22252 $F174:20 53 FB JSR $FB53 A:05 X:02 Y:FF P:E5 SP:FB PPU:246,195 CYC:22254 $FB53:24 01 BIT $01 = #$FF A:05 X:02 Y:FF P:E5 SP:F9 PPU:264,195 CYC:22260 $FB55:18 CLC A:05 X:02 Y:FF P:E5 SP:F9 PPU:273,195 CYC:22263 $FB56:A9 B3 LDA #$B3 A:05 X:02 Y:FF P:NVUbdIzc SP:F9 PPU:279,195 CYC:22265 $FB58:60 RTS A:B3 X:02 Y:FF P:NVUbdIzc SP:F9 PPU:285,195 CYC:22267 $F177:33 45 *RLA ($45),Y = #$0548 @ 0647 = A5 A:B3 X:02 Y:FF P:NVUbdIzc SP:FB PPU:303,195 CYC:22273 $F179:EA NOP A:02 X:02 Y:FF P:65 SP:FB PPU:327,195 CYC:22281 $F17A:EA NOP A:02 X:02 Y:FF P:65 SP:FB PPU:333,195 CYC:22283 $F17B:08 PHP A:02 X:02 Y:FF P:65 SP:FB PPU:339,195 CYC:22285 $F17C:48 PHA A:02 X:02 Y:FF P:65 SP:FA PPU: 7,196 CYC:22288 $F17D:A0 DD LDY #$DD A:02 X:02 Y:FF P:65 SP:F9 PPU: 16,196 CYC:22291 $F17F:68 PLA A:02 X:02 Y:DD P:E5 SP:F9 PPU: 22,196 CYC:22293 $F180:28 PLP A:02 X:02 Y:DD P:65 SP:FA PPU: 34,196 CYC:22297 $F181:20 59 FB JSR $FB59 A:02 X:02 Y:DD P:65 SP:FB PPU: 46,196 CYC:22301 $FB59:50 1A BVC $FB75 A:02 X:02 Y:DD P:65 SP:F9 PPU: 64,196 CYC:22307 $FB5B:90 18 BCC $FB75 A:02 X:02 Y:DD P:65 SP:F9 PPU: 70,196 CYC:22309 $FB5D:30 16 BMI $FB75 A:02 X:02 Y:DD P:65 SP:F9 PPU: 76,196 CYC:22311 $FB5F:C9 02 CMP #$02 A:02 X:02 Y:DD P:65 SP:F9 PPU: 82,196 CYC:22313 $FB61:D0 12 BNE $FB75 A:02 X:02 Y:DD P:67 SP:F9 PPU: 88,196 CYC:22315 $FB63:60 RTS A:02 X:02 Y:DD P:67 SP:F9 PPU: 94,196 CYC:22317 $F184:AD 47 06 LDA $0647 = #$4A A:02 X:02 Y:DD P:67 SP:FB PPU:112,196 CYC:22323 $F187:C9 4A CMP #$4A A:4A X:02 Y:DD P:65 SP:FB PPU:124,196 CYC:22327 $F189:F0 02 BEQ $F18D A:4A X:02 Y:DD P:67 SP:FB PPU:130,196 CYC:22329 $F18D:A0 FF LDY #$FF A:4A X:02 Y:DD P:67 SP:FB PPU:139,196 CYC:22332 $F18F:A9 29 LDA #$29 A:4A X:02 Y:FF P:E5 SP:FB PPU:145,196 CYC:22334 $F191:8D 47 06 STA $0647 = #$4A A:29 X:02 Y:FF P:65 SP:FB PPU:151,196 CYC:22336 $F194:20 64 FB JSR $FB64 A:29 X:02 Y:FF P:65 SP:FB PPU:163,196 CYC:22340 $FB64:B8 CLV A:29 X:02 Y:FF P:65 SP:F9 PPU:181,196 CYC:22346 $FB65:18 CLC A:29 X:02 Y:FF P:25 SP:F9 PPU:187,196 CYC:22348 $FB66:A9 42 LDA #$42 A:29 X:02 Y:FF P:nvUbdIzc SP:F9 PPU:193,196 CYC:22350 $FB68:60 RTS A:42 X:02 Y:FF P:nvUbdIzc SP:F9 PPU:199,196 CYC:22352 $F197:33 45 *RLA ($45),Y = #$0548 @ 0647 = 29 A:42 X:02 Y:FF P:nvUbdIzc SP:FB PPU:217,196 CYC:22358 $F199:EA NOP A:42 X:02 Y:FF P:nvUbdIzc SP:FB PPU:241,196 CYC:22366 $F19A:EA NOP A:42 X:02 Y:FF P:nvUbdIzc SP:FB PPU:247,196 CYC:22368 $F19B:08 PHP A:42 X:02 Y:FF P:nvUbdIzc SP:FB PPU:253,196 CYC:22370 $F19C:48 PHA A:42 X:02 Y:FF P:nvUbdIzc SP:FA PPU:262,196 CYC:22373 $F19D:A0 DE LDY #$DE A:42 X:02 Y:FF P:nvUbdIzc SP:F9 PPU:271,196 CYC:22376 $F19F:68 PLA A:42 X:02 Y:DE P:NvUbdIzc SP:F9 PPU:277,196 CYC:22378 $F1A0:28 PLP A:42 X:02 Y:DE P:nvUbdIzc SP:FA PPU:289,196 CYC:22382 $F1A1:20 69 FB JSR $FB69 A:42 X:02 Y:DE P:nvUbdIzc SP:FB PPU:301,196 CYC:22386 $FB69:70 0A BVS $FB75 A:42 X:02 Y:DE P:nvUbdIzc SP:F9 PPU:319,196 CYC:22392 $FB6B:F0 08 BEQ $FB75 A:42 X:02 Y:DE P:nvUbdIzc SP:F9 PPU:325,196 CYC:22394 $FB6D:30 06 BMI $FB75 A:42 X:02 Y:DE P:nvUbdIzc SP:F9 PPU:331,196 CYC:22396 $FB6F:B0 04 BCS $FB75 A:42 X:02 Y:DE P:nvUbdIzc SP:F9 PPU:337,196 CYC:22398 $FB71:C9 42 CMP #$42 A:42 X:02 Y:DE P:nvUbdIzc SP:F9 PPU: 2,197 CYC:22400 $FB73:F0 02 BEQ $FB77 A:42 X:02 Y:DE P:nvUbdIZC SP:F9 PPU: 8,197 CYC:22402 $FB77:60 RTS A:42 X:02 Y:DE P:nvUbdIZC SP:F9 PPU: 17,197 CYC:22405 $F1A4:AD 47 06 LDA $0647 = #$52 A:42 X:02 Y:DE P:nvUbdIZC SP:FB PPU: 35,197 CYC:22411 $F1A7:C9 52 CMP #$52 A:52 X:02 Y:DE P:25 SP:FB PPU: 47,197 CYC:22415 $F1A9:F0 02 BEQ $F1AD A:52 X:02 Y:DE P:nvUbdIZC SP:FB PPU: 53,197 CYC:22417 $F1AD:A0 FF LDY #$FF A:52 X:02 Y:DE P:nvUbdIZC SP:FB PPU: 62,197 CYC:22420 $F1AF:A9 37 LDA #$37 A:52 X:02 Y:FF P:A5 SP:FB PPU: 68,197 CYC:22422 $F1B1:8D 47 06 STA $0647 = #$52 A:37 X:02 Y:FF P:25 SP:FB PPU: 74,197 CYC:22424 $F1B4:20 68 FA JSR $FA68 A:37 X:02 Y:FF P:25 SP:FB PPU: 86,197 CYC:22428 $FA68:24 01 BIT $01 = #$FF A:37 X:02 Y:FF P:25 SP:F9 PPU:104,197 CYC:22434 $FA6A:38 SEC A:37 X:02 Y:FF P:E5 SP:F9 PPU:113,197 CYC:22437 $FA6B:A9 75 LDA #$75 A:37 X:02 Y:FF P:E5 SP:F9 PPU:119,197 CYC:22439 $FA6D:60 RTS A:75 X:02 Y:FF P:65 SP:F9 PPU:125,197 CYC:22441 $F1B7:33 45 *RLA ($45),Y = #$0548 @ 0647 = 37 A:75 X:02 Y:FF P:65 SP:FB PPU:143,197 CYC:22447 $F1B9:EA NOP A:65 X:02 Y:FF P:64 SP:FB PPU:167,197 CYC:22455 $F1BA:EA NOP A:65 X:02 Y:FF P:64 SP:FB PPU:173,197 CYC:22457 $F1BB:08 PHP A:65 X:02 Y:FF P:64 SP:FB PPU:179,197 CYC:22459 $F1BC:48 PHA A:65 X:02 Y:FF P:64 SP:FA PPU:188,197 CYC:22462 $F1BD:A0 DF LDY #$DF A:65 X:02 Y:FF P:64 SP:F9 PPU:197,197 CYC:22465 $F1BF:68 PLA A:65 X:02 Y:DF P:NVUbdIzc SP:F9 PPU:203,197 CYC:22467 $F1C0:28 PLP A:65 X:02 Y:DF P:64 SP:FA PPU:215,197 CYC:22471 $F1C1:20 6E FA JSR $FA6E A:65 X:02 Y:DF P:64 SP:FB PPU:227,197 CYC:22475 $FA6E:50 76 BVC $FAE6 A:65 X:02 Y:DF P:64 SP:F9 PPU:245,197 CYC:22481 $FA70:F0 74 BEQ $FAE6 A:65 X:02 Y:DF P:64 SP:F9 PPU:251,197 CYC:22483 $FA72:30 72 BMI $FAE6 A:65 X:02 Y:DF P:64 SP:F9 PPU:257,197 CYC:22485 $FA74:B0 70 BCS $FAE6 A:65 X:02 Y:DF P:64 SP:F9 PPU:263,197 CYC:22487 $FA76:C9 65 CMP #$65 A:65 X:02 Y:DF P:64 SP:F9 PPU:269,197 CYC:22489 $FA78:D0 6C BNE $FAE6 A:65 X:02 Y:DF P:67 SP:F9 PPU:275,197 CYC:22491 $FA7A:60 RTS A:65 X:02 Y:DF P:67 SP:F9 PPU:281,197 CYC:22493 $F1C4:AD 47 06 LDA $0647 = #$6F A:65 X:02 Y:DF P:67 SP:FB PPU:299,197 CYC:22499 $F1C7:C9 6F CMP #$6F A:6F X:02 Y:DF P:65 SP:FB PPU:311,197 CYC:22503 $F1C9:F0 02 BEQ $F1CD A:6F X:02 Y:DF P:67 SP:FB PPU:317,197 CYC:22505 $F1CD:A0 E0 LDY #$E0 A:6F X:02 Y:DF P:67 SP:FB PPU:326,197 CYC:22508 $F1CF:A2 FF LDX #$FF A:6F X:02 Y:E0 P:E5 SP:FB PPU:332,197 CYC:22510 $F1D1:A9 A5 LDA #$A5 A:6F X:FF Y:E0 P:E5 SP:FB PPU:338,197 CYC:22512 $F1D3:85 47 STA $47 = #$6F A:A5 X:FF Y:E0 P:E5 SP:FB PPU: 3,198 CYC:22514 $F1D5:20 53 FB JSR $FB53 A:A5 X:FF Y:E0 P:E5 SP:FB PPU: 12,198 CYC:22517 $FB53:24 01 BIT $01 = #$FF A:A5 X:FF Y:E0 P:E5 SP:F9 PPU: 30,198 CYC:22523 $FB55:18 CLC A:A5 X:FF Y:E0 P:E5 SP:F9 PPU: 39,198 CYC:22526 $FB56:A9 B3 LDA #$B3 A:A5 X:FF Y:E0 P:NVUbdIzc SP:F9 PPU: 45,198 CYC:22528 $FB58:60 RTS A:B3 X:FF Y:E0 P:NVUbdIzc SP:F9 PPU: 51,198 CYC:22530 $F1D8:37 48 *RLA $48,X @ 47 = #$A5 A:B3 X:FF Y:E0 P:NVUbdIzc SP:FB PPU: 69,198 CYC:22536 $F1DA:EA NOP A:02 X:FF Y:E0 P:65 SP:FB PPU: 87,198 CYC:22542 $F1DB:EA NOP A:02 X:FF Y:E0 P:65 SP:FB PPU: 93,198 CYC:22544 $F1DC:EA NOP A:02 X:FF Y:E0 P:65 SP:FB PPU: 99,198 CYC:22546 $F1DD:EA NOP A:02 X:FF Y:E0 P:65 SP:FB PPU:105,198 CYC:22548 $F1DE:20 59 FB JSR $FB59 A:02 X:FF Y:E0 P:65 SP:FB PPU:111,198 CYC:22550 $FB59:50 1A BVC $FB75 A:02 X:FF Y:E0 P:65 SP:F9 PPU:129,198 CYC:22556 $FB5B:90 18 BCC $FB75 A:02 X:FF Y:E0 P:65 SP:F9 PPU:135,198 CYC:22558 $FB5D:30 16 BMI $FB75 A:02 X:FF Y:E0 P:65 SP:F9 PPU:141,198 CYC:22560 $FB5F:C9 02 CMP #$02 A:02 X:FF Y:E0 P:65 SP:F9 PPU:147,198 CYC:22562 $FB61:D0 12 BNE $FB75 A:02 X:FF Y:E0 P:67 SP:F9 PPU:153,198 CYC:22564 $FB63:60 RTS A:02 X:FF Y:E0 P:67 SP:F9 PPU:159,198 CYC:22566 $F1E1:A5 47 LDA $47 = #$4A A:02 X:FF Y:E0 P:67 SP:FB PPU:177,198 CYC:22572 $F1E3:C9 4A CMP #$4A A:4A X:FF Y:E0 P:65 SP:FB PPU:186,198 CYC:22575 $F1E5:F0 02 BEQ $F1E9 A:4A X:FF Y:E0 P:67 SP:FB PPU:192,198 CYC:22577 $F1E9:C8 INY A:4A X:FF Y:E0 P:67 SP:FB PPU:201,198 CYC:22580 $F1EA:A9 29 LDA #$29 A:4A X:FF Y:E1 P:E5 SP:FB PPU:207,198 CYC:22582 $F1EC:85 47 STA $47 = #$4A A:29 X:FF Y:E1 P:65 SP:FB PPU:213,198 CYC:22584 $F1EE:20 64 FB JSR $FB64 A:29 X:FF Y:E1 P:65 SP:FB PPU:222,198 CYC:22587 $FB64:B8 CLV A:29 X:FF Y:E1 P:65 SP:F9 PPU:240,198 CYC:22593 $FB65:18 CLC A:29 X:FF Y:E1 P:25 SP:F9 PPU:246,198 CYC:22595 $FB66:A9 42 LDA #$42 A:29 X:FF Y:E1 P:nvUbdIzc SP:F9 PPU:252,198 CYC:22597 $FB68:60 RTS A:42 X:FF Y:E1 P:nvUbdIzc SP:F9 PPU:258,198 CYC:22599 $F1F1:37 48 *RLA $48,X @ 47 = #$29 A:42 X:FF Y:E1 P:nvUbdIzc SP:FB PPU:276,198 CYC:22605 $F1F3:EA NOP A:42 X:FF Y:E1 P:nvUbdIzc SP:FB PPU:294,198 CYC:22611 $F1F4:EA NOP A:42 X:FF Y:E1 P:nvUbdIzc SP:FB PPU:300,198 CYC:22613 $F1F5:EA NOP A:42 X:FF Y:E1 P:nvUbdIzc SP:FB PPU:306,198 CYC:22615 $F1F6:EA NOP A:42 X:FF Y:E1 P:nvUbdIzc SP:FB PPU:312,198 CYC:22617 $F1F7:20 69 FB JSR $FB69 A:42 X:FF Y:E1 P:nvUbdIzc SP:FB PPU:318,198 CYC:22619 $FB69:70 0A BVS $FB75 A:42 X:FF Y:E1 P:nvUbdIzc SP:F9 PPU:336,198 CYC:22625 $FB6B:F0 08 BEQ $FB75 A:42 X:FF Y:E1 P:nvUbdIzc SP:F9 PPU: 1,199 CYC:22627 $FB6D:30 06 BMI $FB75 A:42 X:FF Y:E1 P:nvUbdIzc SP:F9 PPU: 7,199 CYC:22629 $FB6F:B0 04 BCS $FB75 A:42 X:FF Y:E1 P:nvUbdIzc SP:F9 PPU: 13,199 CYC:22631 $FB71:C9 42 CMP #$42 A:42 X:FF Y:E1 P:nvUbdIzc SP:F9 PPU: 19,199 CYC:22633 $FB73:F0 02 BEQ $FB77 A:42 X:FF Y:E1 P:nvUbdIZC SP:F9 PPU: 25,199 CYC:22635 $FB77:60 RTS A:42 X:FF Y:E1 P:nvUbdIZC SP:F9 PPU: 34,199 CYC:22638 $F1FA:A5 47 LDA $47 = #$52 A:42 X:FF Y:E1 P:nvUbdIZC SP:FB PPU: 52,199 CYC:22644 $F1FC:C9 52 CMP #$52 A:52 X:FF Y:E1 P:25 SP:FB PPU: 61,199 CYC:22647 $F1FE:F0 02 BEQ $F202 A:52 X:FF Y:E1 P:nvUbdIZC SP:FB PPU: 67,199 CYC:22649 $F202:C8 INY A:52 X:FF Y:E1 P:nvUbdIZC SP:FB PPU: 76,199 CYC:22652 $F203:A9 37 LDA #$37 A:52 X:FF Y:E2 P:A5 SP:FB PPU: 82,199 CYC:22654 $F205:85 47 STA $47 = #$52 A:37 X:FF Y:E2 P:25 SP:FB PPU: 88,199 CYC:22656 $F207:20 68 FA JSR $FA68 A:37 X:FF Y:E2 P:25 SP:FB PPU: 97,199 CYC:22659 $FA68:24 01 BIT $01 = #$FF A:37 X:FF Y:E2 P:25 SP:F9 PPU:115,199 CYC:22665 $FA6A:38 SEC A:37 X:FF Y:E2 P:E5 SP:F9 PPU:124,199 CYC:22668 $FA6B:A9 75 LDA #$75 A:37 X:FF Y:E2 P:E5 SP:F9 PPU:130,199 CYC:22670 $FA6D:60 RTS A:75 X:FF Y:E2 P:65 SP:F9 PPU:136,199 CYC:22672 $F20A:37 48 *RLA $48,X @ 47 = #$37 A:75 X:FF Y:E2 P:65 SP:FB PPU:154,199 CYC:22678 $F20C:EA NOP A:65 X:FF Y:E2 P:64 SP:FB PPU:172,199 CYC:22684 $F20D:EA NOP A:65 X:FF Y:E2 P:64 SP:FB PPU:178,199 CYC:22686 $F20E:EA NOP A:65 X:FF Y:E2 P:64 SP:FB PPU:184,199 CYC:22688 $F20F:EA NOP A:65 X:FF Y:E2 P:64 SP:FB PPU:190,199 CYC:22690 $F210:20 6E FA JSR $FA6E A:65 X:FF Y:E2 P:64 SP:FB PPU:196,199 CYC:22692 $FA6E:50 76 BVC $FAE6 A:65 X:FF Y:E2 P:64 SP:F9 PPU:214,199 CYC:22698 $FA70:F0 74 BEQ $FAE6 A:65 X:FF Y:E2 P:64 SP:F9 PPU:220,199 CYC:22700 $FA72:30 72 BMI $FAE6 A:65 X:FF Y:E2 P:64 SP:F9 PPU:226,199 CYC:22702 $FA74:B0 70 BCS $FAE6 A:65 X:FF Y:E2 P:64 SP:F9 PPU:232,199 CYC:22704 $FA76:C9 65 CMP #$65 A:65 X:FF Y:E2 P:64 SP:F9 PPU:238,199 CYC:22706 $FA78:D0 6C BNE $FAE6 A:65 X:FF Y:E2 P:67 SP:F9 PPU:244,199 CYC:22708 $FA7A:60 RTS A:65 X:FF Y:E2 P:67 SP:F9 PPU:250,199 CYC:22710 $F213:A5 47 LDA $47 = #$6F A:65 X:FF Y:E2 P:67 SP:FB PPU:268,199 CYC:22716 $F215:C9 6F CMP #$6F A:6F X:FF Y:E2 P:65 SP:FB PPU:277,199 CYC:22719 $F217:F0 02 BEQ $F21B A:6F X:FF Y:E2 P:67 SP:FB PPU:283,199 CYC:22721 $F21B:A9 A5 LDA #$A5 A:6F X:FF Y:E2 P:67 SP:FB PPU:292,199 CYC:22724 $F21D:8D 47 06 STA $0647 = #$6F A:A5 X:FF Y:E2 P:E5 SP:FB PPU:298,199 CYC:22726 $F220:A0 FF LDY #$FF A:A5 X:FF Y:E2 P:E5 SP:FB PPU:310,199 CYC:22730 $F222:20 53 FB JSR $FB53 A:A5 X:FF Y:FF P:E5 SP:FB PPU:316,199 CYC:22732 $FB53:24 01 BIT $01 = #$FF A:A5 X:FF Y:FF P:E5 SP:F9 PPU:334,199 CYC:22738 $FB55:18 CLC A:A5 X:FF Y:FF P:E5 SP:F9 PPU: 2,200 CYC:22741 $FB56:A9 B3 LDA #$B3 A:A5 X:FF Y:FF P:NVUbdIzc SP:F9 PPU: 8,200 CYC:22743 $FB58:60 RTS A:B3 X:FF Y:FF P:NVUbdIzc SP:F9 PPU: 14,200 CYC:22745 $F225:3B 48 05 *RLA $0548,Y @ 0647 = #$A5 A:B3 X:FF Y:FF P:NVUbdIzc SP:FB PPU: 32,200 CYC:22751 $F228:EA NOP A:02 X:FF Y:FF P:65 SP:FB PPU: 53,200 CYC:22758 $F229:EA NOP A:02 X:FF Y:FF P:65 SP:FB PPU: 59,200 CYC:22760 $F22A:08 PHP A:02 X:FF Y:FF P:65 SP:FB PPU: 65,200 CYC:22762 $F22B:48 PHA A:02 X:FF Y:FF P:65 SP:FA PPU: 74,200 CYC:22765 $F22C:A0 E3 LDY #$E3 A:02 X:FF Y:FF P:65 SP:F9 PPU: 83,200 CYC:22768 $F22E:68 PLA A:02 X:FF Y:E3 P:E5 SP:F9 PPU: 89,200 CYC:22770 $F22F:28 PLP A:02 X:FF Y:E3 P:65 SP:FA PPU:101,200 CYC:22774 $F230:20 59 FB JSR $FB59 A:02 X:FF Y:E3 P:65 SP:FB PPU:113,200 CYC:22778 $FB59:50 1A BVC $FB75 A:02 X:FF Y:E3 P:65 SP:F9 PPU:131,200 CYC:22784 $FB5B:90 18 BCC $FB75 A:02 X:FF Y:E3 P:65 SP:F9 PPU:137,200 CYC:22786 $FB5D:30 16 BMI $FB75 A:02 X:FF Y:E3 P:65 SP:F9 PPU:143,200 CYC:22788 $FB5F:C9 02 CMP #$02 A:02 X:FF Y:E3 P:65 SP:F9 PPU:149,200 CYC:22790 $FB61:D0 12 BNE $FB75 A:02 X:FF Y:E3 P:67 SP:F9 PPU:155,200 CYC:22792 $FB63:60 RTS A:02 X:FF Y:E3 P:67 SP:F9 PPU:161,200 CYC:22794 $F233:AD 47 06 LDA $0647 = #$4A A:02 X:FF Y:E3 P:67 SP:FB PPU:179,200 CYC:22800 $F236:C9 4A CMP #$4A A:4A X:FF Y:E3 P:65 SP:FB PPU:191,200 CYC:22804 $F238:F0 02 BEQ $F23C A:4A X:FF Y:E3 P:67 SP:FB PPU:197,200 CYC:22806 $F23C:A0 FF LDY #$FF A:4A X:FF Y:E3 P:67 SP:FB PPU:206,200 CYC:22809 $F23E:A9 29 LDA #$29 A:4A X:FF Y:FF P:E5 SP:FB PPU:212,200 CYC:22811 $F240:8D 47 06 STA $0647 = #$4A A:29 X:FF Y:FF P:65 SP:FB PPU:218,200 CYC:22813 $F243:20 64 FB JSR $FB64 A:29 X:FF Y:FF P:65 SP:FB PPU:230,200 CYC:22817 $FB64:B8 CLV A:29 X:FF Y:FF P:65 SP:F9 PPU:248,200 CYC:22823 $FB65:18 CLC A:29 X:FF Y:FF P:25 SP:F9 PPU:254,200 CYC:22825 $FB66:A9 42 LDA #$42 A:29 X:FF Y:FF P:nvUbdIzc SP:F9 PPU:260,200 CYC:22827 $FB68:60 RTS A:42 X:FF Y:FF P:nvUbdIzc SP:F9 PPU:266,200 CYC:22829 $F246:3B 48 05 *RLA $0548,Y @ 0647 = #$29 A:42 X:FF Y:FF P:nvUbdIzc SP:FB PPU:284,200 CYC:22835 $F249:EA NOP A:42 X:FF Y:FF P:nvUbdIzc SP:FB PPU:305,200 CYC:22842 $F24A:EA NOP A:42 X:FF Y:FF P:nvUbdIzc SP:FB PPU:311,200 CYC:22844 $F24B:08 PHP A:42 X:FF Y:FF P:nvUbdIzc SP:FB PPU:317,200 CYC:22846 $F24C:48 PHA A:42 X:FF Y:FF P:nvUbdIzc SP:FA PPU:326,200 CYC:22849 $F24D:A0 E4 LDY #$E4 A:42 X:FF Y:FF P:nvUbdIzc SP:F9 PPU:335,200 CYC:22852 $F24F:68 PLA A:42 X:FF Y:E4 P:NvUbdIzc SP:F9 PPU: 0,201 CYC:22854 $F250:28 PLP A:42 X:FF Y:E4 P:nvUbdIzc SP:FA PPU: 12,201 CYC:22858 $F251:20 69 FB JSR $FB69 A:42 X:FF Y:E4 P:nvUbdIzc SP:FB PPU: 24,201 CYC:22862 $FB69:70 0A BVS $FB75 A:42 X:FF Y:E4 P:nvUbdIzc SP:F9 PPU: 42,201 CYC:22868 $FB6B:F0 08 BEQ $FB75 A:42 X:FF Y:E4 P:nvUbdIzc SP:F9 PPU: 48,201 CYC:22870 $FB6D:30 06 BMI $FB75 A:42 X:FF Y:E4 P:nvUbdIzc SP:F9 PPU: 54,201 CYC:22872 $FB6F:B0 04 BCS $FB75 A:42 X:FF Y:E4 P:nvUbdIzc SP:F9 PPU: 60,201 CYC:22874 $FB71:C9 42 CMP #$42 A:42 X:FF Y:E4 P:nvUbdIzc SP:F9 PPU: 66,201 CYC:22876 $FB73:F0 02 BEQ $FB77 A:42 X:FF Y:E4 P:nvUbdIZC SP:F9 PPU: 72,201 CYC:22878 $FB77:60 RTS A:42 X:FF Y:E4 P:nvUbdIZC SP:F9 PPU: 81,201 CYC:22881 $F254:AD 47 06 LDA $0647 = #$52 A:42 X:FF Y:E4 P:nvUbdIZC SP:FB PPU: 99,201 CYC:22887 $F257:C9 52 CMP #$52 A:52 X:FF Y:E4 P:25 SP:FB PPU:111,201 CYC:22891 $F259:F0 02 BEQ $F25D A:52 X:FF Y:E4 P:nvUbdIZC SP:FB PPU:117,201 CYC:22893 $F25D:A0 FF LDY #$FF A:52 X:FF Y:E4 P:nvUbdIZC SP:FB PPU:126,201 CYC:22896 $F25F:A9 37 LDA #$37 A:52 X:FF Y:FF P:A5 SP:FB PPU:132,201 CYC:22898 $F261:8D 47 06 STA $0647 = #$52 A:37 X:FF Y:FF P:25 SP:FB PPU:138,201 CYC:22900 $F264:20 68 FA JSR $FA68 A:37 X:FF Y:FF P:25 SP:FB PPU:150,201 CYC:22904 $FA68:24 01 BIT $01 = #$FF A:37 X:FF Y:FF P:25 SP:F9 PPU:168,201 CYC:22910 $FA6A:38 SEC A:37 X:FF Y:FF P:E5 SP:F9 PPU:177,201 CYC:22913 $FA6B:A9 75 LDA #$75 A:37 X:FF Y:FF P:E5 SP:F9 PPU:183,201 CYC:22915 $FA6D:60 RTS A:75 X:FF Y:FF P:65 SP:F9 PPU:189,201 CYC:22917 $F267:3B 48 05 *RLA $0548,Y @ 0647 = #$37 A:75 X:FF Y:FF P:65 SP:FB PPU:207,201 CYC:22923 $F26A:EA NOP A:65 X:FF Y:FF P:64 SP:FB PPU:228,201 CYC:22930 $F26B:EA NOP A:65 X:FF Y:FF P:64 SP:FB PPU:234,201 CYC:22932 $F26C:08 PHP A:65 X:FF Y:FF P:64 SP:FB PPU:240,201 CYC:22934 $F26D:48 PHA A:65 X:FF Y:FF P:64 SP:FA PPU:249,201 CYC:22937 $F26E:A0 E5 LDY #$E5 A:65 X:FF Y:FF P:64 SP:F9 PPU:258,201 CYC:22940 $F270:68 PLA A:65 X:FF Y:E5 P:NVUbdIzc SP:F9 PPU:264,201 CYC:22942 $F271:28 PLP A:65 X:FF Y:E5 P:64 SP:FA PPU:276,201 CYC:22946 $F272:20 6E FA JSR $FA6E A:65 X:FF Y:E5 P:64 SP:FB PPU:288,201 CYC:22950 $FA6E:50 76 BVC $FAE6 A:65 X:FF Y:E5 P:64 SP:F9 PPU:306,201 CYC:22956 $FA70:F0 74 BEQ $FAE6 A:65 X:FF Y:E5 P:64 SP:F9 PPU:312,201 CYC:22958 $FA72:30 72 BMI $FAE6 A:65 X:FF Y:E5 P:64 SP:F9 PPU:318,201 CYC:22960 $FA74:B0 70 BCS $FAE6 A:65 X:FF Y:E5 P:64 SP:F9 PPU:324,201 CYC:22962 $FA76:C9 65 CMP #$65 A:65 X:FF Y:E5 P:64 SP:F9 PPU:330,201 CYC:22964 $FA78:D0 6C BNE $FAE6 A:65 X:FF Y:E5 P:67 SP:F9 PPU:336,201 CYC:22966 $FA7A:60 RTS A:65 X:FF Y:E5 P:67 SP:F9 PPU: 1,202 CYC:22968 $F275:AD 47 06 LDA $0647 = #$6F A:65 X:FF Y:E5 P:67 SP:FB PPU: 19,202 CYC:22974 $F278:C9 6F CMP #$6F A:6F X:FF Y:E5 P:65 SP:FB PPU: 31,202 CYC:22978 $F27A:F0 02 BEQ $F27E A:6F X:FF Y:E5 P:67 SP:FB PPU: 37,202 CYC:22980 $F27E:A0 E6 LDY #$E6 A:6F X:FF Y:E5 P:67 SP:FB PPU: 46,202 CYC:22983 $F280:A2 FF LDX #$FF A:6F X:FF Y:E6 P:E5 SP:FB PPU: 52,202 CYC:22985 $F282:A9 A5 LDA #$A5 A:6F X:FF Y:E6 P:E5 SP:FB PPU: 58,202 CYC:22987 $F284:8D 47 06 STA $0647 = #$6F A:A5 X:FF Y:E6 P:E5 SP:FB PPU: 64,202 CYC:22989 $F287:20 53 FB JSR $FB53 A:A5 X:FF Y:E6 P:E5 SP:FB PPU: 76,202 CYC:22993 $FB53:24 01 BIT $01 = #$FF A:A5 X:FF Y:E6 P:E5 SP:F9 PPU: 94,202 CYC:22999 $FB55:18 CLC A:A5 X:FF Y:E6 P:E5 SP:F9 PPU:103,202 CYC:23002 $FB56:A9 B3 LDA #$B3 A:A5 X:FF Y:E6 P:NVUbdIzc SP:F9 PPU:109,202 CYC:23004 $FB58:60 RTS A:B3 X:FF Y:E6 P:NVUbdIzc SP:F9 PPU:115,202 CYC:23006 $F28A:3F 48 05 *RLA $0548,X @ 0647 = #$A5 A:B3 X:FF Y:E6 P:NVUbdIzc SP:FB PPU:133,202 CYC:23012 $F28D:EA NOP A:02 X:FF Y:E6 P:65 SP:FB PPU:154,202 CYC:23019 $F28E:EA NOP A:02 X:FF Y:E6 P:65 SP:FB PPU:160,202 CYC:23021 $F28F:EA NOP A:02 X:FF Y:E6 P:65 SP:FB PPU:166,202 CYC:23023 $F290:EA NOP A:02 X:FF Y:E6 P:65 SP:FB PPU:172,202 CYC:23025 $F291:20 59 FB JSR $FB59 A:02 X:FF Y:E6 P:65 SP:FB PPU:178,202 CYC:23027 $FB59:50 1A BVC $FB75 A:02 X:FF Y:E6 P:65 SP:F9 PPU:196,202 CYC:23033 $FB5B:90 18 BCC $FB75 A:02 X:FF Y:E6 P:65 SP:F9 PPU:202,202 CYC:23035 $FB5D:30 16 BMI $FB75 A:02 X:FF Y:E6 P:65 SP:F9 PPU:208,202 CYC:23037 $FB5F:C9 02 CMP #$02 A:02 X:FF Y:E6 P:65 SP:F9 PPU:214,202 CYC:23039 $FB61:D0 12 BNE $FB75 A:02 X:FF Y:E6 P:67 SP:F9 PPU:220,202 CYC:23041 $FB63:60 RTS A:02 X:FF Y:E6 P:67 SP:F9 PPU:226,202 CYC:23043 $F294:AD 47 06 LDA $0647 = #$4A A:02 X:FF Y:E6 P:67 SP:FB PPU:244,202 CYC:23049 $F297:C9 4A CMP #$4A A:4A X:FF Y:E6 P:65 SP:FB PPU:256,202 CYC:23053 $F299:F0 02 BEQ $F29D A:4A X:FF Y:E6 P:67 SP:FB PPU:262,202 CYC:23055 $F29D:C8 INY A:4A X:FF Y:E6 P:67 SP:FB PPU:271,202 CYC:23058 $F29E:A9 29 LDA #$29 A:4A X:FF Y:E7 P:E5 SP:FB PPU:277,202 CYC:23060 $F2A0:8D 47 06 STA $0647 = #$4A A:29 X:FF Y:E7 P:65 SP:FB PPU:283,202 CYC:23062 $F2A3:20 64 FB JSR $FB64 A:29 X:FF Y:E7 P:65 SP:FB PPU:295,202 CYC:23066 $FB64:B8 CLV A:29 X:FF Y:E7 P:65 SP:F9 PPU:313,202 CYC:23072 $FB65:18 CLC A:29 X:FF Y:E7 P:25 SP:F9 PPU:319,202 CYC:23074 $FB66:A9 42 LDA #$42 A:29 X:FF Y:E7 P:nvUbdIzc SP:F9 PPU:325,202 CYC:23076 $FB68:60 RTS A:42 X:FF Y:E7 P:nvUbdIzc SP:F9 PPU:331,202 CYC:23078 $F2A6:3F 48 05 *RLA $0548,X @ 0647 = #$29 A:42 X:FF Y:E7 P:nvUbdIzc SP:FB PPU: 8,203 CYC:23084 $F2A9:EA NOP A:42 X:FF Y:E7 P:nvUbdIzc SP:FB PPU: 29,203 CYC:23091 $F2AA:EA NOP A:42 X:FF Y:E7 P:nvUbdIzc SP:FB PPU: 35,203 CYC:23093 $F2AB:EA NOP A:42 X:FF Y:E7 P:nvUbdIzc SP:FB PPU: 41,203 CYC:23095 $F2AC:EA NOP A:42 X:FF Y:E7 P:nvUbdIzc SP:FB PPU: 47,203 CYC:23097 $F2AD:20 69 FB JSR $FB69 A:42 X:FF Y:E7 P:nvUbdIzc SP:FB PPU: 53,203 CYC:23099 $FB69:70 0A BVS $FB75 A:42 X:FF Y:E7 P:nvUbdIzc SP:F9 PPU: 71,203 CYC:23105 $FB6B:F0 08 BEQ $FB75 A:42 X:FF Y:E7 P:nvUbdIzc SP:F9 PPU: 77,203 CYC:23107 $FB6D:30 06 BMI $FB75 A:42 X:FF Y:E7 P:nvUbdIzc SP:F9 PPU: 83,203 CYC:23109 $FB6F:B0 04 BCS $FB75 A:42 X:FF Y:E7 P:nvUbdIzc SP:F9 PPU: 89,203 CYC:23111 $FB71:C9 42 CMP #$42 A:42 X:FF Y:E7 P:nvUbdIzc SP:F9 PPU: 95,203 CYC:23113 $FB73:F0 02 BEQ $FB77 A:42 X:FF Y:E7 P:nvUbdIZC SP:F9 PPU:101,203 CYC:23115 $FB77:60 RTS A:42 X:FF Y:E7 P:nvUbdIZC SP:F9 PPU:110,203 CYC:23118 $F2B0:AD 47 06 LDA $0647 = #$52 A:42 X:FF Y:E7 P:nvUbdIZC SP:FB PPU:128,203 CYC:23124 $F2B3:C9 52 CMP #$52 A:52 X:FF Y:E7 P:25 SP:FB PPU:140,203 CYC:23128 $F2B5:F0 02 BEQ $F2B9 A:52 X:FF Y:E7 P:nvUbdIZC SP:FB PPU:146,203 CYC:23130 $F2B9:C8 INY A:52 X:FF Y:E7 P:nvUbdIZC SP:FB PPU:155,203 CYC:23133 $F2BA:A9 37 LDA #$37 A:52 X:FF Y:E8 P:A5 SP:FB PPU:161,203 CYC:23135 $F2BC:8D 47 06 STA $0647 = #$52 A:37 X:FF Y:E8 P:25 SP:FB PPU:167,203 CYC:23137 $F2BF:20 68 FA JSR $FA68 A:37 X:FF Y:E8 P:25 SP:FB PPU:179,203 CYC:23141 $FA68:24 01 BIT $01 = #$FF A:37 X:FF Y:E8 P:25 SP:F9 PPU:197,203 CYC:23147 $FA6A:38 SEC A:37 X:FF Y:E8 P:E5 SP:F9 PPU:206,203 CYC:23150 $FA6B:A9 75 LDA #$75 A:37 X:FF Y:E8 P:E5 SP:F9 PPU:212,203 CYC:23152 $FA6D:60 RTS A:75 X:FF Y:E8 P:65 SP:F9 PPU:218,203 CYC:23154 $F2C2:3F 48 05 *RLA $0548,X @ 0647 = #$37 A:75 X:FF Y:E8 P:65 SP:FB PPU:236,203 CYC:23160 $F2C5:EA NOP A:65 X:FF Y:E8 P:64 SP:FB PPU:257,203 CYC:23167 $F2C6:EA NOP A:65 X:FF Y:E8 P:64 SP:FB PPU:263,203 CYC:23169 $F2C7:EA NOP A:65 X:FF Y:E8 P:64 SP:FB PPU:269,203 CYC:23171 $F2C8:EA NOP A:65 X:FF Y:E8 P:64 SP:FB PPU:275,203 CYC:23173 $F2C9:20 6E FA JSR $FA6E A:65 X:FF Y:E8 P:64 SP:FB PPU:281,203 CYC:23175 $FA6E:50 76 BVC $FAE6 A:65 X:FF Y:E8 P:64 SP:F9 PPU:299,203 CYC:23181 $FA70:F0 74 BEQ $FAE6 A:65 X:FF Y:E8 P:64 SP:F9 PPU:305,203 CYC:23183 $FA72:30 72 BMI $FAE6 A:65 X:FF Y:E8 P:64 SP:F9 PPU:311,203 CYC:23185 $FA74:B0 70 BCS $FAE6 A:65 X:FF Y:E8 P:64 SP:F9 PPU:317,203 CYC:23187 $FA76:C9 65 CMP #$65 A:65 X:FF Y:E8 P:64 SP:F9 PPU:323,203 CYC:23189 $FA78:D0 6C BNE $FAE6 A:65 X:FF Y:E8 P:67 SP:F9 PPU:329,203 CYC:23191 $FA7A:60 RTS A:65 X:FF Y:E8 P:67 SP:F9 PPU:335,203 CYC:23193 $F2CC:AD 47 06 LDA $0647 = #$6F A:65 X:FF Y:E8 P:67 SP:FB PPU: 12,204 CYC:23199 $F2CF:C9 6F CMP #$6F A:6F X:FF Y:E8 P:65 SP:FB PPU: 24,204 CYC:23203 $F2D1:F0 02 BEQ $F2D5 A:6F X:FF Y:E8 P:67 SP:FB PPU: 30,204 CYC:23205 $F2D5:60 RTS A:6F X:FF Y:E8 P:67 SP:FB PPU: 39,204 CYC:23208 $C647:20 D6 F2 JSR $F2D6 A:6F X:FF Y:E8 P:67 SP:FD PPU: 57,204 CYC:23214 $F2D6:A9 FF LDA #$FF A:6F X:FF Y:E8 P:67 SP:FB PPU: 75,204 CYC:23220 $F2D8:85 01 STA $01 = #$FF A:FF X:FF Y:E8 P:E5 SP:FB PPU: 81,204 CYC:23222 $F2DA:A0 E9 LDY #$E9 A:FF X:FF Y:E8 P:E5 SP:FB PPU: 90,204 CYC:23225 $F2DC:A2 02 LDX #$02 A:FF X:FF Y:E9 P:E5 SP:FB PPU: 96,204 CYC:23227 $F2DE:A9 47 LDA #$47 A:FF X:02 Y:E9 P:65 SP:FB PPU:102,204 CYC:23229 $F2E0:85 47 STA $47 = #$6F A:47 X:02 Y:E9 P:65 SP:FB PPU:108,204 CYC:23231 $F2E2:A9 06 LDA #$06 A:47 X:02 Y:E9 P:65 SP:FB PPU:117,204 CYC:23234 $F2E4:85 48 STA $48 = #$06 A:06 X:02 Y:E9 P:65 SP:FB PPU:123,204 CYC:23236 $F2E6:A9 A5 LDA #$A5 A:06 X:02 Y:E9 P:65 SP:FB PPU:132,204 CYC:23239 $F2E8:8D 47 06 STA $0647 = #$6F A:A5 X:02 Y:E9 P:E5 SP:FB PPU:138,204 CYC:23241 $F2EB:20 1D FB JSR $FB1D A:A5 X:02 Y:E9 P:E5 SP:FB PPU:150,204 CYC:23245 $FB1D:24 01 BIT $01 = #$FF A:A5 X:02 Y:E9 P:E5 SP:F9 PPU:168,204 CYC:23251 $FB1F:18 CLC A:A5 X:02 Y:E9 P:E5 SP:F9 PPU:177,204 CYC:23254 $FB20:A9 B3 LDA #$B3 A:A5 X:02 Y:E9 P:NVUbdIzc SP:F9 PPU:183,204 CYC:23256 $FB22:60 RTS A:B3 X:02 Y:E9 P:NVUbdIzc SP:F9 PPU:189,204 CYC:23258 $F2EE:43 45 *SRE ($45,X) @ 47 = #$0647 = A5 A:B3 X:02 Y:E9 P:NVUbdIzc SP:FB PPU:207,204 CYC:23264 $F2F0:EA NOP A:E1 X:02 Y:E9 P:E5 SP:FB PPU:231,204 CYC:23272 $F2F1:EA NOP A:E1 X:02 Y:E9 P:E5 SP:FB PPU:237,204 CYC:23274 $F2F2:EA NOP A:E1 X:02 Y:E9 P:E5 SP:FB PPU:243,204 CYC:23276 $F2F3:EA NOP A:E1 X:02 Y:E9 P:E5 SP:FB PPU:249,204 CYC:23278 $F2F4:20 23 FB JSR $FB23 A:E1 X:02 Y:E9 P:E5 SP:FB PPU:255,204 CYC:23280 $FB23:50 50 BVC $FB75 A:E1 X:02 Y:E9 P:E5 SP:F9 PPU:273,204 CYC:23286 $FB25:90 4E BCC $FB75 A:E1 X:02 Y:E9 P:E5 SP:F9 PPU:279,204 CYC:23288 $FB27:10 4C BPL $FB75 A:E1 X:02 Y:E9 P:E5 SP:F9 PPU:285,204 CYC:23290 $FB29:C9 E1 CMP #$E1 A:E1 X:02 Y:E9 P:E5 SP:F9 PPU:291,204 CYC:23292 $FB2B:D0 48 BNE $FB75 A:E1 X:02 Y:E9 P:67 SP:F9 PPU:297,204 CYC:23294 $FB2D:60 RTS A:E1 X:02 Y:E9 P:67 SP:F9 PPU:303,204 CYC:23296 $F2F7:AD 47 06 LDA $0647 = #$52 A:E1 X:02 Y:E9 P:67 SP:FB PPU:321,204 CYC:23302 $F2FA:C9 52 CMP #$52 A:52 X:02 Y:E9 P:65 SP:FB PPU:333,204 CYC:23306 $F2FC:F0 02 BEQ $F300 A:52 X:02 Y:E9 P:67 SP:FB PPU:339,204 CYC:23308 $F300:C8 INY A:52 X:02 Y:E9 P:67 SP:FB PPU: 10,205 CYC:23312 $F301:A9 29 LDA #$29 A:52 X:02 Y:EA P:E5 SP:FB PPU: 16,205 CYC:23314 $F303:8D 47 06 STA $0647 = #$52 A:29 X:02 Y:EA P:65 SP:FB PPU: 22,205 CYC:23316 $F306:20 2E FB JSR $FB2E A:29 X:02 Y:EA P:65 SP:FB PPU: 34,205 CYC:23320 $FB2E:B8 CLV A:29 X:02 Y:EA P:65 SP:F9 PPU: 52,205 CYC:23326 $FB2F:18 CLC A:29 X:02 Y:EA P:25 SP:F9 PPU: 58,205 CYC:23328 $FB30:A9 42 LDA #$42 A:29 X:02 Y:EA P:nvUbdIzc SP:F9 PPU: 64,205 CYC:23330 $FB32:60 RTS A:42 X:02 Y:EA P:nvUbdIzc SP:F9 PPU: 70,205 CYC:23332 $F309:43 45 *SRE ($45,X) @ 47 = #$0647 = 29 A:42 X:02 Y:EA P:nvUbdIzc SP:FB PPU: 88,205 CYC:23338 $F30B:EA NOP A:56 X:02 Y:EA P:25 SP:FB PPU:112,205 CYC:23346 $F30C:EA NOP A:56 X:02 Y:EA P:25 SP:FB PPU:118,205 CYC:23348 $F30D:EA NOP A:56 X:02 Y:EA P:25 SP:FB PPU:124,205 CYC:23350 $F30E:EA NOP A:56 X:02 Y:EA P:25 SP:FB PPU:130,205 CYC:23352 $F30F:20 33 FB JSR $FB33 A:56 X:02 Y:EA P:25 SP:FB PPU:136,205 CYC:23354 $FB33:70 40 BVS $FB75 A:56 X:02 Y:EA P:25 SP:F9 PPU:154,205 CYC:23360 $FB35:F0 3E BEQ $FB75 A:56 X:02 Y:EA P:25 SP:F9 PPU:160,205 CYC:23362 $FB37:30 3C BMI $FB75 A:56 X:02 Y:EA P:25 SP:F9 PPU:166,205 CYC:23364 $FB39:90 3A BCC $FB75 A:56 X:02 Y:EA P:25 SP:F9 PPU:172,205 CYC:23366 $FB3B:C9 56 CMP #$56 A:56 X:02 Y:EA P:25 SP:F9 PPU:178,205 CYC:23368 $FB3D:D0 36 BNE $FB75 A:56 X:02 Y:EA P:nvUbdIZC SP:F9 PPU:184,205 CYC:23370 $FB3F:60 RTS A:56 X:02 Y:EA P:nvUbdIZC SP:F9 PPU:190,205 CYC:23372 $F312:AD 47 06 LDA $0647 = #$14 A:56 X:02 Y:EA P:nvUbdIZC SP:FB PPU:208,205 CYC:23378 $F315:C9 14 CMP #$14 A:14 X:02 Y:EA P:25 SP:FB PPU:220,205 CYC:23382 $F317:F0 02 BEQ $F31B A:14 X:02 Y:EA P:nvUbdIZC SP:FB PPU:226,205 CYC:23384 $F31B:C8 INY A:14 X:02 Y:EA P:nvUbdIZC SP:FB PPU:235,205 CYC:23387 $F31C:A9 37 LDA #$37 A:14 X:02 Y:EB P:A5 SP:FB PPU:241,205 CYC:23389 $F31E:8D 47 06 STA $0647 = #$14 A:37 X:02 Y:EB P:25 SP:FB PPU:247,205 CYC:23391 $F321:20 40 FB JSR $FB40 A:37 X:02 Y:EB P:25 SP:FB PPU:259,205 CYC:23395 $FB40:24 01 BIT $01 = #$FF A:37 X:02 Y:EB P:25 SP:F9 PPU:277,205 CYC:23401 $FB42:38 SEC A:37 X:02 Y:EB P:E5 SP:F9 PPU:286,205 CYC:23404 $FB43:A9 75 LDA #$75 A:37 X:02 Y:EB P:E5 SP:F9 PPU:292,205 CYC:23406 $FB45:60 RTS A:75 X:02 Y:EB P:65 SP:F9 PPU:298,205 CYC:23408 $F324:43 45 *SRE ($45,X) @ 47 = #$0647 = 37 A:75 X:02 Y:EB P:65 SP:FB PPU:316,205 CYC:23414 $F326:EA NOP A:6E X:02 Y:EB P:65 SP:FB PPU:340,205 CYC:23422 $F327:EA NOP A:6E X:02 Y:EB P:65 SP:FB PPU: 5,206 CYC:23424 $F328:EA NOP A:6E X:02 Y:EB P:65 SP:FB PPU: 11,206 CYC:23426 $F329:EA NOP A:6E X:02 Y:EB P:65 SP:FB PPU: 17,206 CYC:23428 $F32A:20 46 FB JSR $FB46 A:6E X:02 Y:EB P:65 SP:FB PPU: 23,206 CYC:23430 $FB46:50 2D BVC $FB75 A:6E X:02 Y:EB P:65 SP:F9 PPU: 41,206 CYC:23436 $FB48:F0 2B BEQ $FB75 A:6E X:02 Y:EB P:65 SP:F9 PPU: 47,206 CYC:23438 $FB4A:30 29 BMI $FB75 A:6E X:02 Y:EB P:65 SP:F9 PPU: 53,206 CYC:23440 $FB4C:90 27 BCC $FB75 A:6E X:02 Y:EB P:65 SP:F9 PPU: 59,206 CYC:23442 $FB4E:C9 6E CMP #$6E A:6E X:02 Y:EB P:65 SP:F9 PPU: 65,206 CYC:23444 $FB50:D0 23 BNE $FB75 A:6E X:02 Y:EB P:67 SP:F9 PPU: 71,206 CYC:23446 $FB52:60 RTS A:6E X:02 Y:EB P:67 SP:F9 PPU: 77,206 CYC:23448 $F32D:AD 47 06 LDA $0647 = #$1B A:6E X:02 Y:EB P:67 SP:FB PPU: 95,206 CYC:23454 $F330:C9 1B CMP #$1B A:1B X:02 Y:EB P:65 SP:FB PPU:107,206 CYC:23458 $F332:F0 02 BEQ $F336 A:1B X:02 Y:EB P:67 SP:FB PPU:113,206 CYC:23460 $F336:C8 INY A:1B X:02 Y:EB P:67 SP:FB PPU:122,206 CYC:23463 $F337:A9 A5 LDA #$A5 A:1B X:02 Y:EC P:E5 SP:FB PPU:128,206 CYC:23465 $F339:85 47 STA $47 = #$47 A:A5 X:02 Y:EC P:E5 SP:FB PPU:134,206 CYC:23467 $F33B:20 1D FB JSR $FB1D A:A5 X:02 Y:EC P:E5 SP:FB PPU:143,206 CYC:23470 $FB1D:24 01 BIT $01 = #$FF A:A5 X:02 Y:EC P:E5 SP:F9 PPU:161,206 CYC:23476 $FB1F:18 CLC A:A5 X:02 Y:EC P:E5 SP:F9 PPU:170,206 CYC:23479 $FB20:A9 B3 LDA #$B3 A:A5 X:02 Y:EC P:NVUbdIzc SP:F9 PPU:176,206 CYC:23481 $FB22:60 RTS A:B3 X:02 Y:EC P:NVUbdIzc SP:F9 PPU:182,206 CYC:23483 $F33E:47 47 *SRE $47 = #$A5 A:B3 X:02 Y:EC P:NVUbdIzc SP:FB PPU:200,206 CYC:23489 $F340:EA NOP A:E1 X:02 Y:EC P:E5 SP:FB PPU:215,206 CYC:23494 $F341:EA NOP A:E1 X:02 Y:EC P:E5 SP:FB PPU:221,206 CYC:23496 $F342:EA NOP A:E1 X:02 Y:EC P:E5 SP:FB PPU:227,206 CYC:23498 $F343:EA NOP A:E1 X:02 Y:EC P:E5 SP:FB PPU:233,206 CYC:23500 $F344:20 23 FB JSR $FB23 A:E1 X:02 Y:EC P:E5 SP:FB PPU:239,206 CYC:23502 $FB23:50 50 BVC $FB75 A:E1 X:02 Y:EC P:E5 SP:F9 PPU:257,206 CYC:23508 $FB25:90 4E BCC $FB75 A:E1 X:02 Y:EC P:E5 SP:F9 PPU:263,206 CYC:23510 $FB27:10 4C BPL $FB75 A:E1 X:02 Y:EC P:E5 SP:F9 PPU:269,206 CYC:23512 $FB29:C9 E1 CMP #$E1 A:E1 X:02 Y:EC P:E5 SP:F9 PPU:275,206 CYC:23514 $FB2B:D0 48 BNE $FB75 A:E1 X:02 Y:EC P:67 SP:F9 PPU:281,206 CYC:23516 $FB2D:60 RTS A:E1 X:02 Y:EC P:67 SP:F9 PPU:287,206 CYC:23518 $F347:A5 47 LDA $47 = #$52 A:E1 X:02 Y:EC P:67 SP:FB PPU:305,206 CYC:23524 $F349:C9 52 CMP #$52 A:52 X:02 Y:EC P:65 SP:FB PPU:314,206 CYC:23527 $F34B:F0 02 BEQ $F34F A:52 X:02 Y:EC P:67 SP:FB PPU:320,206 CYC:23529 $F34F:C8 INY A:52 X:02 Y:EC P:67 SP:FB PPU:329,206 CYC:23532 $F350:A9 29 LDA #$29 A:52 X:02 Y:ED P:E5 SP:FB PPU:335,206 CYC:23534 $F352:85 47 STA $47 = #$52 A:29 X:02 Y:ED P:65 SP:FB PPU: 0,207 CYC:23536 $F354:20 2E FB JSR $FB2E A:29 X:02 Y:ED P:65 SP:FB PPU: 9,207 CYC:23539 $FB2E:B8 CLV A:29 X:02 Y:ED P:65 SP:F9 PPU: 27,207 CYC:23545 $FB2F:18 CLC A:29 X:02 Y:ED P:25 SP:F9 PPU: 33,207 CYC:23547 $FB30:A9 42 LDA #$42 A:29 X:02 Y:ED P:nvUbdIzc SP:F9 PPU: 39,207 CYC:23549 $FB32:60 RTS A:42 X:02 Y:ED P:nvUbdIzc SP:F9 PPU: 45,207 CYC:23551 $F357:47 47 *SRE $47 = #$29 A:42 X:02 Y:ED P:nvUbdIzc SP:FB PPU: 63,207 CYC:23557 $F359:EA NOP A:56 X:02 Y:ED P:25 SP:FB PPU: 78,207 CYC:23562 $F35A:EA NOP A:56 X:02 Y:ED P:25 SP:FB PPU: 84,207 CYC:23564 $F35B:EA NOP A:56 X:02 Y:ED P:25 SP:FB PPU: 90,207 CYC:23566 $F35C:EA NOP A:56 X:02 Y:ED P:25 SP:FB PPU: 96,207 CYC:23568 $F35D:20 33 FB JSR $FB33 A:56 X:02 Y:ED P:25 SP:FB PPU:102,207 CYC:23570 $FB33:70 40 BVS $FB75 A:56 X:02 Y:ED P:25 SP:F9 PPU:120,207 CYC:23576 $FB35:F0 3E BEQ $FB75 A:56 X:02 Y:ED P:25 SP:F9 PPU:126,207 CYC:23578 $FB37:30 3C BMI $FB75 A:56 X:02 Y:ED P:25 SP:F9 PPU:132,207 CYC:23580 $FB39:90 3A BCC $FB75 A:56 X:02 Y:ED P:25 SP:F9 PPU:138,207 CYC:23582 $FB3B:C9 56 CMP #$56 A:56 X:02 Y:ED P:25 SP:F9 PPU:144,207 CYC:23584 $FB3D:D0 36 BNE $FB75 A:56 X:02 Y:ED P:nvUbdIZC SP:F9 PPU:150,207 CYC:23586 $FB3F:60 RTS A:56 X:02 Y:ED P:nvUbdIZC SP:F9 PPU:156,207 CYC:23588 $F360:A5 47 LDA $47 = #$14 A:56 X:02 Y:ED P:nvUbdIZC SP:FB PPU:174,207 CYC:23594 $F362:C9 14 CMP #$14 A:14 X:02 Y:ED P:25 SP:FB PPU:183,207 CYC:23597 $F364:F0 02 BEQ $F368 A:14 X:02 Y:ED P:nvUbdIZC SP:FB PPU:189,207 CYC:23599 $F368:C8 INY A:14 X:02 Y:ED P:nvUbdIZC SP:FB PPU:198,207 CYC:23602 $F369:A9 37 LDA #$37 A:14 X:02 Y:EE P:A5 SP:FB PPU:204,207 CYC:23604 $F36B:85 47 STA $47 = #$14 A:37 X:02 Y:EE P:25 SP:FB PPU:210,207 CYC:23606 $F36D:20 40 FB JSR $FB40 A:37 X:02 Y:EE P:25 SP:FB PPU:219,207 CYC:23609 $FB40:24 01 BIT $01 = #$FF A:37 X:02 Y:EE P:25 SP:F9 PPU:237,207 CYC:23615 $FB42:38 SEC A:37 X:02 Y:EE P:E5 SP:F9 PPU:246,207 CYC:23618 $FB43:A9 75 LDA #$75 A:37 X:02 Y:EE P:E5 SP:F9 PPU:252,207 CYC:23620 $FB45:60 RTS A:75 X:02 Y:EE P:65 SP:F9 PPU:258,207 CYC:23622 $F370:47 47 *SRE $47 = #$37 A:75 X:02 Y:EE P:65 SP:FB PPU:276,207 CYC:23628 $F372:EA NOP A:6E X:02 Y:EE P:65 SP:FB PPU:291,207 CYC:23633 $F373:EA NOP A:6E X:02 Y:EE P:65 SP:FB PPU:297,207 CYC:23635 $F374:EA NOP A:6E X:02 Y:EE P:65 SP:FB PPU:303,207 CYC:23637 $F375:EA NOP A:6E X:02 Y:EE P:65 SP:FB PPU:309,207 CYC:23639 $F376:20 46 FB JSR $FB46 A:6E X:02 Y:EE P:65 SP:FB PPU:315,207 CYC:23641 $FB46:50 2D BVC $FB75 A:6E X:02 Y:EE P:65 SP:F9 PPU:333,207 CYC:23647 $FB48:F0 2B BEQ $FB75 A:6E X:02 Y:EE P:65 SP:F9 PPU:339,207 CYC:23649 $FB4A:30 29 BMI $FB75 A:6E X:02 Y:EE P:65 SP:F9 PPU: 4,208 CYC:23651 $FB4C:90 27 BCC $FB75 A:6E X:02 Y:EE P:65 SP:F9 PPU: 10,208 CYC:23653 $FB4E:C9 6E CMP #$6E A:6E X:02 Y:EE P:65 SP:F9 PPU: 16,208 CYC:23655 $FB50:D0 23 BNE $FB75 A:6E X:02 Y:EE P:67 SP:F9 PPU: 22,208 CYC:23657 $FB52:60 RTS A:6E X:02 Y:EE P:67 SP:F9 PPU: 28,208 CYC:23659 $F379:A5 47 LDA $47 = #$1B A:6E X:02 Y:EE P:67 SP:FB PPU: 46,208 CYC:23665 $F37B:C9 1B CMP #$1B A:1B X:02 Y:EE P:65 SP:FB PPU: 55,208 CYC:23668 $F37D:F0 02 BEQ $F381 A:1B X:02 Y:EE P:67 SP:FB PPU: 61,208 CYC:23670 $F381:C8 INY A:1B X:02 Y:EE P:67 SP:FB PPU: 70,208 CYC:23673 $F382:A9 A5 LDA #$A5 A:1B X:02 Y:EF P:E5 SP:FB PPU: 76,208 CYC:23675 $F384:8D 47 06 STA $0647 = #$1B A:A5 X:02 Y:EF P:E5 SP:FB PPU: 82,208 CYC:23677 $F387:20 1D FB JSR $FB1D A:A5 X:02 Y:EF P:E5 SP:FB PPU: 94,208 CYC:23681 $FB1D:24 01 BIT $01 = #$FF A:A5 X:02 Y:EF P:E5 SP:F9 PPU:112,208 CYC:23687 $FB1F:18 CLC A:A5 X:02 Y:EF P:E5 SP:F9 PPU:121,208 CYC:23690 $FB20:A9 B3 LDA #$B3 A:A5 X:02 Y:EF P:NVUbdIzc SP:F9 PPU:127,208 CYC:23692 $FB22:60 RTS A:B3 X:02 Y:EF P:NVUbdIzc SP:F9 PPU:133,208 CYC:23694 $F38A:4F 47 06 *SRE $0647 = #$A5 A:B3 X:02 Y:EF P:NVUbdIzc SP:FB PPU:151,208 CYC:23700 $F38D:EA NOP A:E1 X:02 Y:EF P:E5 SP:FB PPU:169,208 CYC:23706 $F38E:EA NOP A:E1 X:02 Y:EF P:E5 SP:FB PPU:175,208 CYC:23708 $F38F:EA NOP A:E1 X:02 Y:EF P:E5 SP:FB PPU:181,208 CYC:23710 $F390:EA NOP A:E1 X:02 Y:EF P:E5 SP:FB PPU:187,208 CYC:23712 $F391:20 23 FB JSR $FB23 A:E1 X:02 Y:EF P:E5 SP:FB PPU:193,208 CYC:23714 $FB23:50 50 BVC $FB75 A:E1 X:02 Y:EF P:E5 SP:F9 PPU:211,208 CYC:23720 $FB25:90 4E BCC $FB75 A:E1 X:02 Y:EF P:E5 SP:F9 PPU:217,208 CYC:23722 $FB27:10 4C BPL $FB75 A:E1 X:02 Y:EF P:E5 SP:F9 PPU:223,208 CYC:23724 $FB29:C9 E1 CMP #$E1 A:E1 X:02 Y:EF P:E5 SP:F9 PPU:229,208 CYC:23726 $FB2B:D0 48 BNE $FB75 A:E1 X:02 Y:EF P:67 SP:F9 PPU:235,208 CYC:23728 $FB2D:60 RTS A:E1 X:02 Y:EF P:67 SP:F9 PPU:241,208 CYC:23730 $F394:AD 47 06 LDA $0647 = #$52 A:E1 X:02 Y:EF P:67 SP:FB PPU:259,208 CYC:23736 $F397:C9 52 CMP #$52 A:52 X:02 Y:EF P:65 SP:FB PPU:271,208 CYC:23740 $F399:F0 02 BEQ $F39D A:52 X:02 Y:EF P:67 SP:FB PPU:277,208 CYC:23742 $F39D:C8 INY A:52 X:02 Y:EF P:67 SP:FB PPU:286,208 CYC:23745 $F39E:A9 29 LDA #$29 A:52 X:02 Y:F0 P:E5 SP:FB PPU:292,208 CYC:23747 $F3A0:8D 47 06 STA $0647 = #$52 A:29 X:02 Y:F0 P:65 SP:FB PPU:298,208 CYC:23749 $F3A3:20 2E FB JSR $FB2E A:29 X:02 Y:F0 P:65 SP:FB PPU:310,208 CYC:23753 $FB2E:B8 CLV A:29 X:02 Y:F0 P:65 SP:F9 PPU:328,208 CYC:23759 $FB2F:18 CLC A:29 X:02 Y:F0 P:25 SP:F9 PPU:334,208 CYC:23761 $FB30:A9 42 LDA #$42 A:29 X:02 Y:F0 P:nvUbdIzc SP:F9 PPU:340,208 CYC:23763 $FB32:60 RTS A:42 X:02 Y:F0 P:nvUbdIzc SP:F9 PPU: 5,209 CYC:23765 $F3A6:4F 47 06 *SRE $0647 = #$29 A:42 X:02 Y:F0 P:nvUbdIzc SP:FB PPU: 23,209 CYC:23771 $F3A9:EA NOP A:56 X:02 Y:F0 P:25 SP:FB PPU: 41,209 CYC:23777 $F3AA:EA NOP A:56 X:02 Y:F0 P:25 SP:FB PPU: 47,209 CYC:23779 $F3AB:EA NOP A:56 X:02 Y:F0 P:25 SP:FB PPU: 53,209 CYC:23781 $F3AC:EA NOP A:56 X:02 Y:F0 P:25 SP:FB PPU: 59,209 CYC:23783 $F3AD:20 33 FB JSR $FB33 A:56 X:02 Y:F0 P:25 SP:FB PPU: 65,209 CYC:23785 $FB33:70 40 BVS $FB75 A:56 X:02 Y:F0 P:25 SP:F9 PPU: 83,209 CYC:23791 $FB35:F0 3E BEQ $FB75 A:56 X:02 Y:F0 P:25 SP:F9 PPU: 89,209 CYC:23793 $FB37:30 3C BMI $FB75 A:56 X:02 Y:F0 P:25 SP:F9 PPU: 95,209 CYC:23795 $FB39:90 3A BCC $FB75 A:56 X:02 Y:F0 P:25 SP:F9 PPU:101,209 CYC:23797 $FB3B:C9 56 CMP #$56 A:56 X:02 Y:F0 P:25 SP:F9 PPU:107,209 CYC:23799 $FB3D:D0 36 BNE $FB75 A:56 X:02 Y:F0 P:nvUbdIZC SP:F9 PPU:113,209 CYC:23801 $FB3F:60 RTS A:56 X:02 Y:F0 P:nvUbdIZC SP:F9 PPU:119,209 CYC:23803 $F3B0:AD 47 06 LDA $0647 = #$14 A:56 X:02 Y:F0 P:nvUbdIZC SP:FB PPU:137,209 CYC:23809 $F3B3:C9 14 CMP #$14 A:14 X:02 Y:F0 P:25 SP:FB PPU:149,209 CYC:23813 $F3B5:F0 02 BEQ $F3B9 A:14 X:02 Y:F0 P:nvUbdIZC SP:FB PPU:155,209 CYC:23815 $F3B9:C8 INY A:14 X:02 Y:F0 P:nvUbdIZC SP:FB PPU:164,209 CYC:23818 $F3BA:A9 37 LDA #$37 A:14 X:02 Y:F1 P:A5 SP:FB PPU:170,209 CYC:23820 $F3BC:8D 47 06 STA $0647 = #$14 A:37 X:02 Y:F1 P:25 SP:FB PPU:176,209 CYC:23822 $F3BF:20 40 FB JSR $FB40 A:37 X:02 Y:F1 P:25 SP:FB PPU:188,209 CYC:23826 $FB40:24 01 BIT $01 = #$FF A:37 X:02 Y:F1 P:25 SP:F9 PPU:206,209 CYC:23832 $FB42:38 SEC A:37 X:02 Y:F1 P:E5 SP:F9 PPU:215,209 CYC:23835 $FB43:A9 75 LDA #$75 A:37 X:02 Y:F1 P:E5 SP:F9 PPU:221,209 CYC:23837 $FB45:60 RTS A:75 X:02 Y:F1 P:65 SP:F9 PPU:227,209 CYC:23839 $F3C2:4F 47 06 *SRE $0647 = #$37 A:75 X:02 Y:F1 P:65 SP:FB PPU:245,209 CYC:23845 $F3C5:EA NOP A:6E X:02 Y:F1 P:65 SP:FB PPU:263,209 CYC:23851 $F3C6:EA NOP A:6E X:02 Y:F1 P:65 SP:FB PPU:269,209 CYC:23853 $F3C7:EA NOP A:6E X:02 Y:F1 P:65 SP:FB PPU:275,209 CYC:23855 $F3C8:EA NOP A:6E X:02 Y:F1 P:65 SP:FB PPU:281,209 CYC:23857 $F3C9:20 46 FB JSR $FB46 A:6E X:02 Y:F1 P:65 SP:FB PPU:287,209 CYC:23859 $FB46:50 2D BVC $FB75 A:6E X:02 Y:F1 P:65 SP:F9 PPU:305,209 CYC:23865 $FB48:F0 2B BEQ $FB75 A:6E X:02 Y:F1 P:65 SP:F9 PPU:311,209 CYC:23867 $FB4A:30 29 BMI $FB75 A:6E X:02 Y:F1 P:65 SP:F9 PPU:317,209 CYC:23869 $FB4C:90 27 BCC $FB75 A:6E X:02 Y:F1 P:65 SP:F9 PPU:323,209 CYC:23871 $FB4E:C9 6E CMP #$6E A:6E X:02 Y:F1 P:65 SP:F9 PPU:329,209 CYC:23873 $FB50:D0 23 BNE $FB75 A:6E X:02 Y:F1 P:67 SP:F9 PPU:335,209 CYC:23875 $FB52:60 RTS A:6E X:02 Y:F1 P:67 SP:F9 PPU: 0,210 CYC:23877 $F3CC:AD 47 06 LDA $0647 = #$1B A:6E X:02 Y:F1 P:67 SP:FB PPU: 18,210 CYC:23883 $F3CF:C9 1B CMP #$1B A:1B X:02 Y:F1 P:65 SP:FB PPU: 30,210 CYC:23887 $F3D1:F0 02 BEQ $F3D5 A:1B X:02 Y:F1 P:67 SP:FB PPU: 36,210 CYC:23889 $F3D5:A9 A5 LDA #$A5 A:1B X:02 Y:F1 P:67 SP:FB PPU: 45,210 CYC:23892 $F3D7:8D 47 06 STA $0647 = #$1B A:A5 X:02 Y:F1 P:E5 SP:FB PPU: 51,210 CYC:23894 $F3DA:A9 48 LDA #$48 A:A5 X:02 Y:F1 P:E5 SP:FB PPU: 63,210 CYC:23898 $F3DC:85 45 STA $45 = #$48 A:48 X:02 Y:F1 P:65 SP:FB PPU: 69,210 CYC:23900 $F3DE:A9 05 LDA #$05 A:48 X:02 Y:F1 P:65 SP:FB PPU: 78,210 CYC:23903 $F3E0:85 46 STA $46 = #$05 A:05 X:02 Y:F1 P:65 SP:FB PPU: 84,210 CYC:23905 $F3E2:A0 FF LDY #$FF A:05 X:02 Y:F1 P:65 SP:FB PPU: 93,210 CYC:23908 $F3E4:20 1D FB JSR $FB1D A:05 X:02 Y:FF P:E5 SP:FB PPU: 99,210 CYC:23910 $FB1D:24 01 BIT $01 = #$FF A:05 X:02 Y:FF P:E5 SP:F9 PPU:117,210 CYC:23916 $FB1F:18 CLC A:05 X:02 Y:FF P:E5 SP:F9 PPU:126,210 CYC:23919 $FB20:A9 B3 LDA #$B3 A:05 X:02 Y:FF P:NVUbdIzc SP:F9 PPU:132,210 CYC:23921 $FB22:60 RTS A:B3 X:02 Y:FF P:NVUbdIzc SP:F9 PPU:138,210 CYC:23923 $F3E7:53 45 *SRE ($45),Y = #$0548 @ 0647 = A5 A:B3 X:02 Y:FF P:NVUbdIzc SP:FB PPU:156,210 CYC:23929 $F3E9:EA NOP A:E1 X:02 Y:FF P:E5 SP:FB PPU:180,210 CYC:23937 $F3EA:EA NOP A:E1 X:02 Y:FF P:E5 SP:FB PPU:186,210 CYC:23939 $F3EB:08 PHP A:E1 X:02 Y:FF P:E5 SP:FB PPU:192,210 CYC:23941 $F3EC:48 PHA A:E1 X:02 Y:FF P:E5 SP:FA PPU:201,210 CYC:23944 $F3ED:A0 F2 LDY #$F2 A:E1 X:02 Y:FF P:E5 SP:F9 PPU:210,210 CYC:23947 $F3EF:68 PLA A:E1 X:02 Y:F2 P:E5 SP:F9 PPU:216,210 CYC:23949 $F3F0:28 PLP A:E1 X:02 Y:F2 P:E5 SP:FA PPU:228,210 CYC:23953 $F3F1:20 23 FB JSR $FB23 A:E1 X:02 Y:F2 P:E5 SP:FB PPU:240,210 CYC:23957 $FB23:50 50 BVC $FB75 A:E1 X:02 Y:F2 P:E5 SP:F9 PPU:258,210 CYC:23963 $FB25:90 4E BCC $FB75 A:E1 X:02 Y:F2 P:E5 SP:F9 PPU:264,210 CYC:23965 $FB27:10 4C BPL $FB75 A:E1 X:02 Y:F2 P:E5 SP:F9 PPU:270,210 CYC:23967 $FB29:C9 E1 CMP #$E1 A:E1 X:02 Y:F2 P:E5 SP:F9 PPU:276,210 CYC:23969 $FB2B:D0 48 BNE $FB75 A:E1 X:02 Y:F2 P:67 SP:F9 PPU:282,210 CYC:23971 $FB2D:60 RTS A:E1 X:02 Y:F2 P:67 SP:F9 PPU:288,210 CYC:23973 $F3F4:AD 47 06 LDA $0647 = #$52 A:E1 X:02 Y:F2 P:67 SP:FB PPU:306,210 CYC:23979 $F3F7:C9 52 CMP #$52 A:52 X:02 Y:F2 P:65 SP:FB PPU:318,210 CYC:23983 $F3F9:F0 02 BEQ $F3FD A:52 X:02 Y:F2 P:67 SP:FB PPU:324,210 CYC:23985 $F3FD:A0 FF LDY #$FF A:52 X:02 Y:F2 P:67 SP:FB PPU:333,210 CYC:23988 $F3FF:A9 29 LDA #$29 A:52 X:02 Y:FF P:E5 SP:FB PPU:339,210 CYC:23990 $F401:8D 47 06 STA $0647 = #$52 A:29 X:02 Y:FF P:65 SP:FB PPU: 4,211 CYC:23992 $F404:20 2E FB JSR $FB2E A:29 X:02 Y:FF P:65 SP:FB PPU: 16,211 CYC:23996 $FB2E:B8 CLV A:29 X:02 Y:FF P:65 SP:F9 PPU: 34,211 CYC:24002 $FB2F:18 CLC A:29 X:02 Y:FF P:25 SP:F9 PPU: 40,211 CYC:24004 $FB30:A9 42 LDA #$42 A:29 X:02 Y:FF P:nvUbdIzc SP:F9 PPU: 46,211 CYC:24006 $FB32:60 RTS A:42 X:02 Y:FF P:nvUbdIzc SP:F9 PPU: 52,211 CYC:24008 $F407:53 45 *SRE ($45),Y = #$0548 @ 0647 = 29 A:42 X:02 Y:FF P:nvUbdIzc SP:FB PPU: 70,211 CYC:24014 $F409:EA NOP A:56 X:02 Y:FF P:25 SP:FB PPU: 94,211 CYC:24022 $F40A:EA NOP A:56 X:02 Y:FF P:25 SP:FB PPU:100,211 CYC:24024 $F40B:08 PHP A:56 X:02 Y:FF P:25 SP:FB PPU:106,211 CYC:24026 $F40C:48 PHA A:56 X:02 Y:FF P:25 SP:FA PPU:115,211 CYC:24029 $F40D:A0 F3 LDY #$F3 A:56 X:02 Y:FF P:25 SP:F9 PPU:124,211 CYC:24032 $F40F:68 PLA A:56 X:02 Y:F3 P:A5 SP:F9 PPU:130,211 CYC:24034 $F410:28 PLP A:56 X:02 Y:F3 P:25 SP:FA PPU:142,211 CYC:24038 $F411:20 33 FB JSR $FB33 A:56 X:02 Y:F3 P:25 SP:FB PPU:154,211 CYC:24042 $FB33:70 40 BVS $FB75 A:56 X:02 Y:F3 P:25 SP:F9 PPU:172,211 CYC:24048 $FB35:F0 3E BEQ $FB75 A:56 X:02 Y:F3 P:25 SP:F9 PPU:178,211 CYC:24050 $FB37:30 3C BMI $FB75 A:56 X:02 Y:F3 P:25 SP:F9 PPU:184,211 CYC:24052 $FB39:90 3A BCC $FB75 A:56 X:02 Y:F3 P:25 SP:F9 PPU:190,211 CYC:24054 $FB3B:C9 56 CMP #$56 A:56 X:02 Y:F3 P:25 SP:F9 PPU:196,211 CYC:24056 $FB3D:D0 36 BNE $FB75 A:56 X:02 Y:F3 P:nvUbdIZC SP:F9 PPU:202,211 CYC:24058 $FB3F:60 RTS A:56 X:02 Y:F3 P:nvUbdIZC SP:F9 PPU:208,211 CYC:24060 $F414:AD 47 06 LDA $0647 = #$14 A:56 X:02 Y:F3 P:nvUbdIZC SP:FB PPU:226,211 CYC:24066 $F417:C9 14 CMP #$14 A:14 X:02 Y:F3 P:25 SP:FB PPU:238,211 CYC:24070 $F419:F0 02 BEQ $F41D A:14 X:02 Y:F3 P:nvUbdIZC SP:FB PPU:244,211 CYC:24072 $F41D:A0 FF LDY #$FF A:14 X:02 Y:F3 P:nvUbdIZC SP:FB PPU:253,211 CYC:24075 $F41F:A9 37 LDA #$37 A:14 X:02 Y:FF P:A5 SP:FB PPU:259,211 CYC:24077 $F421:8D 47 06 STA $0647 = #$14 A:37 X:02 Y:FF P:25 SP:FB PPU:265,211 CYC:24079 $F424:20 40 FB JSR $FB40 A:37 X:02 Y:FF P:25 SP:FB PPU:277,211 CYC:24083 $FB40:24 01 BIT $01 = #$FF A:37 X:02 Y:FF P:25 SP:F9 PPU:295,211 CYC:24089 $FB42:38 SEC A:37 X:02 Y:FF P:E5 SP:F9 PPU:304,211 CYC:24092 $FB43:A9 75 LDA #$75 A:37 X:02 Y:FF P:E5 SP:F9 PPU:310,211 CYC:24094 $FB45:60 RTS A:75 X:02 Y:FF P:65 SP:F9 PPU:316,211 CYC:24096 $F427:53 45 *SRE ($45),Y = #$0548 @ 0647 = 37 A:75 X:02 Y:FF P:65 SP:FB PPU:334,211 CYC:24102 $F429:EA NOP A:6E X:02 Y:FF P:65 SP:FB PPU: 17,212 CYC:24110 $F42A:EA NOP A:6E X:02 Y:FF P:65 SP:FB PPU: 23,212 CYC:24112 $F42B:08 PHP A:6E X:02 Y:FF P:65 SP:FB PPU: 29,212 CYC:24114 $F42C:48 PHA A:6E X:02 Y:FF P:65 SP:FA PPU: 38,212 CYC:24117 $F42D:A0 F4 LDY #$F4 A:6E X:02 Y:FF P:65 SP:F9 PPU: 47,212 CYC:24120 $F42F:68 PLA A:6E X:02 Y:F4 P:E5 SP:F9 PPU: 53,212 CYC:24122 $F430:28 PLP A:6E X:02 Y:F4 P:65 SP:FA PPU: 65,212 CYC:24126 $F431:20 46 FB JSR $FB46 A:6E X:02 Y:F4 P:65 SP:FB PPU: 77,212 CYC:24130 $FB46:50 2D BVC $FB75 A:6E X:02 Y:F4 P:65 SP:F9 PPU: 95,212 CYC:24136 $FB48:F0 2B BEQ $FB75 A:6E X:02 Y:F4 P:65 SP:F9 PPU:101,212 CYC:24138 $FB4A:30 29 BMI $FB75 A:6E X:02 Y:F4 P:65 SP:F9 PPU:107,212 CYC:24140 $FB4C:90 27 BCC $FB75 A:6E X:02 Y:F4 P:65 SP:F9 PPU:113,212 CYC:24142 $FB4E:C9 6E CMP #$6E A:6E X:02 Y:F4 P:65 SP:F9 PPU:119,212 CYC:24144 $FB50:D0 23 BNE $FB75 A:6E X:02 Y:F4 P:67 SP:F9 PPU:125,212 CYC:24146 $FB52:60 RTS A:6E X:02 Y:F4 P:67 SP:F9 PPU:131,212 CYC:24148 $F434:AD 47 06 LDA $0647 = #$1B A:6E X:02 Y:F4 P:67 SP:FB PPU:149,212 CYC:24154 $F437:C9 1B CMP #$1B A:1B X:02 Y:F4 P:65 SP:FB PPU:161,212 CYC:24158 $F439:F0 02 BEQ $F43D A:1B X:02 Y:F4 P:67 SP:FB PPU:167,212 CYC:24160 $F43D:A0 F5 LDY #$F5 A:1B X:02 Y:F4 P:67 SP:FB PPU:176,212 CYC:24163 $F43F:A2 FF LDX #$FF A:1B X:02 Y:F5 P:E5 SP:FB PPU:182,212 CYC:24165 $F441:A9 A5 LDA #$A5 A:1B X:FF Y:F5 P:E5 SP:FB PPU:188,212 CYC:24167 $F443:85 47 STA $47 = #$1B A:A5 X:FF Y:F5 P:E5 SP:FB PPU:194,212 CYC:24169 $F445:20 1D FB JSR $FB1D A:A5 X:FF Y:F5 P:E5 SP:FB PPU:203,212 CYC:24172 $FB1D:24 01 BIT $01 = #$FF A:A5 X:FF Y:F5 P:E5 SP:F9 PPU:221,212 CYC:24178 $FB1F:18 CLC A:A5 X:FF Y:F5 P:E5 SP:F9 PPU:230,212 CYC:24181 $FB20:A9 B3 LDA #$B3 A:A5 X:FF Y:F5 P:NVUbdIzc SP:F9 PPU:236,212 CYC:24183 $FB22:60 RTS A:B3 X:FF Y:F5 P:NVUbdIzc SP:F9 PPU:242,212 CYC:24185 $F448:57 48 *SRE $48,X @ 47 = #$A5 A:B3 X:FF Y:F5 P:NVUbdIzc SP:FB PPU:260,212 CYC:24191 $F44A:EA NOP A:E1 X:FF Y:F5 P:E5 SP:FB PPU:278,212 CYC:24197 $F44B:EA NOP A:E1 X:FF Y:F5 P:E5 SP:FB PPU:284,212 CYC:24199 $F44C:EA NOP A:E1 X:FF Y:F5 P:E5 SP:FB PPU:290,212 CYC:24201 $F44D:EA NOP A:E1 X:FF Y:F5 P:E5 SP:FB PPU:296,212 CYC:24203 $F44E:20 23 FB JSR $FB23 A:E1 X:FF Y:F5 P:E5 SP:FB PPU:302,212 CYC:24205 $FB23:50 50 BVC $FB75 A:E1 X:FF Y:F5 P:E5 SP:F9 PPU:320,212 CYC:24211 $FB25:90 4E BCC $FB75 A:E1 X:FF Y:F5 P:E5 SP:F9 PPU:326,212 CYC:24213 $FB27:10 4C BPL $FB75 A:E1 X:FF Y:F5 P:E5 SP:F9 PPU:332,212 CYC:24215 $FB29:C9 E1 CMP #$E1 A:E1 X:FF Y:F5 P:E5 SP:F9 PPU:338,212 CYC:24217 $FB2B:D0 48 BNE $FB75 A:E1 X:FF Y:F5 P:67 SP:F9 PPU: 3,213 CYC:24219 $FB2D:60 RTS A:E1 X:FF Y:F5 P:67 SP:F9 PPU: 9,213 CYC:24221 $F451:A5 47 LDA $47 = #$52 A:E1 X:FF Y:F5 P:67 SP:FB PPU: 27,213 CYC:24227 $F453:C9 52 CMP #$52 A:52 X:FF Y:F5 P:65 SP:FB PPU: 36,213 CYC:24230 $F455:F0 02 BEQ $F459 A:52 X:FF Y:F5 P:67 SP:FB PPU: 42,213 CYC:24232 $F459:C8 INY A:52 X:FF Y:F5 P:67 SP:FB PPU: 51,213 CYC:24235 $F45A:A9 29 LDA #$29 A:52 X:FF Y:F6 P:E5 SP:FB PPU: 57,213 CYC:24237 $F45C:85 47 STA $47 = #$52 A:29 X:FF Y:F6 P:65 SP:FB PPU: 63,213 CYC:24239 $F45E:20 2E FB JSR $FB2E A:29 X:FF Y:F6 P:65 SP:FB PPU: 72,213 CYC:24242 $FB2E:B8 CLV A:29 X:FF Y:F6 P:65 SP:F9 PPU: 90,213 CYC:24248 $FB2F:18 CLC A:29 X:FF Y:F6 P:25 SP:F9 PPU: 96,213 CYC:24250 $FB30:A9 42 LDA #$42 A:29 X:FF Y:F6 P:nvUbdIzc SP:F9 PPU:102,213 CYC:24252 $FB32:60 RTS A:42 X:FF Y:F6 P:nvUbdIzc SP:F9 PPU:108,213 CYC:24254 $F461:57 48 *SRE $48,X @ 47 = #$29 A:42 X:FF Y:F6 P:nvUbdIzc SP:FB PPU:126,213 CYC:24260 $F463:EA NOP A:56 X:FF Y:F6 P:25 SP:FB PPU:144,213 CYC:24266 $F464:EA NOP A:56 X:FF Y:F6 P:25 SP:FB PPU:150,213 CYC:24268 $F465:EA NOP A:56 X:FF Y:F6 P:25 SP:FB PPU:156,213 CYC:24270 $F466:EA NOP A:56 X:FF Y:F6 P:25 SP:FB PPU:162,213 CYC:24272 $F467:20 33 FB JSR $FB33 A:56 X:FF Y:F6 P:25 SP:FB PPU:168,213 CYC:24274 $FB33:70 40 BVS $FB75 A:56 X:FF Y:F6 P:25 SP:F9 PPU:186,213 CYC:24280 $FB35:F0 3E BEQ $FB75 A:56 X:FF Y:F6 P:25 SP:F9 PPU:192,213 CYC:24282 $FB37:30 3C BMI $FB75 A:56 X:FF Y:F6 P:25 SP:F9 PPU:198,213 CYC:24284 $FB39:90 3A BCC $FB75 A:56 X:FF Y:F6 P:25 SP:F9 PPU:204,213 CYC:24286 $FB3B:C9 56 CMP #$56 A:56 X:FF Y:F6 P:25 SP:F9 PPU:210,213 CYC:24288 $FB3D:D0 36 BNE $FB75 A:56 X:FF Y:F6 P:nvUbdIZC SP:F9 PPU:216,213 CYC:24290 $FB3F:60 RTS A:56 X:FF Y:F6 P:nvUbdIZC SP:F9 PPU:222,213 CYC:24292 $F46A:A5 47 LDA $47 = #$14 A:56 X:FF Y:F6 P:nvUbdIZC SP:FB PPU:240,213 CYC:24298 $F46C:C9 14 CMP #$14 A:14 X:FF Y:F6 P:25 SP:FB PPU:249,213 CYC:24301 $F46E:F0 02 BEQ $F472 A:14 X:FF Y:F6 P:nvUbdIZC SP:FB PPU:255,213 CYC:24303 $F472:C8 INY A:14 X:FF Y:F6 P:nvUbdIZC SP:FB PPU:264,213 CYC:24306 $F473:A9 37 LDA #$37 A:14 X:FF Y:F7 P:A5 SP:FB PPU:270,213 CYC:24308 $F475:85 47 STA $47 = #$14 A:37 X:FF Y:F7 P:25 SP:FB PPU:276,213 CYC:24310 $F477:20 40 FB JSR $FB40 A:37 X:FF Y:F7 P:25 SP:FB PPU:285,213 CYC:24313 $FB40:24 01 BIT $01 = #$FF A:37 X:FF Y:F7 P:25 SP:F9 PPU:303,213 CYC:24319 $FB42:38 SEC A:37 X:FF Y:F7 P:E5 SP:F9 PPU:312,213 CYC:24322 $FB43:A9 75 LDA #$75 A:37 X:FF Y:F7 P:E5 SP:F9 PPU:318,213 CYC:24324 $FB45:60 RTS A:75 X:FF Y:F7 P:65 SP:F9 PPU:324,213 CYC:24326 $F47A:57 48 *SRE $48,X @ 47 = #$37 A:75 X:FF Y:F7 P:65 SP:FB PPU: 1,214 CYC:24332 $F47C:EA NOP A:6E X:FF Y:F7 P:65 SP:FB PPU: 19,214 CYC:24338 $F47D:EA NOP A:6E X:FF Y:F7 P:65 SP:FB PPU: 25,214 CYC:24340 $F47E:EA NOP A:6E X:FF Y:F7 P:65 SP:FB PPU: 31,214 CYC:24342 $F47F:EA NOP A:6E X:FF Y:F7 P:65 SP:FB PPU: 37,214 CYC:24344 $F480:20 46 FB JSR $FB46 A:6E X:FF Y:F7 P:65 SP:FB PPU: 43,214 CYC:24346 $FB46:50 2D BVC $FB75 A:6E X:FF Y:F7 P:65 SP:F9 PPU: 61,214 CYC:24352 $FB48:F0 2B BEQ $FB75 A:6E X:FF Y:F7 P:65 SP:F9 PPU: 67,214 CYC:24354 $FB4A:30 29 BMI $FB75 A:6E X:FF Y:F7 P:65 SP:F9 PPU: 73,214 CYC:24356 $FB4C:90 27 BCC $FB75 A:6E X:FF Y:F7 P:65 SP:F9 PPU: 79,214 CYC:24358 $FB4E:C9 6E CMP #$6E A:6E X:FF Y:F7 P:65 SP:F9 PPU: 85,214 CYC:24360 $FB50:D0 23 BNE $FB75 A:6E X:FF Y:F7 P:67 SP:F9 PPU: 91,214 CYC:24362 $FB52:60 RTS A:6E X:FF Y:F7 P:67 SP:F9 PPU: 97,214 CYC:24364 $F483:A5 47 LDA $47 = #$1B A:6E X:FF Y:F7 P:67 SP:FB PPU:115,214 CYC:24370 $F485:C9 1B CMP #$1B A:1B X:FF Y:F7 P:65 SP:FB PPU:124,214 CYC:24373 $F487:F0 02 BEQ $F48B A:1B X:FF Y:F7 P:67 SP:FB PPU:130,214 CYC:24375 $F48B:A9 A5 LDA #$A5 A:1B X:FF Y:F7 P:67 SP:FB PPU:139,214 CYC:24378 $F48D:8D 47 06 STA $0647 = #$1B A:A5 X:FF Y:F7 P:E5 SP:FB PPU:145,214 CYC:24380 $F490:A0 FF LDY #$FF A:A5 X:FF Y:F7 P:E5 SP:FB PPU:157,214 CYC:24384 $F492:20 1D FB JSR $FB1D A:A5 X:FF Y:FF P:E5 SP:FB PPU:163,214 CYC:24386 $FB1D:24 01 BIT $01 = #$FF A:A5 X:FF Y:FF P:E5 SP:F9 PPU:181,214 CYC:24392 $FB1F:18 CLC A:A5 X:FF Y:FF P:E5 SP:F9 PPU:190,214 CYC:24395 $FB20:A9 B3 LDA #$B3 A:A5 X:FF Y:FF P:NVUbdIzc SP:F9 PPU:196,214 CYC:24397 $FB22:60 RTS A:B3 X:FF Y:FF P:NVUbdIzc SP:F9 PPU:202,214 CYC:24399 $F495:5B 48 05 *SRE $0548,Y @ 0647 = #$A5 A:B3 X:FF Y:FF P:NVUbdIzc SP:FB PPU:220,214 CYC:24405 $F498:EA NOP A:E1 X:FF Y:FF P:E5 SP:FB PPU:241,214 CYC:24412 $F499:EA NOP A:E1 X:FF Y:FF P:E5 SP:FB PPU:247,214 CYC:24414 $F49A:08 PHP A:E1 X:FF Y:FF P:E5 SP:FB PPU:253,214 CYC:24416 $F49B:48 PHA A:E1 X:FF Y:FF P:E5 SP:FA PPU:262,214 CYC:24419 $F49C:A0 F8 LDY #$F8 A:E1 X:FF Y:FF P:E5 SP:F9 PPU:271,214 CYC:24422 $F49E:68 PLA A:E1 X:FF Y:F8 P:E5 SP:F9 PPU:277,214 CYC:24424 $F49F:28 PLP A:E1 X:FF Y:F8 P:E5 SP:FA PPU:289,214 CYC:24428 $F4A0:20 23 FB JSR $FB23 A:E1 X:FF Y:F8 P:E5 SP:FB PPU:301,214 CYC:24432 $FB23:50 50 BVC $FB75 A:E1 X:FF Y:F8 P:E5 SP:F9 PPU:319,214 CYC:24438 $FB25:90 4E BCC $FB75 A:E1 X:FF Y:F8 P:E5 SP:F9 PPU:325,214 CYC:24440 $FB27:10 4C BPL $FB75 A:E1 X:FF Y:F8 P:E5 SP:F9 PPU:331,214 CYC:24442 $FB29:C9 E1 CMP #$E1 A:E1 X:FF Y:F8 P:E5 SP:F9 PPU:337,214 CYC:24444 $FB2B:D0 48 BNE $FB75 A:E1 X:FF Y:F8 P:67 SP:F9 PPU: 2,215 CYC:24446 $FB2D:60 RTS A:E1 X:FF Y:F8 P:67 SP:F9 PPU: 8,215 CYC:24448 $F4A3:AD 47 06 LDA $0647 = #$52 A:E1 X:FF Y:F8 P:67 SP:FB PPU: 26,215 CYC:24454 $F4A6:C9 52 CMP #$52 A:52 X:FF Y:F8 P:65 SP:FB PPU: 38,215 CYC:24458 $F4A8:F0 02 BEQ $F4AC A:52 X:FF Y:F8 P:67 SP:FB PPU: 44,215 CYC:24460 $F4AC:A0 FF LDY #$FF A:52 X:FF Y:F8 P:67 SP:FB PPU: 53,215 CYC:24463 $F4AE:A9 29 LDA #$29 A:52 X:FF Y:FF P:E5 SP:FB PPU: 59,215 CYC:24465 $F4B0:8D 47 06 STA $0647 = #$52 A:29 X:FF Y:FF P:65 SP:FB PPU: 65,215 CYC:24467 $F4B3:20 2E FB JSR $FB2E A:29 X:FF Y:FF P:65 SP:FB PPU: 77,215 CYC:24471 $FB2E:B8 CLV A:29 X:FF Y:FF P:65 SP:F9 PPU: 95,215 CYC:24477 $FB2F:18 CLC A:29 X:FF Y:FF P:25 SP:F9 PPU:101,215 CYC:24479 $FB30:A9 42 LDA #$42 A:29 X:FF Y:FF P:nvUbdIzc SP:F9 PPU:107,215 CYC:24481 $FB32:60 RTS A:42 X:FF Y:FF P:nvUbdIzc SP:F9 PPU:113,215 CYC:24483 $F4B6:5B 48 05 *SRE $0548,Y @ 0647 = #$29 A:42 X:FF Y:FF P:nvUbdIzc SP:FB PPU:131,215 CYC:24489 $F4B9:EA NOP A:56 X:FF Y:FF P:25 SP:FB PPU:152,215 CYC:24496 $F4BA:EA NOP A:56 X:FF Y:FF P:25 SP:FB PPU:158,215 CYC:24498 $F4BB:08 PHP A:56 X:FF Y:FF P:25 SP:FB PPU:164,215 CYC:24500 $F4BC:48 PHA A:56 X:FF Y:FF P:25 SP:FA PPU:173,215 CYC:24503 $F4BD:A0 F9 LDY #$F9 A:56 X:FF Y:FF P:25 SP:F9 PPU:182,215 CYC:24506 $F4BF:68 PLA A:56 X:FF Y:F9 P:A5 SP:F9 PPU:188,215 CYC:24508 $F4C0:28 PLP A:56 X:FF Y:F9 P:25 SP:FA PPU:200,215 CYC:24512 $F4C1:20 33 FB JSR $FB33 A:56 X:FF Y:F9 P:25 SP:FB PPU:212,215 CYC:24516 $FB33:70 40 BVS $FB75 A:56 X:FF Y:F9 P:25 SP:F9 PPU:230,215 CYC:24522 $FB35:F0 3E BEQ $FB75 A:56 X:FF Y:F9 P:25 SP:F9 PPU:236,215 CYC:24524 $FB37:30 3C BMI $FB75 A:56 X:FF Y:F9 P:25 SP:F9 PPU:242,215 CYC:24526 $FB39:90 3A BCC $FB75 A:56 X:FF Y:F9 P:25 SP:F9 PPU:248,215 CYC:24528 $FB3B:C9 56 CMP #$56 A:56 X:FF Y:F9 P:25 SP:F9 PPU:254,215 CYC:24530 $FB3D:D0 36 BNE $FB75 A:56 X:FF Y:F9 P:nvUbdIZC SP:F9 PPU:260,215 CYC:24532 $FB3F:60 RTS A:56 X:FF Y:F9 P:nvUbdIZC SP:F9 PPU:266,215 CYC:24534 $F4C4:AD 47 06 LDA $0647 = #$14 A:56 X:FF Y:F9 P:nvUbdIZC SP:FB PPU:284,215 CYC:24540 $F4C7:C9 14 CMP #$14 A:14 X:FF Y:F9 P:25 SP:FB PPU:296,215 CYC:24544 $F4C9:F0 02 BEQ $F4CD A:14 X:FF Y:F9 P:nvUbdIZC SP:FB PPU:302,215 CYC:24546 $F4CD:A0 FF LDY #$FF A:14 X:FF Y:F9 P:nvUbdIZC SP:FB PPU:311,215 CYC:24549 $F4CF:A9 37 LDA #$37 A:14 X:FF Y:FF P:A5 SP:FB PPU:317,215 CYC:24551 $F4D1:8D 47 06 STA $0647 = #$14 A:37 X:FF Y:FF P:25 SP:FB PPU:323,215 CYC:24553 $F4D4:20 40 FB JSR $FB40 A:37 X:FF Y:FF P:25 SP:FB PPU:335,215 CYC:24557 $FB40:24 01 BIT $01 = #$FF A:37 X:FF Y:FF P:25 SP:F9 PPU: 12,216 CYC:24563 $FB42:38 SEC A:37 X:FF Y:FF P:E5 SP:F9 PPU: 21,216 CYC:24566 $FB43:A9 75 LDA #$75 A:37 X:FF Y:FF P:E5 SP:F9 PPU: 27,216 CYC:24568 $FB45:60 RTS A:75 X:FF Y:FF P:65 SP:F9 PPU: 33,216 CYC:24570 $F4D7:5B 48 05 *SRE $0548,Y @ 0647 = #$37 A:75 X:FF Y:FF P:65 SP:FB PPU: 51,216 CYC:24576 $F4DA:EA NOP A:6E X:FF Y:FF P:65 SP:FB PPU: 72,216 CYC:24583 $F4DB:EA NOP A:6E X:FF Y:FF P:65 SP:FB PPU: 78,216 CYC:24585 $F4DC:08 PHP A:6E X:FF Y:FF P:65 SP:FB PPU: 84,216 CYC:24587 $F4DD:48 PHA A:6E X:FF Y:FF P:65 SP:FA PPU: 93,216 CYC:24590 $F4DE:A0 FA LDY #$FA A:6E X:FF Y:FF P:65 SP:F9 PPU:102,216 CYC:24593 $F4E0:68 PLA A:6E X:FF Y:FA P:E5 SP:F9 PPU:108,216 CYC:24595 $F4E1:28 PLP A:6E X:FF Y:FA P:65 SP:FA PPU:120,216 CYC:24599 $F4E2:20 46 FB JSR $FB46 A:6E X:FF Y:FA P:65 SP:FB PPU:132,216 CYC:24603 $FB46:50 2D BVC $FB75 A:6E X:FF Y:FA P:65 SP:F9 PPU:150,216 CYC:24609 $FB48:F0 2B BEQ $FB75 A:6E X:FF Y:FA P:65 SP:F9 PPU:156,216 CYC:24611 $FB4A:30 29 BMI $FB75 A:6E X:FF Y:FA P:65 SP:F9 PPU:162,216 CYC:24613 $FB4C:90 27 BCC $FB75 A:6E X:FF Y:FA P:65 SP:F9 PPU:168,216 CYC:24615 $FB4E:C9 6E CMP #$6E A:6E X:FF Y:FA P:65 SP:F9 PPU:174,216 CYC:24617 $FB50:D0 23 BNE $FB75 A:6E X:FF Y:FA P:67 SP:F9 PPU:180,216 CYC:24619 $FB52:60 RTS A:6E X:FF Y:FA P:67 SP:F9 PPU:186,216 CYC:24621 $F4E5:AD 47 06 LDA $0647 = #$1B A:6E X:FF Y:FA P:67 SP:FB PPU:204,216 CYC:24627 $F4E8:C9 1B CMP #$1B A:1B X:FF Y:FA P:65 SP:FB PPU:216,216 CYC:24631 $F4EA:F0 02 BEQ $F4EE A:1B X:FF Y:FA P:67 SP:FB PPU:222,216 CYC:24633 $F4EE:A0 FB LDY #$FB A:1B X:FF Y:FA P:67 SP:FB PPU:231,216 CYC:24636 $F4F0:A2 FF LDX #$FF A:1B X:FF Y:FB P:E5 SP:FB PPU:237,216 CYC:24638 $F4F2:A9 A5 LDA #$A5 A:1B X:FF Y:FB P:E5 SP:FB PPU:243,216 CYC:24640 $F4F4:8D 47 06 STA $0647 = #$1B A:A5 X:FF Y:FB P:E5 SP:FB PPU:249,216 CYC:24642 $F4F7:20 1D FB JSR $FB1D A:A5 X:FF Y:FB P:E5 SP:FB PPU:261,216 CYC:24646 $FB1D:24 01 BIT $01 = #$FF A:A5 X:FF Y:FB P:E5 SP:F9 PPU:279,216 CYC:24652 $FB1F:18 CLC A:A5 X:FF Y:FB P:E5 SP:F9 PPU:288,216 CYC:24655 $FB20:A9 B3 LDA #$B3 A:A5 X:FF Y:FB P:NVUbdIzc SP:F9 PPU:294,216 CYC:24657 $FB22:60 RTS A:B3 X:FF Y:FB P:NVUbdIzc SP:F9 PPU:300,216 CYC:24659 $F4FA:5F 48 05 *SRE $0548,X @ 0647 = #$A5 A:B3 X:FF Y:FB P:NVUbdIzc SP:FB PPU:318,216 CYC:24665 $F4FD:EA NOP A:E1 X:FF Y:FB P:E5 SP:FB PPU:339,216 CYC:24672 $F4FE:EA NOP A:E1 X:FF Y:FB P:E5 SP:FB PPU: 4,217 CYC:24674 $F4FF:EA NOP A:E1 X:FF Y:FB P:E5 SP:FB PPU: 10,217 CYC:24676 $F500:EA NOP A:E1 X:FF Y:FB P:E5 SP:FB PPU: 16,217 CYC:24678 $F501:20 23 FB JSR $FB23 A:E1 X:FF Y:FB P:E5 SP:FB PPU: 22,217 CYC:24680 $FB23:50 50 BVC $FB75 A:E1 X:FF Y:FB P:E5 SP:F9 PPU: 40,217 CYC:24686 $FB25:90 4E BCC $FB75 A:E1 X:FF Y:FB P:E5 SP:F9 PPU: 46,217 CYC:24688 $FB27:10 4C BPL $FB75 A:E1 X:FF Y:FB P:E5 SP:F9 PPU: 52,217 CYC:24690 $FB29:C9 E1 CMP #$E1 A:E1 X:FF Y:FB P:E5 SP:F9 PPU: 58,217 CYC:24692 $FB2B:D0 48 BNE $FB75 A:E1 X:FF Y:FB P:67 SP:F9 PPU: 64,217 CYC:24694 $FB2D:60 RTS A:E1 X:FF Y:FB P:67 SP:F9 PPU: 70,217 CYC:24696 $F504:AD 47 06 LDA $0647 = #$52 A:E1 X:FF Y:FB P:67 SP:FB PPU: 88,217 CYC:24702 $F507:C9 52 CMP #$52 A:52 X:FF Y:FB P:65 SP:FB PPU:100,217 CYC:24706 $F509:F0 02 BEQ $F50D A:52 X:FF Y:FB P:67 SP:FB PPU:106,217 CYC:24708 $F50D:C8 INY A:52 X:FF Y:FB P:67 SP:FB PPU:115,217 CYC:24711 $F50E:A9 29 LDA #$29 A:52 X:FF Y:FC P:E5 SP:FB PPU:121,217 CYC:24713 $F510:8D 47 06 STA $0647 = #$52 A:29 X:FF Y:FC P:65 SP:FB PPU:127,217 CYC:24715 $F513:20 2E FB JSR $FB2E A:29 X:FF Y:FC P:65 SP:FB PPU:139,217 CYC:24719 $FB2E:B8 CLV A:29 X:FF Y:FC P:65 SP:F9 PPU:157,217 CYC:24725 $FB2F:18 CLC A:29 X:FF Y:FC P:25 SP:F9 PPU:163,217 CYC:24727 $FB30:A9 42 LDA #$42 A:29 X:FF Y:FC P:nvUbdIzc SP:F9 PPU:169,217 CYC:24729 $FB32:60 RTS A:42 X:FF Y:FC P:nvUbdIzc SP:F9 PPU:175,217 CYC:24731 $F516:5F 48 05 *SRE $0548,X @ 0647 = #$29 A:42 X:FF Y:FC P:nvUbdIzc SP:FB PPU:193,217 CYC:24737 $F519:EA NOP A:56 X:FF Y:FC P:25 SP:FB PPU:214,217 CYC:24744 $F51A:EA NOP A:56 X:FF Y:FC P:25 SP:FB PPU:220,217 CYC:24746 $F51B:EA NOP A:56 X:FF Y:FC P:25 SP:FB PPU:226,217 CYC:24748 $F51C:EA NOP A:56 X:FF Y:FC P:25 SP:FB PPU:232,217 CYC:24750 $F51D:20 33 FB JSR $FB33 A:56 X:FF Y:FC P:25 SP:FB PPU:238,217 CYC:24752 $FB33:70 40 BVS $FB75 A:56 X:FF Y:FC P:25 SP:F9 PPU:256,217 CYC:24758 $FB35:F0 3E BEQ $FB75 A:56 X:FF Y:FC P:25 SP:F9 PPU:262,217 CYC:24760 $FB37:30 3C BMI $FB75 A:56 X:FF Y:FC P:25 SP:F9 PPU:268,217 CYC:24762 $FB39:90 3A BCC $FB75 A:56 X:FF Y:FC P:25 SP:F9 PPU:274,217 CYC:24764 $FB3B:C9 56 CMP #$56 A:56 X:FF Y:FC P:25 SP:F9 PPU:280,217 CYC:24766 $FB3D:D0 36 BNE $FB75 A:56 X:FF Y:FC P:nvUbdIZC SP:F9 PPU:286,217 CYC:24768 $FB3F:60 RTS A:56 X:FF Y:FC P:nvUbdIZC SP:F9 PPU:292,217 CYC:24770 $F520:AD 47 06 LDA $0647 = #$14 A:56 X:FF Y:FC P:nvUbdIZC SP:FB PPU:310,217 CYC:24776 $F523:C9 14 CMP #$14 A:14 X:FF Y:FC P:25 SP:FB PPU:322,217 CYC:24780 $F525:F0 02 BEQ $F529 A:14 X:FF Y:FC P:nvUbdIZC SP:FB PPU:328,217 CYC:24782 $F529:C8 INY A:14 X:FF Y:FC P:nvUbdIZC SP:FB PPU:337,217 CYC:24785 $F52A:A9 37 LDA #$37 A:14 X:FF Y:FD P:A5 SP:FB PPU: 2,218 CYC:24787 $F52C:8D 47 06 STA $0647 = #$14 A:37 X:FF Y:FD P:25 SP:FB PPU: 8,218 CYC:24789 $F52F:20 40 FB JSR $FB40 A:37 X:FF Y:FD P:25 SP:FB PPU: 20,218 CYC:24793 $FB40:24 01 BIT $01 = #$FF A:37 X:FF Y:FD P:25 SP:F9 PPU: 38,218 CYC:24799 $FB42:38 SEC A:37 X:FF Y:FD P:E5 SP:F9 PPU: 47,218 CYC:24802 $FB43:A9 75 LDA #$75 A:37 X:FF Y:FD P:E5 SP:F9 PPU: 53,218 CYC:24804 $FB45:60 RTS A:75 X:FF Y:FD P:65 SP:F9 PPU: 59,218 CYC:24806 $F532:5F 48 05 *SRE $0548,X @ 0647 = #$37 A:75 X:FF Y:FD P:65 SP:FB PPU: 77,218 CYC:24812 $F535:EA NOP A:6E X:FF Y:FD P:65 SP:FB PPU: 98,218 CYC:24819 $F536:EA NOP A:6E X:FF Y:FD P:65 SP:FB PPU:104,218 CYC:24821 $F537:EA NOP A:6E X:FF Y:FD P:65 SP:FB PPU:110,218 CYC:24823 $F538:EA NOP A:6E X:FF Y:FD P:65 SP:FB PPU:116,218 CYC:24825 $F539:20 46 FB JSR $FB46 A:6E X:FF Y:FD P:65 SP:FB PPU:122,218 CYC:24827 $FB46:50 2D BVC $FB75 A:6E X:FF Y:FD P:65 SP:F9 PPU:140,218 CYC:24833 $FB48:F0 2B BEQ $FB75 A:6E X:FF Y:FD P:65 SP:F9 PPU:146,218 CYC:24835 $FB4A:30 29 BMI $FB75 A:6E X:FF Y:FD P:65 SP:F9 PPU:152,218 CYC:24837 $FB4C:90 27 BCC $FB75 A:6E X:FF Y:FD P:65 SP:F9 PPU:158,218 CYC:24839 $FB4E:C9 6E CMP #$6E A:6E X:FF Y:FD P:65 SP:F9 PPU:164,218 CYC:24841 $FB50:D0 23 BNE $FB75 A:6E X:FF Y:FD P:67 SP:F9 PPU:170,218 CYC:24843 $FB52:60 RTS A:6E X:FF Y:FD P:67 SP:F9 PPU:176,218 CYC:24845 $F53C:AD 47 06 LDA $0647 = #$1B A:6E X:FF Y:FD P:67 SP:FB PPU:194,218 CYC:24851 $F53F:C9 1B CMP #$1B A:1B X:FF Y:FD P:65 SP:FB PPU:206,218 CYC:24855 $F541:F0 02 BEQ $F545 A:1B X:FF Y:FD P:67 SP:FB PPU:212,218 CYC:24857 $F545:60 RTS A:1B X:FF Y:FD P:67 SP:FB PPU:221,218 CYC:24860 $C64A:A5 00 LDA $00 = #$00 A:1B X:FF Y:FD P:67 SP:FD PPU:239,218 CYC:24866 $C64C:85 11 STA $11 = #$00 A:00 X:FF Y:FD P:67 SP:FD PPU:248,218 CYC:24869 $C64E:A9 00 LDA #$00 A:00 X:FF Y:FD P:67 SP:FD PPU:257,218 CYC:24872 $C650:85 00 STA $00 = #$00 A:00 X:FF Y:FD P:67 SP:FD PPU:263,218 CYC:24874 $C652:20 46 F5 JSR $F546 A:00 X:FF Y:FD P:67 SP:FD PPU:272,218 CYC:24877 $F546:A9 FF LDA #$FF A:00 X:FF Y:FD P:67 SP:FB PPU:290,218 CYC:24883 $F548:85 01 STA $01 = #$FF A:FF X:FF Y:FD P:E5 SP:FB PPU:296,218 CYC:24885 $F54A:A0 01 LDY #$01 A:FF X:FF Y:FD P:E5 SP:FB PPU:305,218 CYC:24888 $F54C:A2 02 LDX #$02 A:FF X:FF Y:01 P:65 SP:FB PPU:311,218 CYC:24890 $F54E:A9 47 LDA #$47 A:FF X:02 Y:01 P:65 SP:FB PPU:317,218 CYC:24892 $F550:85 47 STA $47 = #$1B A:47 X:02 Y:01 P:65 SP:FB PPU:323,218 CYC:24894 $F552:A9 06 LDA #$06 A:47 X:02 Y:01 P:65 SP:FB PPU:332,218 CYC:24897 $F554:85 48 STA $48 = #$06 A:06 X:02 Y:01 P:65 SP:FB PPU:338,218 CYC:24899 $F556:A9 A5 LDA #$A5 A:06 X:02 Y:01 P:65 SP:FB PPU: 6,219 CYC:24902 $F558:8D 47 06 STA $0647 = #$1B A:A5 X:02 Y:01 P:E5 SP:FB PPU: 12,219 CYC:24904 $F55B:20 E9 FA JSR $FAE9 A:A5 X:02 Y:01 P:E5 SP:FB PPU: 24,219 CYC:24908 $FAE9:24 01 BIT $01 = #$FF A:A5 X:02 Y:01 P:E5 SP:F9 PPU: 42,219 CYC:24914 $FAEB:18 CLC A:A5 X:02 Y:01 P:E5 SP:F9 PPU: 51,219 CYC:24917 $FAEC:A9 B2 LDA #$B2 A:A5 X:02 Y:01 P:NVUbdIzc SP:F9 PPU: 57,219 CYC:24919 $FAEE:60 RTS A:B2 X:02 Y:01 P:NVUbdIzc SP:F9 PPU: 63,219 CYC:24921 $F55E:63 45 *RRA ($45,X) @ 47 = #$0647 = A5 A:B2 X:02 Y:01 P:NVUbdIzc SP:FB PPU: 81,219 CYC:24927 $F560:EA NOP A:05 X:02 Y:01 P:25 SP:FB PPU:105,219 CYC:24935 $F561:EA NOP A:05 X:02 Y:01 P:25 SP:FB PPU:111,219 CYC:24937 $F562:EA NOP A:05 X:02 Y:01 P:25 SP:FB PPU:117,219 CYC:24939 $F563:EA NOP A:05 X:02 Y:01 P:25 SP:FB PPU:123,219 CYC:24941 $F564:20 EF FA JSR $FAEF A:05 X:02 Y:01 P:25 SP:FB PPU:129,219 CYC:24943 $FAEF:70 2A BVS $FB1B A:05 X:02 Y:01 P:25 SP:F9 PPU:147,219 CYC:24949 $FAF1:90 28 BCC $FB1B A:05 X:02 Y:01 P:25 SP:F9 PPU:153,219 CYC:24951 $FAF3:30 26 BMI $FB1B A:05 X:02 Y:01 P:25 SP:F9 PPU:159,219 CYC:24953 $FAF5:C9 05 CMP #$05 A:05 X:02 Y:01 P:25 SP:F9 PPU:165,219 CYC:24955 $FAF7:D0 22 BNE $FB1B A:05 X:02 Y:01 P:nvUbdIZC SP:F9 PPU:171,219 CYC:24957 $FAF9:60 RTS A:05 X:02 Y:01 P:nvUbdIZC SP:F9 PPU:177,219 CYC:24959 $F567:AD 47 06 LDA $0647 = #$52 A:05 X:02 Y:01 P:nvUbdIZC SP:FB PPU:195,219 CYC:24965 $F56A:C9 52 CMP #$52 A:52 X:02 Y:01 P:25 SP:FB PPU:207,219 CYC:24969 $F56C:F0 02 BEQ $F570 A:52 X:02 Y:01 P:nvUbdIZC SP:FB PPU:213,219 CYC:24971 $F570:C8 INY A:52 X:02 Y:01 P:nvUbdIZC SP:FB PPU:222,219 CYC:24974 $F571:A9 29 LDA #$29 A:52 X:02 Y:02 P:25 SP:FB PPU:228,219 CYC:24976 $F573:8D 47 06 STA $0647 = #$52 A:29 X:02 Y:02 P:25 SP:FB PPU:234,219 CYC:24978 $F576:20 FA FA JSR $FAFA A:29 X:02 Y:02 P:25 SP:FB PPU:246,219 CYC:24982 $FAFA:B8 CLV A:29 X:02 Y:02 P:25 SP:F9 PPU:264,219 CYC:24988 $FAFB:18 CLC A:29 X:02 Y:02 P:25 SP:F9 PPU:270,219 CYC:24990 $FAFC:A9 42 LDA #$42 A:29 X:02 Y:02 P:nvUbdIzc SP:F9 PPU:276,219 CYC:24992 $FAFE:60 RTS A:42 X:02 Y:02 P:nvUbdIzc SP:F9 PPU:282,219 CYC:24994 $F579:63 45 *RRA ($45,X) @ 47 = #$0647 = 29 A:42 X:02 Y:02 P:nvUbdIzc SP:FB PPU:300,219 CYC:25000 $F57B:EA NOP A:57 X:02 Y:02 P:nvUbdIzc SP:FB PPU:324,219 CYC:25008 $F57C:EA NOP A:57 X:02 Y:02 P:nvUbdIzc SP:FB PPU:330,219 CYC:25010 $F57D:EA NOP A:57 X:02 Y:02 P:nvUbdIzc SP:FB PPU:336,219 CYC:25012 $F57E:EA NOP A:57 X:02 Y:02 P:nvUbdIzc SP:FB PPU: 1,220 CYC:25014 $F57F:20 FF FA JSR $FAFF A:57 X:02 Y:02 P:nvUbdIzc SP:FB PPU: 7,220 CYC:25016 $FAFF:70 1A BVS $FB1B A:57 X:02 Y:02 P:nvUbdIzc SP:F9 PPU: 25,220 CYC:25022 $FB01:30 18 BMI $FB1B A:57 X:02 Y:02 P:nvUbdIzc SP:F9 PPU: 31,220 CYC:25024 $FB03:B0 16 BCS $FB1B A:57 X:02 Y:02 P:nvUbdIzc SP:F9 PPU: 37,220 CYC:25026 $FB05:C9 57 CMP #$57 A:57 X:02 Y:02 P:nvUbdIzc SP:F9 PPU: 43,220 CYC:25028 $FB07:D0 12 BNE $FB1B A:57 X:02 Y:02 P:nvUbdIZC SP:F9 PPU: 49,220 CYC:25030 $FB09:60 RTS A:57 X:02 Y:02 P:nvUbdIZC SP:F9 PPU: 55,220 CYC:25032 $F582:AD 47 06 LDA $0647 = #$14 A:57 X:02 Y:02 P:nvUbdIZC SP:FB PPU: 73,220 CYC:25038 $F585:C9 14 CMP #$14 A:14 X:02 Y:02 P:25 SP:FB PPU: 85,220 CYC:25042 $F587:F0 02 BEQ $F58B A:14 X:02 Y:02 P:nvUbdIZC SP:FB PPU: 91,220 CYC:25044 $F58B:C8 INY A:14 X:02 Y:02 P:nvUbdIZC SP:FB PPU:100,220 CYC:25047 $F58C:A9 37 LDA #$37 A:14 X:02 Y:03 P:25 SP:FB PPU:106,220 CYC:25049 $F58E:8D 47 06 STA $0647 = #$14 A:37 X:02 Y:03 P:25 SP:FB PPU:112,220 CYC:25051 $F591:20 0A FB JSR $FB0A A:37 X:02 Y:03 P:25 SP:FB PPU:124,220 CYC:25055 $FB0A:24 01 BIT $01 = #$FF A:37 X:02 Y:03 P:25 SP:F9 PPU:142,220 CYC:25061 $FB0C:38 SEC A:37 X:02 Y:03 P:E5 SP:F9 PPU:151,220 CYC:25064 $FB0D:A9 75 LDA #$75 A:37 X:02 Y:03 P:E5 SP:F9 PPU:157,220 CYC:25066 $FB0F:60 RTS A:75 X:02 Y:03 P:65 SP:F9 PPU:163,220 CYC:25068 $F594:63 45 *RRA ($45,X) @ 47 = #$0647 = 37 A:75 X:02 Y:03 P:65 SP:FB PPU:181,220 CYC:25074 $F596:EA NOP A:11 X:02 Y:03 P:25 SP:FB PPU:205,220 CYC:25082 $F597:EA NOP A:11 X:02 Y:03 P:25 SP:FB PPU:211,220 CYC:25084 $F598:EA NOP A:11 X:02 Y:03 P:25 SP:FB PPU:217,220 CYC:25086 $F599:EA NOP A:11 X:02 Y:03 P:25 SP:FB PPU:223,220 CYC:25088 $F59A:20 10 FB JSR $FB10 A:11 X:02 Y:03 P:25 SP:FB PPU:229,220 CYC:25090 $FB10:70 09 BVS $FB1B A:11 X:02 Y:03 P:25 SP:F9 PPU:247,220 CYC:25096 $FB12:30 07 BMI $FB1B A:11 X:02 Y:03 P:25 SP:F9 PPU:253,220 CYC:25098 $FB14:90 05 BCC $FB1B A:11 X:02 Y:03 P:25 SP:F9 PPU:259,220 CYC:25100 $FB16:C9 11 CMP #$11 A:11 X:02 Y:03 P:25 SP:F9 PPU:265,220 CYC:25102 $FB18:D0 01 BNE $FB1B A:11 X:02 Y:03 P:nvUbdIZC SP:F9 PPU:271,220 CYC:25104 $FB1A:60 RTS A:11 X:02 Y:03 P:nvUbdIZC SP:F9 PPU:277,220 CYC:25106 $F59D:AD 47 06 LDA $0647 = #$9B A:11 X:02 Y:03 P:nvUbdIZC SP:FB PPU:295,220 CYC:25112 $F5A0:C9 9B CMP #$9B A:9B X:02 Y:03 P:A5 SP:FB PPU:307,220 CYC:25116 $F5A2:F0 02 BEQ $F5A6 A:9B X:02 Y:03 P:nvUbdIZC SP:FB PPU:313,220 CYC:25118 $F5A6:C8 INY A:9B X:02 Y:03 P:nvUbdIZC SP:FB PPU:322,220 CYC:25121 $F5A7:A9 A5 LDA #$A5 A:9B X:02 Y:04 P:25 SP:FB PPU:328,220 CYC:25123 $F5A9:85 47 STA $47 = #$47 A:A5 X:02 Y:04 P:A5 SP:FB PPU:334,220 CYC:25125 $F5AB:20 E9 FA JSR $FAE9 A:A5 X:02 Y:04 P:A5 SP:FB PPU: 2,221 CYC:25128 $FAE9:24 01 BIT $01 = #$FF A:A5 X:02 Y:04 P:A5 SP:F9 PPU: 20,221 CYC:25134 $FAEB:18 CLC A:A5 X:02 Y:04 P:E5 SP:F9 PPU: 29,221 CYC:25137 $FAEC:A9 B2 LDA #$B2 A:A5 X:02 Y:04 P:NVUbdIzc SP:F9 PPU: 35,221 CYC:25139 $FAEE:60 RTS A:B2 X:02 Y:04 P:NVUbdIzc SP:F9 PPU: 41,221 CYC:25141 $F5AE:67 47 *RRA $47 = #$A5 A:B2 X:02 Y:04 P:NVUbdIzc SP:FB PPU: 59,221 CYC:25147 $F5B0:EA NOP A:05 X:02 Y:04 P:25 SP:FB PPU: 74,221 CYC:25152 $F5B1:EA NOP A:05 X:02 Y:04 P:25 SP:FB PPU: 80,221 CYC:25154 $F5B2:EA NOP A:05 X:02 Y:04 P:25 SP:FB PPU: 86,221 CYC:25156 $F5B3:EA NOP A:05 X:02 Y:04 P:25 SP:FB PPU: 92,221 CYC:25158 $F5B4:20 EF FA JSR $FAEF A:05 X:02 Y:04 P:25 SP:FB PPU: 98,221 CYC:25160 $FAEF:70 2A BVS $FB1B A:05 X:02 Y:04 P:25 SP:F9 PPU:116,221 CYC:25166 $FAF1:90 28 BCC $FB1B A:05 X:02 Y:04 P:25 SP:F9 PPU:122,221 CYC:25168 $FAF3:30 26 BMI $FB1B A:05 X:02 Y:04 P:25 SP:F9 PPU:128,221 CYC:25170 $FAF5:C9 05 CMP #$05 A:05 X:02 Y:04 P:25 SP:F9 PPU:134,221 CYC:25172 $FAF7:D0 22 BNE $FB1B A:05 X:02 Y:04 P:nvUbdIZC SP:F9 PPU:140,221 CYC:25174 $FAF9:60 RTS A:05 X:02 Y:04 P:nvUbdIZC SP:F9 PPU:146,221 CYC:25176 $F5B7:A5 47 LDA $47 = #$52 A:05 X:02 Y:04 P:nvUbdIZC SP:FB PPU:164,221 CYC:25182 $F5B9:C9 52 CMP #$52 A:52 X:02 Y:04 P:25 SP:FB PPU:173,221 CYC:25185 $F5BB:F0 02 BEQ $F5BF A:52 X:02 Y:04 P:nvUbdIZC SP:FB PPU:179,221 CYC:25187 $F5BF:C8 INY A:52 X:02 Y:04 P:nvUbdIZC SP:FB PPU:188,221 CYC:25190 $F5C0:A9 29 LDA #$29 A:52 X:02 Y:05 P:25 SP:FB PPU:194,221 CYC:25192 $F5C2:85 47 STA $47 = #$52 A:29 X:02 Y:05 P:25 SP:FB PPU:200,221 CYC:25194 $F5C4:20 FA FA JSR $FAFA A:29 X:02 Y:05 P:25 SP:FB PPU:209,221 CYC:25197 $FAFA:B8 CLV A:29 X:02 Y:05 P:25 SP:F9 PPU:227,221 CYC:25203 $FAFB:18 CLC A:29 X:02 Y:05 P:25 SP:F9 PPU:233,221 CYC:25205 $FAFC:A9 42 LDA #$42 A:29 X:02 Y:05 P:nvUbdIzc SP:F9 PPU:239,221 CYC:25207 $FAFE:60 RTS A:42 X:02 Y:05 P:nvUbdIzc SP:F9 PPU:245,221 CYC:25209 $F5C7:67 47 *RRA $47 = #$29 A:42 X:02 Y:05 P:nvUbdIzc SP:FB PPU:263,221 CYC:25215 $F5C9:EA NOP A:57 X:02 Y:05 P:nvUbdIzc SP:FB PPU:278,221 CYC:25220 $F5CA:EA NOP A:57 X:02 Y:05 P:nvUbdIzc SP:FB PPU:284,221 CYC:25222 $F5CB:EA NOP A:57 X:02 Y:05 P:nvUbdIzc SP:FB PPU:290,221 CYC:25224 $F5CC:EA NOP A:57 X:02 Y:05 P:nvUbdIzc SP:FB PPU:296,221 CYC:25226 $F5CD:20 FF FA JSR $FAFF A:57 X:02 Y:05 P:nvUbdIzc SP:FB PPU:302,221 CYC:25228 $FAFF:70 1A BVS $FB1B A:57 X:02 Y:05 P:nvUbdIzc SP:F9 PPU:320,221 CYC:25234 $FB01:30 18 BMI $FB1B A:57 X:02 Y:05 P:nvUbdIzc SP:F9 PPU:326,221 CYC:25236 $FB03:B0 16 BCS $FB1B A:57 X:02 Y:05 P:nvUbdIzc SP:F9 PPU:332,221 CYC:25238 $FB05:C9 57 CMP #$57 A:57 X:02 Y:05 P:nvUbdIzc SP:F9 PPU:338,221 CYC:25240 $FB07:D0 12 BNE $FB1B A:57 X:02 Y:05 P:nvUbdIZC SP:F9 PPU: 3,222 CYC:25242 $FB09:60 RTS A:57 X:02 Y:05 P:nvUbdIZC SP:F9 PPU: 9,222 CYC:25244 $F5D0:A5 47 LDA $47 = #$14 A:57 X:02 Y:05 P:nvUbdIZC SP:FB PPU: 27,222 CYC:25250 $F5D2:C9 14 CMP #$14 A:14 X:02 Y:05 P:25 SP:FB PPU: 36,222 CYC:25253 $F5D4:F0 02 BEQ $F5D8 A:14 X:02 Y:05 P:nvUbdIZC SP:FB PPU: 42,222 CYC:25255 $F5D8:C8 INY A:14 X:02 Y:05 P:nvUbdIZC SP:FB PPU: 51,222 CYC:25258 $F5D9:A9 37 LDA #$37 A:14 X:02 Y:06 P:25 SP:FB PPU: 57,222 CYC:25260 $F5DB:85 47 STA $47 = #$14 A:37 X:02 Y:06 P:25 SP:FB PPU: 63,222 CYC:25262 $F5DD:20 0A FB JSR $FB0A A:37 X:02 Y:06 P:25 SP:FB PPU: 72,222 CYC:25265 $FB0A:24 01 BIT $01 = #$FF A:37 X:02 Y:06 P:25 SP:F9 PPU: 90,222 CYC:25271 $FB0C:38 SEC A:37 X:02 Y:06 P:E5 SP:F9 PPU: 99,222 CYC:25274 $FB0D:A9 75 LDA #$75 A:37 X:02 Y:06 P:E5 SP:F9 PPU:105,222 CYC:25276 $FB0F:60 RTS A:75 X:02 Y:06 P:65 SP:F9 PPU:111,222 CYC:25278 $F5E0:67 47 *RRA $47 = #$37 A:75 X:02 Y:06 P:65 SP:FB PPU:129,222 CYC:25284 $F5E2:EA NOP A:11 X:02 Y:06 P:25 SP:FB PPU:144,222 CYC:25289 $F5E3:EA NOP A:11 X:02 Y:06 P:25 SP:FB PPU:150,222 CYC:25291 $F5E4:EA NOP A:11 X:02 Y:06 P:25 SP:FB PPU:156,222 CYC:25293 $F5E5:EA NOP A:11 X:02 Y:06 P:25 SP:FB PPU:162,222 CYC:25295 $F5E6:20 10 FB JSR $FB10 A:11 X:02 Y:06 P:25 SP:FB PPU:168,222 CYC:25297 $FB10:70 09 BVS $FB1B A:11 X:02 Y:06 P:25 SP:F9 PPU:186,222 CYC:25303 $FB12:30 07 BMI $FB1B A:11 X:02 Y:06 P:25 SP:F9 PPU:192,222 CYC:25305 $FB14:90 05 BCC $FB1B A:11 X:02 Y:06 P:25 SP:F9 PPU:198,222 CYC:25307 $FB16:C9 11 CMP #$11 A:11 X:02 Y:06 P:25 SP:F9 PPU:204,222 CYC:25309 $FB18:D0 01 BNE $FB1B A:11 X:02 Y:06 P:nvUbdIZC SP:F9 PPU:210,222 CYC:25311 $FB1A:60 RTS A:11 X:02 Y:06 P:nvUbdIZC SP:F9 PPU:216,222 CYC:25313 $F5E9:A5 47 LDA $47 = #$9B A:11 X:02 Y:06 P:nvUbdIZC SP:FB PPU:234,222 CYC:25319 $F5EB:C9 9B CMP #$9B A:9B X:02 Y:06 P:A5 SP:FB PPU:243,222 CYC:25322 $F5ED:F0 02 BEQ $F5F1 A:9B X:02 Y:06 P:nvUbdIZC SP:FB PPU:249,222 CYC:25324 $F5F1:C8 INY A:9B X:02 Y:06 P:nvUbdIZC SP:FB PPU:258,222 CYC:25327 $F5F2:A9 A5 LDA #$A5 A:9B X:02 Y:07 P:25 SP:FB PPU:264,222 CYC:25329 $F5F4:8D 47 06 STA $0647 = #$9B A:A5 X:02 Y:07 P:A5 SP:FB PPU:270,222 CYC:25331 $F5F7:20 E9 FA JSR $FAE9 A:A5 X:02 Y:07 P:A5 SP:FB PPU:282,222 CYC:25335 $FAE9:24 01 BIT $01 = #$FF A:A5 X:02 Y:07 P:A5 SP:F9 PPU:300,222 CYC:25341 $FAEB:18 CLC A:A5 X:02 Y:07 P:E5 SP:F9 PPU:309,222 CYC:25344 $FAEC:A9 B2 LDA #$B2 A:A5 X:02 Y:07 P:NVUbdIzc SP:F9 PPU:315,222 CYC:25346 $FAEE:60 RTS A:B2 X:02 Y:07 P:NVUbdIzc SP:F9 PPU:321,222 CYC:25348 $F5FA:6F 47 06 *RRA $0647 = #$A5 A:B2 X:02 Y:07 P:NVUbdIzc SP:FB PPU:339,222 CYC:25354 $F5FD:EA NOP A:05 X:02 Y:07 P:25 SP:FB PPU: 16,223 CYC:25360 $F5FE:EA NOP A:05 X:02 Y:07 P:25 SP:FB PPU: 22,223 CYC:25362 $F5FF:EA NOP A:05 X:02 Y:07 P:25 SP:FB PPU: 28,223 CYC:25364 $F600:EA NOP A:05 X:02 Y:07 P:25 SP:FB PPU: 34,223 CYC:25366 $F601:20 EF FA JSR $FAEF A:05 X:02 Y:07 P:25 SP:FB PPU: 40,223 CYC:25368 $FAEF:70 2A BVS $FB1B A:05 X:02 Y:07 P:25 SP:F9 PPU: 58,223 CYC:25374 $FAF1:90 28 BCC $FB1B A:05 X:02 Y:07 P:25 SP:F9 PPU: 64,223 CYC:25376 $FAF3:30 26 BMI $FB1B A:05 X:02 Y:07 P:25 SP:F9 PPU: 70,223 CYC:25378 $FAF5:C9 05 CMP #$05 A:05 X:02 Y:07 P:25 SP:F9 PPU: 76,223 CYC:25380 $FAF7:D0 22 BNE $FB1B A:05 X:02 Y:07 P:nvUbdIZC SP:F9 PPU: 82,223 CYC:25382 $FAF9:60 RTS A:05 X:02 Y:07 P:nvUbdIZC SP:F9 PPU: 88,223 CYC:25384 $F604:AD 47 06 LDA $0647 = #$52 A:05 X:02 Y:07 P:nvUbdIZC SP:FB PPU:106,223 CYC:25390 $F607:C9 52 CMP #$52 A:52 X:02 Y:07 P:25 SP:FB PPU:118,223 CYC:25394 $F609:F0 02 BEQ $F60D A:52 X:02 Y:07 P:nvUbdIZC SP:FB PPU:124,223 CYC:25396 $F60D:C8 INY A:52 X:02 Y:07 P:nvUbdIZC SP:FB PPU:133,223 CYC:25399 $F60E:A9 29 LDA #$29 A:52 X:02 Y:08 P:25 SP:FB PPU:139,223 CYC:25401 $F610:8D 47 06 STA $0647 = #$52 A:29 X:02 Y:08 P:25 SP:FB PPU:145,223 CYC:25403 $F613:20 FA FA JSR $FAFA A:29 X:02 Y:08 P:25 SP:FB PPU:157,223 CYC:25407 $FAFA:B8 CLV A:29 X:02 Y:08 P:25 SP:F9 PPU:175,223 CYC:25413 $FAFB:18 CLC A:29 X:02 Y:08 P:25 SP:F9 PPU:181,223 CYC:25415 $FAFC:A9 42 LDA #$42 A:29 X:02 Y:08 P:nvUbdIzc SP:F9 PPU:187,223 CYC:25417 $FAFE:60 RTS A:42 X:02 Y:08 P:nvUbdIzc SP:F9 PPU:193,223 CYC:25419 $F616:6F 47 06 *RRA $0647 = #$29 A:42 X:02 Y:08 P:nvUbdIzc SP:FB PPU:211,223 CYC:25425 $F619:EA NOP A:57 X:02 Y:08 P:nvUbdIzc SP:FB PPU:229,223 CYC:25431 $F61A:EA NOP A:57 X:02 Y:08 P:nvUbdIzc SP:FB PPU:235,223 CYC:25433 $F61B:EA NOP A:57 X:02 Y:08 P:nvUbdIzc SP:FB PPU:241,223 CYC:25435 $F61C:EA NOP A:57 X:02 Y:08 P:nvUbdIzc SP:FB PPU:247,223 CYC:25437 $F61D:20 FF FA JSR $FAFF A:57 X:02 Y:08 P:nvUbdIzc SP:FB PPU:253,223 CYC:25439 $FAFF:70 1A BVS $FB1B A:57 X:02 Y:08 P:nvUbdIzc SP:F9 PPU:271,223 CYC:25445 $FB01:30 18 BMI $FB1B A:57 X:02 Y:08 P:nvUbdIzc SP:F9 PPU:277,223 CYC:25447 $FB03:B0 16 BCS $FB1B A:57 X:02 Y:08 P:nvUbdIzc SP:F9 PPU:283,223 CYC:25449 $FB05:C9 57 CMP #$57 A:57 X:02 Y:08 P:nvUbdIzc SP:F9 PPU:289,223 CYC:25451 $FB07:D0 12 BNE $FB1B A:57 X:02 Y:08 P:nvUbdIZC SP:F9 PPU:295,223 CYC:25453 $FB09:60 RTS A:57 X:02 Y:08 P:nvUbdIZC SP:F9 PPU:301,223 CYC:25455 $F620:AD 47 06 LDA $0647 = #$14 A:57 X:02 Y:08 P:nvUbdIZC SP:FB PPU:319,223 CYC:25461 $F623:C9 14 CMP #$14 A:14 X:02 Y:08 P:25 SP:FB PPU:331,223 CYC:25465 $F625:F0 02 BEQ $F629 A:14 X:02 Y:08 P:nvUbdIZC SP:FB PPU:337,223 CYC:25467 $F629:C8 INY A:14 X:02 Y:08 P:nvUbdIZC SP:FB PPU: 5,224 CYC:25470 $F62A:A9 37 LDA #$37 A:14 X:02 Y:09 P:25 SP:FB PPU: 11,224 CYC:25472 $F62C:8D 47 06 STA $0647 = #$14 A:37 X:02 Y:09 P:25 SP:FB PPU: 17,224 CYC:25474 $F62F:20 0A FB JSR $FB0A A:37 X:02 Y:09 P:25 SP:FB PPU: 29,224 CYC:25478 $FB0A:24 01 BIT $01 = #$FF A:37 X:02 Y:09 P:25 SP:F9 PPU: 47,224 CYC:25484 $FB0C:38 SEC A:37 X:02 Y:09 P:E5 SP:F9 PPU: 56,224 CYC:25487 $FB0D:A9 75 LDA #$75 A:37 X:02 Y:09 P:E5 SP:F9 PPU: 62,224 CYC:25489 $FB0F:60 RTS A:75 X:02 Y:09 P:65 SP:F9 PPU: 68,224 CYC:25491 $F632:6F 47 06 *RRA $0647 = #$37 A:75 X:02 Y:09 P:65 SP:FB PPU: 86,224 CYC:25497 $F635:EA NOP A:11 X:02 Y:09 P:25 SP:FB PPU:104,224 CYC:25503 $F636:EA NOP A:11 X:02 Y:09 P:25 SP:FB PPU:110,224 CYC:25505 $F637:EA NOP A:11 X:02 Y:09 P:25 SP:FB PPU:116,224 CYC:25507 $F638:EA NOP A:11 X:02 Y:09 P:25 SP:FB PPU:122,224 CYC:25509 $F639:20 10 FB JSR $FB10 A:11 X:02 Y:09 P:25 SP:FB PPU:128,224 CYC:25511 $FB10:70 09 BVS $FB1B A:11 X:02 Y:09 P:25 SP:F9 PPU:146,224 CYC:25517 $FB12:30 07 BMI $FB1B A:11 X:02 Y:09 P:25 SP:F9 PPU:152,224 CYC:25519 $FB14:90 05 BCC $FB1B A:11 X:02 Y:09 P:25 SP:F9 PPU:158,224 CYC:25521 $FB16:C9 11 CMP #$11 A:11 X:02 Y:09 P:25 SP:F9 PPU:164,224 CYC:25523 $FB18:D0 01 BNE $FB1B A:11 X:02 Y:09 P:nvUbdIZC SP:F9 PPU:170,224 CYC:25525 $FB1A:60 RTS A:11 X:02 Y:09 P:nvUbdIZC SP:F9 PPU:176,224 CYC:25527 $F63C:AD 47 06 LDA $0647 = #$9B A:11 X:02 Y:09 P:nvUbdIZC SP:FB PPU:194,224 CYC:25533 $F63F:C9 9B CMP #$9B A:9B X:02 Y:09 P:A5 SP:FB PPU:206,224 CYC:25537 $F641:F0 02 BEQ $F645 A:9B X:02 Y:09 P:nvUbdIZC SP:FB PPU:212,224 CYC:25539 $F645:A9 A5 LDA #$A5 A:9B X:02 Y:09 P:nvUbdIZC SP:FB PPU:221,224 CYC:25542 $F647:8D 47 06 STA $0647 = #$9B A:A5 X:02 Y:09 P:A5 SP:FB PPU:227,224 CYC:25544 $F64A:A9 48 LDA #$48 A:A5 X:02 Y:09 P:A5 SP:FB PPU:239,224 CYC:25548 $F64C:85 45 STA $45 = #$48 A:48 X:02 Y:09 P:25 SP:FB PPU:245,224 CYC:25550 $F64E:A9 05 LDA #$05 A:48 X:02 Y:09 P:25 SP:FB PPU:254,224 CYC:25553 $F650:85 46 STA $46 = #$05 A:05 X:02 Y:09 P:25 SP:FB PPU:260,224 CYC:25555 $F652:A0 FF LDY #$FF A:05 X:02 Y:09 P:25 SP:FB PPU:269,224 CYC:25558 $F654:20 E9 FA JSR $FAE9 A:05 X:02 Y:FF P:A5 SP:FB PPU:275,224 CYC:25560 $FAE9:24 01 BIT $01 = #$FF A:05 X:02 Y:FF P:A5 SP:F9 PPU:293,224 CYC:25566 $FAEB:18 CLC A:05 X:02 Y:FF P:E5 SP:F9 PPU:302,224 CYC:25569 $FAEC:A9 B2 LDA #$B2 A:05 X:02 Y:FF P:NVUbdIzc SP:F9 PPU:308,224 CYC:25571 $FAEE:60 RTS A:B2 X:02 Y:FF P:NVUbdIzc SP:F9 PPU:314,224 CYC:25573 $F657:73 45 *RRA ($45),Y = #$0548 @ 0647 = A5 A:B2 X:02 Y:FF P:NVUbdIzc SP:FB PPU:332,224 CYC:25579 $F659:EA NOP A:05 X:02 Y:FF P:25 SP:FB PPU: 15,225 CYC:25587 $F65A:EA NOP A:05 X:02 Y:FF P:25 SP:FB PPU: 21,225 CYC:25589 $F65B:08 PHP A:05 X:02 Y:FF P:25 SP:FB PPU: 27,225 CYC:25591 $F65C:48 PHA A:05 X:02 Y:FF P:25 SP:FA PPU: 36,225 CYC:25594 $F65D:A0 0A LDY #$0A A:05 X:02 Y:FF P:25 SP:F9 PPU: 45,225 CYC:25597 $F65F:68 PLA A:05 X:02 Y:0A P:25 SP:F9 PPU: 51,225 CYC:25599 $F660:28 PLP A:05 X:02 Y:0A P:25 SP:FA PPU: 63,225 CYC:25603 $F661:20 EF FA JSR $FAEF A:05 X:02 Y:0A P:25 SP:FB PPU: 75,225 CYC:25607 $FAEF:70 2A BVS $FB1B A:05 X:02 Y:0A P:25 SP:F9 PPU: 93,225 CYC:25613 $FAF1:90 28 BCC $FB1B A:05 X:02 Y:0A P:25 SP:F9 PPU: 99,225 CYC:25615 $FAF3:30 26 BMI $FB1B A:05 X:02 Y:0A P:25 SP:F9 PPU:105,225 CYC:25617 $FAF5:C9 05 CMP #$05 A:05 X:02 Y:0A P:25 SP:F9 PPU:111,225 CYC:25619 $FAF7:D0 22 BNE $FB1B A:05 X:02 Y:0A P:nvUbdIZC SP:F9 PPU:117,225 CYC:25621 $FAF9:60 RTS A:05 X:02 Y:0A P:nvUbdIZC SP:F9 PPU:123,225 CYC:25623 $F664:AD 47 06 LDA $0647 = #$52 A:05 X:02 Y:0A P:nvUbdIZC SP:FB PPU:141,225 CYC:25629 $F667:C9 52 CMP #$52 A:52 X:02 Y:0A P:25 SP:FB PPU:153,225 CYC:25633 $F669:F0 02 BEQ $F66D A:52 X:02 Y:0A P:nvUbdIZC SP:FB PPU:159,225 CYC:25635 $F66D:A0 FF LDY #$FF A:52 X:02 Y:0A P:nvUbdIZC SP:FB PPU:168,225 CYC:25638 $F66F:A9 29 LDA #$29 A:52 X:02 Y:FF P:A5 SP:FB PPU:174,225 CYC:25640 $F671:8D 47 06 STA $0647 = #$52 A:29 X:02 Y:FF P:25 SP:FB PPU:180,225 CYC:25642 $F674:20 FA FA JSR $FAFA A:29 X:02 Y:FF P:25 SP:FB PPU:192,225 CYC:25646 $FAFA:B8 CLV A:29 X:02 Y:FF P:25 SP:F9 PPU:210,225 CYC:25652 $FAFB:18 CLC A:29 X:02 Y:FF P:25 SP:F9 PPU:216,225 CYC:25654 $FAFC:A9 42 LDA #$42 A:29 X:02 Y:FF P:nvUbdIzc SP:F9 PPU:222,225 CYC:25656 $FAFE:60 RTS A:42 X:02 Y:FF P:nvUbdIzc SP:F9 PPU:228,225 CYC:25658 $F677:73 45 *RRA ($45),Y = #$0548 @ 0647 = 29 A:42 X:02 Y:FF P:nvUbdIzc SP:FB PPU:246,225 CYC:25664 $F679:EA NOP A:57 X:02 Y:FF P:nvUbdIzc SP:FB PPU:270,225 CYC:25672 $F67A:EA NOP A:57 X:02 Y:FF P:nvUbdIzc SP:FB PPU:276,225 CYC:25674 $F67B:08 PHP A:57 X:02 Y:FF P:nvUbdIzc SP:FB PPU:282,225 CYC:25676 $F67C:48 PHA A:57 X:02 Y:FF P:nvUbdIzc SP:FA PPU:291,225 CYC:25679 $F67D:A0 0B LDY #$0B A:57 X:02 Y:FF P:nvUbdIzc SP:F9 PPU:300,225 CYC:25682 $F67F:68 PLA A:57 X:02 Y:0B P:nvUbdIzc SP:F9 PPU:306,225 CYC:25684 $F680:28 PLP A:57 X:02 Y:0B P:nvUbdIzc SP:FA PPU:318,225 CYC:25688 $F681:20 FF FA JSR $FAFF A:57 X:02 Y:0B P:nvUbdIzc SP:FB PPU:330,225 CYC:25692 $FAFF:70 1A BVS $FB1B A:57 X:02 Y:0B P:nvUbdIzc SP:F9 PPU: 7,226 CYC:25698 $FB01:30 18 BMI $FB1B A:57 X:02 Y:0B P:nvUbdIzc SP:F9 PPU: 13,226 CYC:25700 $FB03:B0 16 BCS $FB1B A:57 X:02 Y:0B P:nvUbdIzc SP:F9 PPU: 19,226 CYC:25702 $FB05:C9 57 CMP #$57 A:57 X:02 Y:0B P:nvUbdIzc SP:F9 PPU: 25,226 CYC:25704 $FB07:D0 12 BNE $FB1B A:57 X:02 Y:0B P:nvUbdIZC SP:F9 PPU: 31,226 CYC:25706 $FB09:60 RTS A:57 X:02 Y:0B P:nvUbdIZC SP:F9 PPU: 37,226 CYC:25708 $F684:AD 47 06 LDA $0647 = #$14 A:57 X:02 Y:0B P:nvUbdIZC SP:FB PPU: 55,226 CYC:25714 $F687:C9 14 CMP #$14 A:14 X:02 Y:0B P:25 SP:FB PPU: 67,226 CYC:25718 $F689:F0 02 BEQ $F68D A:14 X:02 Y:0B P:nvUbdIZC SP:FB PPU: 73,226 CYC:25720 $F68D:A0 FF LDY #$FF A:14 X:02 Y:0B P:nvUbdIZC SP:FB PPU: 82,226 CYC:25723 $F68F:A9 37 LDA #$37 A:14 X:02 Y:FF P:A5 SP:FB PPU: 88,226 CYC:25725 $F691:8D 47 06 STA $0647 = #$14 A:37 X:02 Y:FF P:25 SP:FB PPU: 94,226 CYC:25727 $F694:20 0A FB JSR $FB0A A:37 X:02 Y:FF P:25 SP:FB PPU:106,226 CYC:25731 $FB0A:24 01 BIT $01 = #$FF A:37 X:02 Y:FF P:25 SP:F9 PPU:124,226 CYC:25737 $FB0C:38 SEC A:37 X:02 Y:FF P:E5 SP:F9 PPU:133,226 CYC:25740 $FB0D:A9 75 LDA #$75 A:37 X:02 Y:FF P:E5 SP:F9 PPU:139,226 CYC:25742 $FB0F:60 RTS A:75 X:02 Y:FF P:65 SP:F9 PPU:145,226 CYC:25744 $F697:73 45 *RRA ($45),Y = #$0548 @ 0647 = 37 A:75 X:02 Y:FF P:65 SP:FB PPU:163,226 CYC:25750 $F699:EA NOP A:11 X:02 Y:FF P:25 SP:FB PPU:187,226 CYC:25758 $F69A:EA NOP A:11 X:02 Y:FF P:25 SP:FB PPU:193,226 CYC:25760 $F69B:08 PHP A:11 X:02 Y:FF P:25 SP:FB PPU:199,226 CYC:25762 $F69C:48 PHA A:11 X:02 Y:FF P:25 SP:FA PPU:208,226 CYC:25765 $F69D:A0 0C LDY #$0C A:11 X:02 Y:FF P:25 SP:F9 PPU:217,226 CYC:25768 $F69F:68 PLA A:11 X:02 Y:0C P:25 SP:F9 PPU:223,226 CYC:25770 $F6A0:28 PLP A:11 X:02 Y:0C P:25 SP:FA PPU:235,226 CYC:25774 $F6A1:20 10 FB JSR $FB10 A:11 X:02 Y:0C P:25 SP:FB PPU:247,226 CYC:25778 $FB10:70 09 BVS $FB1B A:11 X:02 Y:0C P:25 SP:F9 PPU:265,226 CYC:25784 $FB12:30 07 BMI $FB1B A:11 X:02 Y:0C P:25 SP:F9 PPU:271,226 CYC:25786 $FB14:90 05 BCC $FB1B A:11 X:02 Y:0C P:25 SP:F9 PPU:277,226 CYC:25788 $FB16:C9 11 CMP #$11 A:11 X:02 Y:0C P:25 SP:F9 PPU:283,226 CYC:25790 $FB18:D0 01 BNE $FB1B A:11 X:02 Y:0C P:nvUbdIZC SP:F9 PPU:289,226 CYC:25792 $FB1A:60 RTS A:11 X:02 Y:0C P:nvUbdIZC SP:F9 PPU:295,226 CYC:25794 $F6A4:AD 47 06 LDA $0647 = #$9B A:11 X:02 Y:0C P:nvUbdIZC SP:FB PPU:313,226 CYC:25800 $F6A7:C9 9B CMP #$9B A:9B X:02 Y:0C P:A5 SP:FB PPU:325,226 CYC:25804 $F6A9:F0 02 BEQ $F6AD A:9B X:02 Y:0C P:nvUbdIZC SP:FB PPU:331,226 CYC:25806 $F6AD:A0 0D LDY #$0D A:9B X:02 Y:0C P:nvUbdIZC SP:FB PPU:340,226 CYC:25809 $F6AF:A2 FF LDX #$FF A:9B X:02 Y:0D P:25 SP:FB PPU: 5,227 CYC:25811 $F6B1:A9 A5 LDA #$A5 A:9B X:FF Y:0D P:A5 SP:FB PPU: 11,227 CYC:25813 $F6B3:85 47 STA $47 = #$9B A:A5 X:FF Y:0D P:A5 SP:FB PPU: 17,227 CYC:25815 $F6B5:20 E9 FA JSR $FAE9 A:A5 X:FF Y:0D P:A5 SP:FB PPU: 26,227 CYC:25818 $FAE9:24 01 BIT $01 = #$FF A:A5 X:FF Y:0D P:A5 SP:F9 PPU: 44,227 CYC:25824 $FAEB:18 CLC A:A5 X:FF Y:0D P:E5 SP:F9 PPU: 53,227 CYC:25827 $FAEC:A9 B2 LDA #$B2 A:A5 X:FF Y:0D P:NVUbdIzc SP:F9 PPU: 59,227 CYC:25829 $FAEE:60 RTS A:B2 X:FF Y:0D P:NVUbdIzc SP:F9 PPU: 65,227 CYC:25831 $F6B8:77 48 *RRA $48,X @ 47 = #$A5 A:B2 X:FF Y:0D P:NVUbdIzc SP:FB PPU: 83,227 CYC:25837 $F6BA:EA NOP A:05 X:FF Y:0D P:25 SP:FB PPU:101,227 CYC:25843 $F6BB:EA NOP A:05 X:FF Y:0D P:25 SP:FB PPU:107,227 CYC:25845 $F6BC:EA NOP A:05 X:FF Y:0D P:25 SP:FB PPU:113,227 CYC:25847 $F6BD:EA NOP A:05 X:FF Y:0D P:25 SP:FB PPU:119,227 CYC:25849 $F6BE:20 EF FA JSR $FAEF A:05 X:FF Y:0D P:25 SP:FB PPU:125,227 CYC:25851 $FAEF:70 2A BVS $FB1B A:05 X:FF Y:0D P:25 SP:F9 PPU:143,227 CYC:25857 $FAF1:90 28 BCC $FB1B A:05 X:FF Y:0D P:25 SP:F9 PPU:149,227 CYC:25859 $FAF3:30 26 BMI $FB1B A:05 X:FF Y:0D P:25 SP:F9 PPU:155,227 CYC:25861 $FAF5:C9 05 CMP #$05 A:05 X:FF Y:0D P:25 SP:F9 PPU:161,227 CYC:25863 $FAF7:D0 22 BNE $FB1B A:05 X:FF Y:0D P:nvUbdIZC SP:F9 PPU:167,227 CYC:25865 $FAF9:60 RTS A:05 X:FF Y:0D P:nvUbdIZC SP:F9 PPU:173,227 CYC:25867 $F6C1:A5 47 LDA $47 = #$52 A:05 X:FF Y:0D P:nvUbdIZC SP:FB PPU:191,227 CYC:25873 $F6C3:C9 52 CMP #$52 A:52 X:FF Y:0D P:25 SP:FB PPU:200,227 CYC:25876 $F6C5:F0 02 BEQ $F6C9 A:52 X:FF Y:0D P:nvUbdIZC SP:FB PPU:206,227 CYC:25878 $F6C9:C8 INY A:52 X:FF Y:0D P:nvUbdIZC SP:FB PPU:215,227 CYC:25881 $F6CA:A9 29 LDA #$29 A:52 X:FF Y:0E P:25 SP:FB PPU:221,227 CYC:25883 $F6CC:85 47 STA $47 = #$52 A:29 X:FF Y:0E P:25 SP:FB PPU:227,227 CYC:25885 $F6CE:20 FA FA JSR $FAFA A:29 X:FF Y:0E P:25 SP:FB PPU:236,227 CYC:25888 $FAFA:B8 CLV A:29 X:FF Y:0E P:25 SP:F9 PPU:254,227 CYC:25894 $FAFB:18 CLC A:29 X:FF Y:0E P:25 SP:F9 PPU:260,227 CYC:25896 $FAFC:A9 42 LDA #$42 A:29 X:FF Y:0E P:nvUbdIzc SP:F9 PPU:266,227 CYC:25898 $FAFE:60 RTS A:42 X:FF Y:0E P:nvUbdIzc SP:F9 PPU:272,227 CYC:25900 $F6D1:77 48 *RRA $48,X @ 47 = #$29 A:42 X:FF Y:0E P:nvUbdIzc SP:FB PPU:290,227 CYC:25906 $F6D3:EA NOP A:57 X:FF Y:0E P:nvUbdIzc SP:FB PPU:308,227 CYC:25912 $F6D4:EA NOP A:57 X:FF Y:0E P:nvUbdIzc SP:FB PPU:314,227 CYC:25914 $F6D5:EA NOP A:57 X:FF Y:0E P:nvUbdIzc SP:FB PPU:320,227 CYC:25916 $F6D6:EA NOP A:57 X:FF Y:0E P:nvUbdIzc SP:FB PPU:326,227 CYC:25918 $F6D7:20 FF FA JSR $FAFF A:57 X:FF Y:0E P:nvUbdIzc SP:FB PPU:332,227 CYC:25920 $FAFF:70 1A BVS $FB1B A:57 X:FF Y:0E P:nvUbdIzc SP:F9 PPU: 9,228 CYC:25926 $FB01:30 18 BMI $FB1B A:57 X:FF Y:0E P:nvUbdIzc SP:F9 PPU: 15,228 CYC:25928 $FB03:B0 16 BCS $FB1B A:57 X:FF Y:0E P:nvUbdIzc SP:F9 PPU: 21,228 CYC:25930 $FB05:C9 57 CMP #$57 A:57 X:FF Y:0E P:nvUbdIzc SP:F9 PPU: 27,228 CYC:25932 $FB07:D0 12 BNE $FB1B A:57 X:FF Y:0E P:nvUbdIZC SP:F9 PPU: 33,228 CYC:25934 $FB09:60 RTS A:57 X:FF Y:0E P:nvUbdIZC SP:F9 PPU: 39,228 CYC:25936 $F6DA:A5 47 LDA $47 = #$14 A:57 X:FF Y:0E P:nvUbdIZC SP:FB PPU: 57,228 CYC:25942 $F6DC:C9 14 CMP #$14 A:14 X:FF Y:0E P:25 SP:FB PPU: 66,228 CYC:25945 $F6DE:F0 02 BEQ $F6E2 A:14 X:FF Y:0E P:nvUbdIZC SP:FB PPU: 72,228 CYC:25947 $F6E2:C8 INY A:14 X:FF Y:0E P:nvUbdIZC SP:FB PPU: 81,228 CYC:25950 $F6E3:A9 37 LDA #$37 A:14 X:FF Y:0F P:25 SP:FB PPU: 87,228 CYC:25952 $F6E5:85 47 STA $47 = #$14 A:37 X:FF Y:0F P:25 SP:FB PPU: 93,228 CYC:25954 $F6E7:20 0A FB JSR $FB0A A:37 X:FF Y:0F P:25 SP:FB PPU:102,228 CYC:25957 $FB0A:24 01 BIT $01 = #$FF A:37 X:FF Y:0F P:25 SP:F9 PPU:120,228 CYC:25963 $FB0C:38 SEC A:37 X:FF Y:0F P:E5 SP:F9 PPU:129,228 CYC:25966 $FB0D:A9 75 LDA #$75 A:37 X:FF Y:0F P:E5 SP:F9 PPU:135,228 CYC:25968 $FB0F:60 RTS A:75 X:FF Y:0F P:65 SP:F9 PPU:141,228 CYC:25970 $F6EA:77 48 *RRA $48,X @ 47 = #$37 A:75 X:FF Y:0F P:65 SP:FB PPU:159,228 CYC:25976 $F6EC:EA NOP A:11 X:FF Y:0F P:25 SP:FB PPU:177,228 CYC:25982 $F6ED:EA NOP A:11 X:FF Y:0F P:25 SP:FB PPU:183,228 CYC:25984 $F6EE:EA NOP A:11 X:FF Y:0F P:25 SP:FB PPU:189,228 CYC:25986 $F6EF:EA NOP A:11 X:FF Y:0F P:25 SP:FB PPU:195,228 CYC:25988 $F6F0:20 10 FB JSR $FB10 A:11 X:FF Y:0F P:25 SP:FB PPU:201,228 CYC:25990 $FB10:70 09 BVS $FB1B A:11 X:FF Y:0F P:25 SP:F9 PPU:219,228 CYC:25996 $FB12:30 07 BMI $FB1B A:11 X:FF Y:0F P:25 SP:F9 PPU:225,228 CYC:25998 $FB14:90 05 BCC $FB1B A:11 X:FF Y:0F P:25 SP:F9 PPU:231,228 CYC:26000 $FB16:C9 11 CMP #$11 A:11 X:FF Y:0F P:25 SP:F9 PPU:237,228 CYC:26002 $FB18:D0 01 BNE $FB1B A:11 X:FF Y:0F P:nvUbdIZC SP:F9 PPU:243,228 CYC:26004 $FB1A:60 RTS A:11 X:FF Y:0F P:nvUbdIZC SP:F9 PPU:249,228 CYC:26006 $F6F3:A5 47 LDA $47 = #$9B A:11 X:FF Y:0F P:nvUbdIZC SP:FB PPU:267,228 CYC:26012 $F6F5:C9 9B CMP #$9B A:9B X:FF Y:0F P:A5 SP:FB PPU:276,228 CYC:26015 $F6F7:F0 02 BEQ $F6FB A:9B X:FF Y:0F P:nvUbdIZC SP:FB PPU:282,228 CYC:26017 $F6FB:A9 A5 LDA #$A5 A:9B X:FF Y:0F P:nvUbdIZC SP:FB PPU:291,228 CYC:26020 $F6FD:8D 47 06 STA $0647 = #$9B A:A5 X:FF Y:0F P:A5 SP:FB PPU:297,228 CYC:26022 $F700:A0 FF LDY #$FF A:A5 X:FF Y:0F P:A5 SP:FB PPU:309,228 CYC:26026 $F702:20 E9 FA JSR $FAE9 A:A5 X:FF Y:FF P:A5 SP:FB PPU:315,228 CYC:26028 $FAE9:24 01 BIT $01 = #$FF A:A5 X:FF Y:FF P:A5 SP:F9 PPU:333,228 CYC:26034 $FAEB:18 CLC A:A5 X:FF Y:FF P:E5 SP:F9 PPU: 1,229 CYC:26037 $FAEC:A9 B2 LDA #$B2 A:A5 X:FF Y:FF P:NVUbdIzc SP:F9 PPU: 7,229 CYC:26039 $FAEE:60 RTS A:B2 X:FF Y:FF P:NVUbdIzc SP:F9 PPU: 13,229 CYC:26041 $F705:7B 48 05 *RRA $0548,Y @ 0647 = #$A5 A:B2 X:FF Y:FF P:NVUbdIzc SP:FB PPU: 31,229 CYC:26047 $F708:EA NOP A:05 X:FF Y:FF P:25 SP:FB PPU: 52,229 CYC:26054 $F709:EA NOP A:05 X:FF Y:FF P:25 SP:FB PPU: 58,229 CYC:26056 $F70A:08 PHP A:05 X:FF Y:FF P:25 SP:FB PPU: 64,229 CYC:26058 $F70B:48 PHA A:05 X:FF Y:FF P:25 SP:FA PPU: 73,229 CYC:26061 $F70C:A0 10 LDY #$10 A:05 X:FF Y:FF P:25 SP:F9 PPU: 82,229 CYC:26064 $F70E:68 PLA A:05 X:FF Y:10 P:25 SP:F9 PPU: 88,229 CYC:26066 $F70F:28 PLP A:05 X:FF Y:10 P:25 SP:FA PPU:100,229 CYC:26070 $F710:20 EF FA JSR $FAEF A:05 X:FF Y:10 P:25 SP:FB PPU:112,229 CYC:26074 $FAEF:70 2A BVS $FB1B A:05 X:FF Y:10 P:25 SP:F9 PPU:130,229 CYC:26080 $FAF1:90 28 BCC $FB1B A:05 X:FF Y:10 P:25 SP:F9 PPU:136,229 CYC:26082 $FAF3:30 26 BMI $FB1B A:05 X:FF Y:10 P:25 SP:F9 PPU:142,229 CYC:26084 $FAF5:C9 05 CMP #$05 A:05 X:FF Y:10 P:25 SP:F9 PPU:148,229 CYC:26086 $FAF7:D0 22 BNE $FB1B A:05 X:FF Y:10 P:nvUbdIZC SP:F9 PPU:154,229 CYC:26088 $FAF9:60 RTS A:05 X:FF Y:10 P:nvUbdIZC SP:F9 PPU:160,229 CYC:26090 $F713:AD 47 06 LDA $0647 = #$52 A:05 X:FF Y:10 P:nvUbdIZC SP:FB PPU:178,229 CYC:26096 $F716:C9 52 CMP #$52 A:52 X:FF Y:10 P:25 SP:FB PPU:190,229 CYC:26100 $F718:F0 02 BEQ $F71C A:52 X:FF Y:10 P:nvUbdIZC SP:FB PPU:196,229 CYC:26102 $F71C:A0 FF LDY #$FF A:52 X:FF Y:10 P:nvUbdIZC SP:FB PPU:205,229 CYC:26105 $F71E:A9 29 LDA #$29 A:52 X:FF Y:FF P:A5 SP:FB PPU:211,229 CYC:26107 $F720:8D 47 06 STA $0647 = #$52 A:29 X:FF Y:FF P:25 SP:FB PPU:217,229 CYC:26109 $F723:20 FA FA JSR $FAFA A:29 X:FF Y:FF P:25 SP:FB PPU:229,229 CYC:26113 $FAFA:B8 CLV A:29 X:FF Y:FF P:25 SP:F9 PPU:247,229 CYC:26119 $FAFB:18 CLC A:29 X:FF Y:FF P:25 SP:F9 PPU:253,229 CYC:26121 $FAFC:A9 42 LDA #$42 A:29 X:FF Y:FF P:nvUbdIzc SP:F9 PPU:259,229 CYC:26123 $FAFE:60 RTS A:42 X:FF Y:FF P:nvUbdIzc SP:F9 PPU:265,229 CYC:26125 $F726:7B 48 05 *RRA $0548,Y @ 0647 = #$29 A:42 X:FF Y:FF P:nvUbdIzc SP:FB PPU:283,229 CYC:26131 $F729:EA NOP A:57 X:FF Y:FF P:nvUbdIzc SP:FB PPU:304,229 CYC:26138 $F72A:EA NOP A:57 X:FF Y:FF P:nvUbdIzc SP:FB PPU:310,229 CYC:26140 $F72B:08 PHP A:57 X:FF Y:FF P:nvUbdIzc SP:FB PPU:316,229 CYC:26142 $F72C:48 PHA A:57 X:FF Y:FF P:nvUbdIzc SP:FA PPU:325,229 CYC:26145 $F72D:A0 11 LDY #$11 A:57 X:FF Y:FF P:nvUbdIzc SP:F9 PPU:334,229 CYC:26148 $F72F:68 PLA A:57 X:FF Y:11 P:nvUbdIzc SP:F9 PPU:340,229 CYC:26150 $F730:28 PLP A:57 X:FF Y:11 P:nvUbdIzc SP:FA PPU: 11,230 CYC:26154 $F731:20 FF FA JSR $FAFF A:57 X:FF Y:11 P:nvUbdIzc SP:FB PPU: 23,230 CYC:26158 $FAFF:70 1A BVS $FB1B A:57 X:FF Y:11 P:nvUbdIzc SP:F9 PPU: 41,230 CYC:26164 $FB01:30 18 BMI $FB1B A:57 X:FF Y:11 P:nvUbdIzc SP:F9 PPU: 47,230 CYC:26166 $FB03:B0 16 BCS $FB1B A:57 X:FF Y:11 P:nvUbdIzc SP:F9 PPU: 53,230 CYC:26168 $FB05:C9 57 CMP #$57 A:57 X:FF Y:11 P:nvUbdIzc SP:F9 PPU: 59,230 CYC:26170 $FB07:D0 12 BNE $FB1B A:57 X:FF Y:11 P:nvUbdIZC SP:F9 PPU: 65,230 CYC:26172 $FB09:60 RTS A:57 X:FF Y:11 P:nvUbdIZC SP:F9 PPU: 71,230 CYC:26174 $F734:AD 47 06 LDA $0647 = #$14 A:57 X:FF Y:11 P:nvUbdIZC SP:FB PPU: 89,230 CYC:26180 $F737:C9 14 CMP #$14 A:14 X:FF Y:11 P:25 SP:FB PPU:101,230 CYC:26184 $F739:F0 02 BEQ $F73D A:14 X:FF Y:11 P:nvUbdIZC SP:FB PPU:107,230 CYC:26186 $F73D:A0 FF LDY #$FF A:14 X:FF Y:11 P:nvUbdIZC SP:FB PPU:116,230 CYC:26189 $F73F:A9 37 LDA #$37 A:14 X:FF Y:FF P:A5 SP:FB PPU:122,230 CYC:26191 $F741:8D 47 06 STA $0647 = #$14 A:37 X:FF Y:FF P:25 SP:FB PPU:128,230 CYC:26193 $F744:20 0A FB JSR $FB0A A:37 X:FF Y:FF P:25 SP:FB PPU:140,230 CYC:26197 $FB0A:24 01 BIT $01 = #$FF A:37 X:FF Y:FF P:25 SP:F9 PPU:158,230 CYC:26203 $FB0C:38 SEC A:37 X:FF Y:FF P:E5 SP:F9 PPU:167,230 CYC:26206 $FB0D:A9 75 LDA #$75 A:37 X:FF Y:FF P:E5 SP:F9 PPU:173,230 CYC:26208 $FB0F:60 RTS A:75 X:FF Y:FF P:65 SP:F9 PPU:179,230 CYC:26210 $F747:7B 48 05 *RRA $0548,Y @ 0647 = #$37 A:75 X:FF Y:FF P:65 SP:FB PPU:197,230 CYC:26216 $F74A:EA NOP A:11 X:FF Y:FF P:25 SP:FB PPU:218,230 CYC:26223 $F74B:EA NOP A:11 X:FF Y:FF P:25 SP:FB PPU:224,230 CYC:26225 $F74C:08 PHP A:11 X:FF Y:FF P:25 SP:FB PPU:230,230 CYC:26227 $F74D:48 PHA A:11 X:FF Y:FF P:25 SP:FA PPU:239,230 CYC:26230 $F74E:A0 12 LDY #$12 A:11 X:FF Y:FF P:25 SP:F9 PPU:248,230 CYC:26233 $F750:68 PLA A:11 X:FF Y:12 P:25 SP:F9 PPU:254,230 CYC:26235 $F751:28 PLP A:11 X:FF Y:12 P:25 SP:FA PPU:266,230 CYC:26239 $F752:20 10 FB JSR $FB10 A:11 X:FF Y:12 P:25 SP:FB PPU:278,230 CYC:26243 $FB10:70 09 BVS $FB1B A:11 X:FF Y:12 P:25 SP:F9 PPU:296,230 CYC:26249 $FB12:30 07 BMI $FB1B A:11 X:FF Y:12 P:25 SP:F9 PPU:302,230 CYC:26251 $FB14:90 05 BCC $FB1B A:11 X:FF Y:12 P:25 SP:F9 PPU:308,230 CYC:26253 $FB16:C9 11 CMP #$11 A:11 X:FF Y:12 P:25 SP:F9 PPU:314,230 CYC:26255 $FB18:D0 01 BNE $FB1B A:11 X:FF Y:12 P:nvUbdIZC SP:F9 PPU:320,230 CYC:26257 $FB1A:60 RTS A:11 X:FF Y:12 P:nvUbdIZC SP:F9 PPU:326,230 CYC:26259 $F755:AD 47 06 LDA $0647 = #$9B A:11 X:FF Y:12 P:nvUbdIZC SP:FB PPU: 3,231 CYC:26265 $F758:C9 9B CMP #$9B A:9B X:FF Y:12 P:A5 SP:FB PPU: 15,231 CYC:26269 $F75A:F0 02 BEQ $F75E A:9B X:FF Y:12 P:nvUbdIZC SP:FB PPU: 21,231 CYC:26271 $F75E:A0 13 LDY #$13 A:9B X:FF Y:12 P:nvUbdIZC SP:FB PPU: 30,231 CYC:26274 $F760:A2 FF LDX #$FF A:9B X:FF Y:13 P:25 SP:FB PPU: 36,231 CYC:26276 $F762:A9 A5 LDA #$A5 A:9B X:FF Y:13 P:A5 SP:FB PPU: 42,231 CYC:26278 $F764:8D 47 06 STA $0647 = #$9B A:A5 X:FF Y:13 P:A5 SP:FB PPU: 48,231 CYC:26280 $F767:20 E9 FA JSR $FAE9 A:A5 X:FF Y:13 P:A5 SP:FB PPU: 60,231 CYC:26284 $FAE9:24 01 BIT $01 = #$FF A:A5 X:FF Y:13 P:A5 SP:F9 PPU: 78,231 CYC:26290 $FAEB:18 CLC A:A5 X:FF Y:13 P:E5 SP:F9 PPU: 87,231 CYC:26293 $FAEC:A9 B2 LDA #$B2 A:A5 X:FF Y:13 P:NVUbdIzc SP:F9 PPU: 93,231 CYC:26295 $FAEE:60 RTS A:B2 X:FF Y:13 P:NVUbdIzc SP:F9 PPU: 99,231 CYC:26297 $F76A:7F 48 05 *RRA $0548,X @ 0647 = #$A5 A:B2 X:FF Y:13 P:NVUbdIzc SP:FB PPU:117,231 CYC:26303 $F76D:EA NOP A:05 X:FF Y:13 P:25 SP:FB PPU:138,231 CYC:26310 $F76E:EA NOP A:05 X:FF Y:13 P:25 SP:FB PPU:144,231 CYC:26312 $F76F:EA NOP A:05 X:FF Y:13 P:25 SP:FB PPU:150,231 CYC:26314 $F770:EA NOP A:05 X:FF Y:13 P:25 SP:FB PPU:156,231 CYC:26316 $F771:20 EF FA JSR $FAEF A:05 X:FF Y:13 P:25 SP:FB PPU:162,231 CYC:26318 $FAEF:70 2A BVS $FB1B A:05 X:FF Y:13 P:25 SP:F9 PPU:180,231 CYC:26324 $FAF1:90 28 BCC $FB1B A:05 X:FF Y:13 P:25 SP:F9 PPU:186,231 CYC:26326 $FAF3:30 26 BMI $FB1B A:05 X:FF Y:13 P:25 SP:F9 PPU:192,231 CYC:26328 $FAF5:C9 05 CMP #$05 A:05 X:FF Y:13 P:25 SP:F9 PPU:198,231 CYC:26330 $FAF7:D0 22 BNE $FB1B A:05 X:FF Y:13 P:nvUbdIZC SP:F9 PPU:204,231 CYC:26332 $FAF9:60 RTS A:05 X:FF Y:13 P:nvUbdIZC SP:F9 PPU:210,231 CYC:26334 $F774:AD 47 06 LDA $0647 = #$52 A:05 X:FF Y:13 P:nvUbdIZC SP:FB PPU:228,231 CYC:26340 $F777:C9 52 CMP #$52 A:52 X:FF Y:13 P:25 SP:FB PPU:240,231 CYC:26344 $F779:F0 02 BEQ $F77D A:52 X:FF Y:13 P:nvUbdIZC SP:FB PPU:246,231 CYC:26346 $F77D:C8 INY A:52 X:FF Y:13 P:nvUbdIZC SP:FB PPU:255,231 CYC:26349 $F77E:A9 29 LDA #$29 A:52 X:FF Y:14 P:25 SP:FB PPU:261,231 CYC:26351 $F780:8D 47 06 STA $0647 = #$52 A:29 X:FF Y:14 P:25 SP:FB PPU:267,231 CYC:26353 $F783:20 FA FA JSR $FAFA A:29 X:FF Y:14 P:25 SP:FB PPU:279,231 CYC:26357 $FAFA:B8 CLV A:29 X:FF Y:14 P:25 SP:F9 PPU:297,231 CYC:26363 $FAFB:18 CLC A:29 X:FF Y:14 P:25 SP:F9 PPU:303,231 CYC:26365 $FAFC:A9 42 LDA #$42 A:29 X:FF Y:14 P:nvUbdIzc SP:F9 PPU:309,231 CYC:26367 $FAFE:60 RTS A:42 X:FF Y:14 P:nvUbdIzc SP:F9 PPU:315,231 CYC:26369 $F786:7F 48 05 *RRA $0548,X @ 0647 = #$29 A:42 X:FF Y:14 P:nvUbdIzc SP:FB PPU:333,231 CYC:26375 $F789:EA NOP A:57 X:FF Y:14 P:nvUbdIzc SP:FB PPU: 13,232 CYC:26382 $F78A:EA NOP A:57 X:FF Y:14 P:nvUbdIzc SP:FB PPU: 19,232 CYC:26384 $F78B:EA NOP A:57 X:FF Y:14 P:nvUbdIzc SP:FB PPU: 25,232 CYC:26386 $F78C:EA NOP A:57 X:FF Y:14 P:nvUbdIzc SP:FB PPU: 31,232 CYC:26388 $F78D:20 FF FA JSR $FAFF A:57 X:FF Y:14 P:nvUbdIzc SP:FB PPU: 37,232 CYC:26390 $FAFF:70 1A BVS $FB1B A:57 X:FF Y:14 P:nvUbdIzc SP:F9 PPU: 55,232 CYC:26396 $FB01:30 18 BMI $FB1B A:57 X:FF Y:14 P:nvUbdIzc SP:F9 PPU: 61,232 CYC:26398 $FB03:B0 16 BCS $FB1B A:57 X:FF Y:14 P:nvUbdIzc SP:F9 PPU: 67,232 CYC:26400 $FB05:C9 57 CMP #$57 A:57 X:FF Y:14 P:nvUbdIzc SP:F9 PPU: 73,232 CYC:26402 $FB07:D0 12 BNE $FB1B A:57 X:FF Y:14 P:nvUbdIZC SP:F9 PPU: 79,232 CYC:26404 $FB09:60 RTS A:57 X:FF Y:14 P:nvUbdIZC SP:F9 PPU: 85,232 CYC:26406 $F790:AD 47 06 LDA $0647 = #$14 A:57 X:FF Y:14 P:nvUbdIZC SP:FB PPU:103,232 CYC:26412 $F793:C9 14 CMP #$14 A:14 X:FF Y:14 P:25 SP:FB PPU:115,232 CYC:26416 $F795:F0 02 BEQ $F799 A:14 X:FF Y:14 P:nvUbdIZC SP:FB PPU:121,232 CYC:26418 $F799:C8 INY A:14 X:FF Y:14 P:nvUbdIZC SP:FB PPU:130,232 CYC:26421 $F79A:A9 37 LDA #$37 A:14 X:FF Y:15 P:25 SP:FB PPU:136,232 CYC:26423 $F79C:8D 47 06 STA $0647 = #$14 A:37 X:FF Y:15 P:25 SP:FB PPU:142,232 CYC:26425 $F79F:20 0A FB JSR $FB0A A:37 X:FF Y:15 P:25 SP:FB PPU:154,232 CYC:26429 $FB0A:24 01 BIT $01 = #$FF A:37 X:FF Y:15 P:25 SP:F9 PPU:172,232 CYC:26435 $FB0C:38 SEC A:37 X:FF Y:15 P:E5 SP:F9 PPU:181,232 CYC:26438 $FB0D:A9 75 LDA #$75 A:37 X:FF Y:15 P:E5 SP:F9 PPU:187,232 CYC:26440 $FB0F:60 RTS A:75 X:FF Y:15 P:65 SP:F9 PPU:193,232 CYC:26442 $F7A2:7F 48 05 *RRA $0548,X @ 0647 = #$37 A:75 X:FF Y:15 P:65 SP:FB PPU:211,232 CYC:26448 $F7A5:EA NOP A:11 X:FF Y:15 P:25 SP:FB PPU:232,232 CYC:26455 $F7A6:EA NOP A:11 X:FF Y:15 P:25 SP:FB PPU:238,232 CYC:26457 $F7A7:EA NOP A:11 X:FF Y:15 P:25 SP:FB PPU:244,232 CYC:26459 $F7A8:EA NOP A:11 X:FF Y:15 P:25 SP:FB PPU:250,232 CYC:26461 $F7A9:20 10 FB JSR $FB10 A:11 X:FF Y:15 P:25 SP:FB PPU:256,232 CYC:26463 $FB10:70 09 BVS $FB1B A:11 X:FF Y:15 P:25 SP:F9 PPU:274,232 CYC:26469 $FB12:30 07 BMI $FB1B A:11 X:FF Y:15 P:25 SP:F9 PPU:280,232 CYC:26471 $FB14:90 05 BCC $FB1B A:11 X:FF Y:15 P:25 SP:F9 PPU:286,232 CYC:26473 $FB16:C9 11 CMP #$11 A:11 X:FF Y:15 P:25 SP:F9 PPU:292,232 CYC:26475 $FB18:D0 01 BNE $FB1B A:11 X:FF Y:15 P:nvUbdIZC SP:F9 PPU:298,232 CYC:26477 $FB1A:60 RTS A:11 X:FF Y:15 P:nvUbdIZC SP:F9 PPU:304,232 CYC:26479 $F7AC:AD 47 06 LDA $0647 = #$9B A:11 X:FF Y:15 P:nvUbdIZC SP:FB PPU:322,232 CYC:26485 $F7AF:C9 9B CMP #$9B A:9B X:FF Y:15 P:A5 SP:FB PPU:334,232 CYC:26489 $F7B1:F0 02 BEQ $F7B5 A:9B X:FF Y:15 P:nvUbdIZC SP:FB PPU:340,232 CYC:26491 $F7B5:60 RTS A:9B X:FF Y:15 P:nvUbdIZC SP:FB PPU: 8,233 CYC:26494 $C655:A5 00 LDA $00 = #$00 A:9B X:FF Y:15 P:nvUbdIZC SP:FD PPU: 26,233 CYC:26500 $C657:05 10 ORA $10 = #$00 A:00 X:FF Y:15 P:nvUbdIZC SP:FD PPU: 35,233 CYC:26503 $C659:05 11 ORA $11 = #$00 A:00 X:FF Y:15 P:nvUbdIZC SP:FD PPU: 44,233 CYC:26506 $C65B:F0 0E BEQ $C66B A:00 X:FF Y:15 P:nvUbdIZC SP:FD PPU: 53,233 CYC:26509 $C66B:20 89 C6 JSR $C689 A:00 X:FF Y:15 P:nvUbdIZC SP:FD PPU: 62,233 CYC:26512 $C689:A9 02 LDA #$02 A:00 X:FF Y:15 P:nvUbdIZC SP:FB PPU: 80,233 CYC:26518 ================================================ FILE: tetanes-core/test_roms/cpu/reset.txt ================================================ CPU Power/Reset Tests --------------------- Verifies CPU register values at power, and changes that occur during reset. Also verifies that RAM isn't modified during reset. Expected behavior ----------------- At power: A, X, Y = 0 P = $34 S = $FD After reset: A, X, Y unchanged I flag set (P ORed with $04) S decremented by 3, but nothing written to stack Multi-tests ----------- The NES/NSF builds in the main directory consist of multiple sub-tests. When run, they list the subtests as they are run. The final result code refers to the first sub-test that failed. For more information about any failed subtests, run them individually from rom_singles/ and nsf_singles/. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. -- Shay Green ================================================ FILE: tetanes-core/test_roms/cpu/tests.json ================================================ [ { "name": "branch_backward", "audio": false, "frames": [ { "number": 20, "hash": 14061918475422239861 } ] }, { "name": "nestest", "audio": false, "frames": [ { "number": 10, "action": { "Joypad": ["One", "Start"] } }, { "number": 27, "name": "valid_ops", "hash": 2359113843044038883 }, { "number": 28, "action": { "Joypad": ["One", "Select"] } }, { "number": 30, "action": { "Joypad": ["One", "Start"] } }, { "number": 45, "name": "invalid_ops", "hash": 2651861775339687657 } ] }, { "name": "branch_basics", "audio": false, "frames": [ { "number": 20, "hash": 4683326619789796318 } ] }, { "name": "branch_forward", "audio": false, "frames": [ { "number": 20, "hash": 7096557190932891340 } ] }, { "name": "dummy_reads", "audio": false, "frames": [ { "number": 48, "hash": 1419558795669194341 } ] }, { "name": "dummy_writes_oam", "audio": false, "frames": [ { "number": 330, "hash": 17732444874837838893 } ] }, { "name": "dummy_writes_ppumem", "audio": false, "frames": [ { "number": 235, "hash": 16009121487930158525 } ] }, { "name": "exec_space_apu", "audio": false, "frames": [ { "number": 309, "hash": 12540582317851669771 } ] }, { "name": "exec_space_ppuio", "audio": false, "frames": [ { "number": 50, "hash": 788051276363575926 } ] }, { "name": "flag_concurrency", "audio": false, "frames": [ { "number": 855, "hash": 16686490019241741598 } ] }, { "name": "instr_abs", "audio": false, "frames": [ { "number": 120, "hash": 12549919245884652726 } ] }, { "name": "instr_abs_xy", "audio": false, "frames": [ { "number": 367, "hash": 279001387970484616 } ] }, { "name": "instr_basics", "audio": false, "frames": [ { "number": 20, "hash": 2843100842160455037 } ] }, { "name": "instr_branches", "audio": false, "frames": [ { "number": 44, "hash": 8530223784940472857 } ] }, { "name": "instr_brk", "audio": false, "frames": [ { "number": 26, "hash": 3358184297522776232 } ] }, { "name": "instr_imm", "audio": false, "frames": [ { "number": 90, "hash": 3550829661495618367 } ] }, { "name": "instr_imp", "audio": false, "frames": [ { "number": 110, "hash": 1126135879873262284 } ] }, { "name": "instr_ind_x", "audio": false, "frames": [ { "number": 148, "hash": 2183839236103019073 } ] }, { "name": "instr_ind_y", "audio": false, "frames": [ { "number": 138, "hash": 18163414548108273019 } ] }, { "name": "instr_jmp_jsr", "audio": false, "frames": [ { "number": 20, "hash": 6765162597571383868 } ] }, { "name": "instr_misc", "audio": false, "frames": [ { "number": 240, "hash": 12558610457372999105 } ] }, { "name": "instr_rti", "audio": false, "frames": [ { "number": 20, "hash": 14286295284764617179 } ] }, { "name": "instr_rts", "audio": false, "frames": [ { "number": 20, "hash": 13839107880836985000 } ] }, { "name": "instr_special", "audio": false, "frames": [ { "number": 20, "hash": 13808374822852460766 } ] }, { "name": "instr_stack", "audio": false, "frames": [ { "number": 168, "hash": 13615317851392776885 } ] }, { "name": "instr_timing", "audio": false, "frames": [ { "number": 1305, "hash": 16429169488661253642 } ] }, { "name": "instr_zp", "audio": false, "frames": [ { "number": 119, "hash": 8836625608436215272 } ] }, { "name": "instr_zp_xy", "audio": false, "frames": [ { "number": 261, "hash": 13074514701701181028 } ] }, { "name": "int_branch_delays_irq", "audio": false, "frames": [ { "number": 384, "hash": 16263009087054765958 } ] }, { "name": "int_cli_latency", "audio": false, "frames": [ { "number": 20, "hash": 13339131754356242456 } ] }, { "name": "int_irq_and_dma", "audio": false, "frames": [ { "number": 75, "hash": 5630784201587328054 } ] }, { "name": "int_nmi_and_brk", "audio": false, "frames": [ { "number": 114, "hash": 10078891178501487086 } ] }, { "name": "int_nmi_and_irq", "audio": false, "frames": [ { "number": 134, "hash": 14942820692507785390 } ] }, { "name": "overclock", "audio": false, "frames": [ { "number": 15, "hash": 15555854521700294520 } ] }, { "name": "sprdma_and_dmc_dma", "audio": false, "frames": [ { "number": 145, "hash": 8348901162335247941 } ] }, { "name": "sprdma_and_dmc_dma_512", "audio": false, "frames": [ { "number": 145, "hash": 6133494704757795069 } ] }, { "name": "timing_test", "audio": false, "frames": [ { "number": 615, "hash": 891002094812730945 } ] }, { "name": "ram_after_reset", "audio": false, "frames": [ { "number": 142, "action": { "Reset": "Soft" } }, { "number": 153, "hash": 531954482786283984 } ] }, { "name": "regs_after_reset", "audio": false, "frames": [ { "number": 140, "action": { "Reset": "Soft" } }, { "number": 155, "hash": 13935544292750434938 } ] } ] ================================================ FILE: tetanes-core/test_roms/cpu/timing.txt ================================================ NES 6502 Timing Test -------------------- This program tests instruction timing for all official and unofficial NES 6502 instructions except the 8 branch instructions (Bxx) and the 12 halt instructions (HLT). It tests normal and page crossing cases of all instructions (including instructions that should not take longer due to a page crossing). It passes when run on an NTSC NES (it will not work on a PAL NES due to the differing refresh rate). The test takes up to 16 seconds to complete. If everything passes, it prints a message and beeps twice. If it fails, it prints an error message and beeps once. The test can be restricted to only official instructions or test some or all of the unofficial instructions, in case your emulator doesn't emulate all the unofficial instructions. Select between these by holding the following buttons on controller #1 when starting the test: (nothing) Official instructions only B Official + all unofficial instructions A Official + $EB (equivalent to $E9) + unofficial NOPs: 1-byte NOPs: $1A $3A $5A $7A $DA $FA 2-byte NOPs: $04 $14 $34 $44 $54 $64 $74 $80 $82 $89 $C2 $D4 $E2 $F4 3-byte NOPs: $0C $1C $3C $5C $7C $DC $FC The 12 halt instructions are never tested, as they freeze the NES and require a reset: HLT: $02 $12 $22 $32 $42 $52 $62 $72 $92 $B2 $D2 $F2 The 8 branch instructions aren't tested since they have more subtle page-crossing behavior. Use my branch_timing_tests for these. Source code is included. Support code is also included, but it runs on a custom devcart and assembler so it will require some effort to assemble. Contact me if you'd like assistance porting them to your setup. I really do plan on making my source work with ca65 eventually. Errors ------ All instructions are first tested without a page crossing, then with a page crossing, allowing you to more easily debug timing problems. FAIL OP: The indicated opcode failed. If it was being timed where a page crossing should occur, that will be noted. The number of clocks will be shown that the emulator used, and the correct number of clocks it should have used. UNKNOWN ERROR: Occurs if the instruction timing fails or NMI unexpectedly returns. Prints the opcode and a hex value. Post to the Nesdev forum if you get this error. BASIC TIMING WRONG: If you get this error, then the loop that tests NMI and basic instruction timing (below) ran too many/too few times. If this occurs, verify the timing of the following instructions and your PPU's NMI interrupt timing. loop: cpx zero-page bne stop inc zero-page bne loop inc zero-page jmp loop stop: How the tests work ------------------ All instructions are tested using a common framework which runs the instruction in an infinite loop. Once the loop is eventually interrupted by NMI, the number of times the loop ran is cross-referenced with a table to determine how many clocks the instruction used. For normal timing, instructions which use some form of indexed addressing reference address $0xFD and X and Y are set to 2, which is just shy of a page cross ($FD+2=$FF). To test page crossing timing, X and Y are set to 3, causing a page crossing for relevant instructions ($FD+3=$100). Not all instructions add an extra clock when a page is crossed, so this test reveals missing and extra page crossing penalties. Some instructions require special handling. JMP and JSR are tested by jumping to the next instruction. RTS and RTI are handled by filling the stack with the value $02 and having the next instruction of the loop be at address $0202 (for RTI) or $0203 (for RTS, since it adds one to the return address). BRK is handled by setting the IRQ vector to $0202, the address of the next instruction in the loop after BRK. The trickiness of these special cases might reveal non-timing problems in an emulator. Instruction timing ------------------ The following unofficial instructions have an extra clock added for page crossing and use the indicated addressing mode: Absolute, X: $1C $3C $5C $7C $DC $FC Absolute, Y: $BB $BF Indirect, Y: $B3 These are the tables the test uses. Since it passes on a NES, the values are pretty much guaranteed correct. No page crossing: 0 1 2 3 4 5 6 7 8 9 A B C D E F -------------------------------- 7,6,0,8,3,3,5,5,3,2,2,2,4,4,6,6 | 0 0,5,0,8,4,4,6,6,2,4,2,7,4,4,7,7 | 1 6,6,0,8,3,3,5,5,4,2,2,2,4,4,6,6 | 2 0,5,0,8,4,4,6,6,2,4,2,7,4,4,7,7 | 3 6,6,0,8,3,3,5,5,3,2,2,2,3,4,6,6 | 4 0,5,0,8,4,4,6,6,2,4,2,7,4,4,7,7 | 5 6,6,0,8,3,3,5,5,4,2,2,2,5,4,6,6 | 6 0,5,0,8,4,4,6,6,2,4,2,7,4,4,7,7 | 7 2,6,2,6,3,3,3,3,2,2,2,2,4,4,4,4 | 8 0,6,0,6,4,4,4,4,2,5,2,5,5,5,5,5 | 9 2,6,2,6,3,3,3,3,2,2,2,2,4,4,4,4 | A 0,5,0,5,4,4,4,4,2,4,2,4,4,4,4,4 | B 2,6,2,8,3,3,5,5,2,2,2,2,4,4,6,6 | C 0,5,0,8,4,4,6,6,2,4,2,7,4,4,7,7 | D 2,6,2,8,3,3,5,5,2,2,2,2,4,4,6,6 | E 0,5,0,8,4,4,6,6,2,4,2,7,4,4,7,7 | F Page crossing: 0 1 2 3 4 5 6 7 8 9 A B C D E F -------------------------------- 7,6,0,8,3,3,5,5,3,2,2,2,4,4,6,6 | 0 0,6,0,8,4,4,6,6,2,5,2,7,5,5,7,7 | 1 6,6,0,8,3,3,5,5,4,2,2,2,4,4,6,6 | 2 0,6,0,8,4,4,6,6,2,5,2,7,5,5,7,7 | 3 6,6,0,8,3,3,5,5,3,2,2,2,3,4,6,6 | 4 0,6,0,8,4,4,6,6,2,5,2,7,5,5,7,7 | 5 6,6,0,8,3,3,5,5,4,2,2,2,5,4,6,6 | 6 0,6,0,8,4,4,6,6,2,5,2,7,5,5,7,7 | 7 2,6,2,6,3,3,3,3,2,2,2,2,4,4,4,4 | 8 0,6,0,6,4,4,4,4,2,5,2,5,5,5,5,5 | 9 2,6,2,6,3,3,3,3,2,2,2,2,4,4,4,4 | A 0,6,0,6,4,4,4,4,2,5,2,5,5,5,5,5 | B 2,6,2,8,3,3,5,5,2,2,2,2,4,4,6,6 | C 0,6,0,8,4,4,6,6,2,5,2,7,5,5,7,7 | D 2,6,2,8,3,3,5,5,2,2,2,2,4,4,6,6 | E 0,6,0,8,4,4,6,6,2,5,2,7,5,5,7,7 | F -- Shay Green ================================================ FILE: tetanes-core/test_roms/input/tests.json ================================================ [ { "name": "zapper_flip", "frames": [ { "number": 0, "action": "ToggleZapperConnected" }, { "number": 0, "action": { "ZapperAim": [ 10, 10 ] } }, { "number": 4, "hash": 8615630426075156747 }, { "number": 5, "action": "ZapperTrigger" }, { "number": 6, "hash": 14878161860519346933 }, { "number": 13, "hash": 8615630426075156747 } ] }, { "name": "zapper_light", "frames": [ { "number": 0, "action": "ToggleZapperConnected" }, { "number": 0, "action": { "ZapperAim": [ 10, 10 ] } }, { "number": 4, "hash": 8615630426075156747 }, { "number": 5, "hash": 8615630426075156747 }, { "number": 5, "action": { "ZapperAim": [ 61, 61 ] } }, { "number": 6, "hash": 8615630426075156747 }, { "number": 7, "action": { "ZapperAim": [ 194, 178 ] } }, { "number": 8, "hash": 8615630426075156747 } ] }, { "name": "zapper_stream", "frames": [ { "number": 0, "action": "ToggleZapperConnected" }, { "number": 20, "hash": 0 } ] }, { "name": "zapper_trigger", "frames": [ { "number": 0, "action": "ToggleZapperConnected" }, { "number": 20, "hash": 0 } ] } ] ================================================ FILE: tetanes-core/test_roms/mapper/m004_txrom/irq.txt ================================================ NTSC NES MMC3 IRQ Counter Test ROMs ----------------------------------- These ROMs test much of MMC3 IRQ counter behavior on an NTSC NES PPU. They have been tested on an actual NES with on several MMC3 cartridges and all give a passing result. Many tests are written specifically to catch likely errors in an emulator. Each ROM runs several tests and reports the result on screen and by beeping a number of times. Failure codes for each ROM are listed below. It's best to run the tests in order, because some earlier ROMs test things that later ones assume will work properly. The ROMs mainly test behavior by manually clocking the MMC3's IRQ counter by writing to $2006 to change the current VRAM address. The last two ROMs test different revisions of the MMC3, so at most only one will pass on a particular emulator. All the asm source is included, and most tests are clearly divided into sections. The code runs on a custom devcart and assembler so it will require some effort to assemble. Contact me if you'd like assistance porting them to your setup. MMC3 Operation -------------- I have fairly thoroughly tested MMC3 IRQ counter operation and found the following behaviors that differ as described in kevtris's (draft?) MMC3 documentation: - The counter can be clocked manually via bit 12 of the VRAM address even when $2000 = $00 (bg and sprites both use tiles from $0xxx). - The IRQ flag is not set when the counter is cleared by writing to $C001. - I uncovered some pathological behavior that isn't covered by the test ROMs. If $C001 is written, the counter clocked, then $C001 written again, on the next counter clock the counter will be ORed with $80 (revision B)/frozen (revision A) and neither decremented nor reloaded. If $C001 is written again at this point, on the next counter clock it will be reloaded normally. I put a check in my emulator and none of the several games I tested ever caused this situation to occur, so it's probably not a good idea to implement this. The MMC3 in Crystalis (referred to here as revision A) worked as described in kevtris's document, with the above changes. The MMC3 in Super Mario Bros. 3 and Mega Man 3 (I think revision B, but I don't have the special screw driver) further differed when $C000 was written with 0: - Writing 0 to $C000 works no differently than any other value written; it will cause the counter to be reloaded every time it is clocked (once it reaches zero). - When the counter is clocked, if it's not zero, it is decremented, otherwise it is reloaded with the last value written to $C000. *After* decrementing/reloading, if the counter is zero and IRQ is enabled via $E001, the IRQ flag is set. 1.Clocking ---------- Tests counter operation. Requires support for clocking via manual toggling of VRAM address. 2) Counter/IRQ/A12 clocking isn't working at all 3) Should decrement when A12 is toggled via $2006 4) Writing to $C000 shouldn't cause reload 5) Writing to $C001 shouldn't cause immediate reload 6) Should reload (no decrement) on first clock after clear 7) IRQ should be set when counter is decremented to 0 8) IRQ should never be set when disabled 9) Should reload when clocked when counter is 0 2.Details --------- Tests counter details. 2) Counter isn't working when reloaded with 255 3) Counter should run even when IRQ is disabled 4) Counter should run even after IRQ flag has been set 5) IRQ should not be set when counter reloads with non-zero 6) IRQ should not be set when counter is cleared via $C001 7) Counter should be clocked 241 times in PPU frame 3.A12 Clocking -------------- Tests clocking via bit 12 of VRAM address. 2) Shouldn't be clocked when A12 doesn't change 3) Shouldn't be clocked when A12 changes to 0 4) Should be clocked when A12 changes to 1 via $2006 write 5) Should be clocked when A12 changes to 1 via $2007 read 6) Should be clocked when A12 changes to 1 via $2007 write 4.Scanline Timing ----------------- Tests basic timing for scanlines 0, 1, and 240. 2) Scanline 0 time is too soon 3) Scanline 0 time is too late 4) Scanline 1 time is too soon 5) Scanline 1 time is too late 6) Scanline 239 time is too soon 7) Scanline 239 time is too late 5.MMC3 Rev A ------------ Tests MMC3 revision A differences (tested with Crystalis board). 2) IRQ should be set when reloading to 0 after clear 3) IRQ shouldn't occur when reloading after counter normally reaches 0 6.MMC3 Rev B ------------ Tests MMC3 revision B differences (tested with Super Mario Bros. 3 and Mega Man 3 boards). 2) Should reload and set IRQ every clock when reload is 0 3) IRQ should be set when counter is 0 after reloading -- Shay Green (swap to e-mail) ================================================ FILE: tetanes-core/test_roms/mapper/m004_txrom/tests.json ================================================ [ { "name": "a12_clocking", "frames": [ { "number": 25, "hash": 12040201839270376890 } ] }, { "name": "clocking", "frames": [ { "number": 25, "hash": 3159592045864818260 } ] }, { "name": "details", "frames": [ { "number": 30, "hash": 15211845500498666234 } ] }, { "name": "scanline_timing", "frames": [ { "number": 90, "hash": 6306567989779835575 } ] }, { "name": "big_chr_ram", "frames": [ { "number": 10, "hash": 5491179751774366132 }, { "number": 11, "action": { "Joypad": ["One", "Start"] } }, { "number": 80, "hash": 14367881047247501485 } ] }, { "name": "rev_a", "frames": [ { "number": 0, "action": { "MapperRevision": { "Mmc3": "A" } } }, { "number": 30, "hash": 7203694655519112220 } ] }, { "name": "rev_b", "frames": [ { "number": 0, "action": { "MapperRevision": { "Mmc3": "BC" } } }, { "number": 30, "hash": 5332315838929142497 } ] } ] ================================================ FILE: tetanes-core/test_roms/mapper/m005_exrom/tests.json ================================================ [ { "name": "exram", "frames": [ { "number": 0, "action": { "SetVideoFilter": "Ntsc" } }, { "number": 10, "hash": 17551728759146689352 }, { "number": 15, "hash": 4756351708356043917 }, { "number": 45, "hash": 1380234192467300615 }, { "number": 100, "hash": 9477694461559011324 } ] }, { "name": "basics", "frames": [ { "number": 10, "hash": 13185952026495389028 }, { "number": 11, "action": { "Joypad": [ "One", "A" ] } }, { "number": 14, "name": "obj_table", "hash": 5835282650047076017 }, { "number": 15, "action": { "Joypad": [ "One", "B" ] } }, { "number": 18, "name": "bg_table", "hash": 14203633185999536969 }, { "number": 19, "action": { "Joypad": [ "One", "Start" ] } }, { "number": 22, "name": "obj_size", "hash": 14397147232086588632 }, { "number": 23, "action": { "Joypad": [ "One", "Select" ] } }, { "number": 26, "name": "exram", "hash": 14992356073095190656 }, { "number": 27, "action": { "Joypad": [ "One", "Up" ] } }, { "number": 30, "name": "fill", "hash": 8833133164801041322 }, { "number": 31, "action": { "Joypad": [ "One", "Up" ] } }, { "number": 33, "action": { "Joypad": [ "One", "Left" ] } }, { "number": 36, "name": "bank_left", "hash": 13357435064697061420 }, { "number": 37, "action": { "Joypad": [ "One", "Right" ] } }, { "number": 39, "action": { "Joypad": [ "One", "Right" ] } }, { "number": 42, "name": "bank_right", "hash": 10119745483606927626 } ] } ] ================================================ FILE: tetanes-core/test_roms/ppu/blargg_readme.txt ================================================ NTSC NES PPU Tests ------------------ These ROMs test a few aspects of the NTSC NES PPU operation. They have been tested on an actual NES and all give a passing result. I wrote them to verify that my NES emulator's PPU was working properly. Each ROM runs several tests and reports a result code on screen and by beeping a number of times. A result code of 1 always indicates that all tests were passed; see below for the meaning of other codes for each test. The main source code for each test is included, and most tests are clearly divided into sections. Some of the common support code is included, but not all, since it runs on a custom setup. Contact me if you want to assemble the tests yourself. Shay Green (swap to e-mail) palette_ram ----------- PPU palette RAM read/write and mirroring test 1) Tests passed 2) Palette read shouldn't be buffered like other VRAM 3) Palette write/read doesn't work 4) Palette should be mirrored within $3f00-$3fff 5) Write to $10 should be mirrored at $00 6) Write to $00 should be mirrored at $10 power_up_palette ---------------- Reports whether initial values in palette at power-up match those that my NES has. These values are probably unique to my NES. 1) Palette matches 2) Palette differs from table sprite_ram ---------- Tests sprite RAM access via $2003, $2004, and $4014 1) Tests passed 2) Basic read/write doesn't work 3) Address should increment on $2004 write 4) Address should not increment on $2004 read 5) Third sprite bytes should be masked with $e3 on read 6) $4014 DMA copy doesn't work at all 7) $4014 DMA copy should start at value in $2003 and wrap 8) $4014 DMA copy should leave value in $2003 intact vbl_clear_time -------------- The VBL flag ($2002.7) is cleared by the PPU around 2270 CPU clocks after NMI occurs. 1) Tests passed 2) VBL flag cleared too soon 3) VBL flag cleared too late vram_access ----------- Tests PPU VRAM read/write and internal read buffer operation 1) Tests passed 2) VRAM reads should be delayed in a buffer 3) Basic Write/read doesn't work 4) Read buffer shouldn't be affected by VRAM write 5) Read buffer shouldn't be affected by palette write 6) Palette read should also read VRAM into read buffer 7) "Shadow" VRAM read unaffected by palette transparent color mirroring ================================================ FILE: tetanes-core/test_roms/ppu/oam_read.txt ================================================ NES OAM Read Test ----------------- Tests OAM reading ($2004), being sure it reads the byte from OAM at the current address in $2003. It scans OAM from 0 to $FF, testing each byte in sequence. It prints a '-' where it reads back from the current address, and '*' where it doesn't. Each row represents 16 bytes of OAM, 16 rows total. Results ------- On my NTSC front-loader NES, I get the following four general patterns at random after power/reset: ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- oam_read Passed ---------------- ---------------- --------*------* ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- 694ADBE0 oam_read Failed ---------------- ---------------- ********-------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- ---------------- E9E8E60F oam_read Failed **************** *********------- --------*-*-*-*- *-*-*-*-*-*-*-*- *-*-*-*-*-*-*-*- *-*-*-*-*-*-*-*- ***-*-*-*-*-*-*- *-*-*-*-*-*-*-*- *-*-*-*-*-*-*-*- *-*-*-*-*-*-*-*- *-*-*-*-*-*-*-*- *-*-*-*-*-*-*-*- *-*-*-*-*-*-*-*- *-*-*-*-*-*-*-*- ***-*-*-*-*-*-*- *-*-*-*-*-*-*-*- 44551956 oam_read Failed Flashes, clicks, other glitches ------------------------------- Some tests might need to turn the screen off and on, or cause slight audio clicks. This does not indicate failure, and should be ignored. Only the test result reported at the end is important, unless stated otherwise. Text output ----------- Tests generally print information on screen. They also output the same text as a zero-terminted string beginning at $6004, allowing examination of output in an NSF player, or a NES emulator without a working PPU. The tests also work properly if the PPU doesn't set the VBL flag properly or doesn't implement it at all. The final result is displayed and also written to $6000. Before the test starts, $80 is written there so you can tell when it's done. If a test needs the NES to be reset, it writes $81 there (emulator should wait a couple of frames after seeing $81). In addition, $DE $B0 $G1 is written to $6001-$6003 to allow an emulator to detect when a test is being run, as opposed to some other NES program. In NSF builds, the final result is also reported via a series of beeps (see below). See the source code for more information about a particular test and why it might be failing. Each test has comments and correct output at the top. NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to avoid the NSF player from thinking the track is silent and thus ending the track before it's done testing. In addition to the other text output methods described above, NSF builds report essential information bytes audibly, including the final result. A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason as listed in the source code by the corresponding set_code line. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 -- Shay Green ================================================ FILE: tetanes-core/test_roms/ppu/oam_stress.txt ================================================ NES OAM Stress Test ------------------- Thoroughly tests OAM address ($2003) and read/write ($2004). On an NTSC NES, this passes only for one of the four random PPU-CPU synchronizations at power/reset. Test takes about 30 seconds, unless it fails. This test randomly sets the address, then randomly either writes a random number of random bytes, or reads from the current address a random number of times and verifies that it matches what's expected. It does this for tens of seconds (refreshing OAM periodically so it doesn't fade). Once done, it verifies that all bytes in OAM match what's expected. Expected behavior: $2003 write sets OAM address. $2004 write sets byte at current OAM address to byte written, then increments OAM address. $2004 read gives byte at current OAM address, without modifying OAM address. Flashes, clicks, other glitches ------------------------------- Some tests might need to turn the screen off and on, or cause slight audio clicks. This does not indicate failure, and should be ignored. Only the test result reported at the end is important, unless stated otherwise. Text output ----------- Tests generally print information on screen. They also output the same text as a zero-terminted string beginning at $6004, allowing examination of output in an NSF player, or a NES emulator without a working PPU. The tests also work properly if the PPU doesn't set the VBL flag properly or doesn't implement it at all. The final result is displayed and also written to $6000. Before the test starts, $80 is written there so you can tell when it's done. If a test needs the NES to be reset, it writes $81 there (emulator should wait a couple of frames after seeing $81). In addition, $DE $B0 $G1 is written to $6001-$6003 to allow an emulator to detect when a test is being run, as opposed to some other NES program. In NSF builds, the final result is also reported via a series of beeps (see below). See the source code for more information about a particular test and why it might be failing. Each test has comments and correct output at the top. NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to avoid the NSF player from thinking the track is silent and thus ending the track before it's done testing. In addition to the other text output methods described above, NSF builds report essential information bytes audibly, including the final result. A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason as listed in the source code by the corresponding set_code line. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 -- Shay Green ================================================ FILE: tetanes-core/test_roms/ppu/open_bus.txt ================================================ NES PPU Open-Bus Test --------------------- Tests behavior when reading from open-bus PPU bits/registers, those bits that aren't otherwise defined. Unlike other open-bus addresses, the PPU ones are separate. Takes about 5 seconds to run. The PPU effectively has a "decay register", an 8-bit register. Each bit can be refreshed with a 0 or 1. If a bit isn't refreshed with a 1 for about 600 milliseconds, it will decay to 0 (some decay sooner, depending on the NES and temperature). Writing to any PPU register sets the decay register to the value written. Reading from a PPU register is more complex. The following shows the effect of a read from each register: Addr Open-bus bits 7654 3210 - - - - - - - - - - - - - - - - $2000 DDDD DDDD $2001 DDDD DDDD $2002 ---D DDDD $2003 DDDD DDDD $2004 ---- ---- $2005 DDDD DDDD $2006 DDDD DDDD $2007 ---- ---- non-palette DD-- ---- palette A D means that this bit reads back as whatever is in the decay register at that bit, and doesn't refresh the decay register at that bit. A - means that this bit reads back as defined by the PPU, and refreshes the decay register at the corresponding bit. Flashes, clicks, other glitches ------------------------------- Some tests might need to turn the screen off and on, or cause slight audio clicks. This does not indicate failure, and should be ignored. Only the test result reported at the end is important, unless stated otherwise. Text output ----------- Tests generally print information on screen. They also output the same text as a zero-terminted string beginning at $6004, allowing examination of output in an NSF player, or a NES emulator without a working PPU. The tests also work properly if the PPU doesn't set the VBL flag properly or doesn't implement it at all. The final result is displayed and also written to $6000. Before the test starts, $80 is written there so you can tell when it's done. If a test needs the NES to be reset, it writes $81 there (emulator should wait a couple of frames after seeing $81). In addition, $DE $B0 $G1 is written to $6001-$6003 to allow an emulator to detect when a test is being run, as opposed to some other NES program. In NSF builds, the final result is also reported via a series of beeps (see below). See the source code for more information about a particular test and why it might be failing. Each test has comments and correct output at the top. NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to avoid the NSF player from thinking the track is silent and thus ending the track before it's done testing. In addition to the other text output methods described above, NSF builds report essential information bytes audibly, including the final result. A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason as listed in the source code by the corresponding set_code line. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 -- Shay Green ================================================ FILE: tetanes-core/test_roms/ppu/read_buffer.txt ================================================ NES PPU Read Buffer Tests ---------------------------------- This mammoth test pack tests many aspects of the NES system, mostly centering around the PPU $2007 read buffer. The test will take about 20 seconds. The program attempts to do as many tests as possible before reporting the result. When the screen is blanked for a long time, audio is used to report progress. A low-pitched fat tone indicates failure; bright beeps indicate progress. If a sub-test fails, at a certain point the list of all failed tests is provided in a numeric form, and a textual explanation of the first failed test is shown. Full list of tests performed is below. Note that the tests are not performed in a numerical order. For example, test #47 (does palette reading work at all) is performed before test #7 (does sequential palette reading work). Test 2 (TEST_PPUMEMORYIO): PPU memory I/O does not work. Possible areas of problem: - PPU not implemented - PPU memory writing ($2007) - PPU memory reading ($2007) - PPU memory area $2C00-$2FFF Test 3 (TEST_ONEBYTEBUFFER): Non-palette PPU memory reads should have one-byte buffer. Test 4 (TEST_CIRAM_READ): CIRAM reading does not work. Test 5 (TEST_CIRAM_SEQ_READ_1): Sequential CIRAM reading with 1-byte increment does not work. Test 6 (TEST_CIRAM_SEQ_READ_32): Sequential CIRAM reading with 32-byte increment does not work. Test 7 (TEST_PALETTE_RAM_SEQ_READ_1): Sequential PALETTE reading with 1-byte increment does not work. Test 8 (TEST_PALETTE_RAM_SEQ_READ_32): Sequential PALETTE reading with 32-byte increment does not work. Test 9 (TEST_CHRROM_READ): CHR-ROM reading does not work. Test 10 (TEST_CHRROM_SEQ_READ_1): Sequential CHR-ROM reading with 1-byte increment does not work. Test 11 (TEST_CHRROM_SEQ_READ_32): Sequential CHR-ROM reading with 32-byte increment does not work. Test 12 (TEST_CIRAM_SEQ_WRITE_1): Sequential CIRAM writes with 1-byte increment does not work. Test 13 (TEST_CIRAM_SEQ_WRITE_32): Sequential CIRAM writes with 32-byte increment does not work. Test 14 (TEST_NTA_MIRRORING_FAIL_1NTA): 1-nametable setup seems to be active, even though this ROM is explicitly configured for horizontal mirroring. Test 15 (TEST_NTA_MIRRORING_FAIL_4NTA): Four-screen setup seems to be active, even though this ROM is explicitly configured for horizontal mirroring. Test 16 (TEST_NTA_MIRRORING_FAIL_VERT): Vertical mirroring seems to be active, even though this ROM is explicitly configured for horizontal mirroring. Test 17 (TEST_PPU_OPEN_BUS): Any data that is transferred through PPU I/O should linger and be readable for a while in any PPU register that does not have a read function. This is called "open bus". To minimally pass this test, you need to at least provide a bridge between $2003(W) and $2000(R). Test 18 (TEST_PPU_OPEN_BUS_SHORTCUT): Reading a write-only PPU register should not just give the current value of SPRADDR. That would be a too lazy workaround for a failed test! Test 19 (TEST_PPU_OPENBUS_MUST_NOT_COPY_READBUFFER): PPU memory read buffer is not the open bus. Reading the bus should repeat the last value that was transferred, not disclose the buffered byte. Test 20 (TEST_PPU_OPENBUS_FROM_WRITE2000_MUST_NOT_WRITETO_READBUFFER): A write to $2000 must not overwrite the $2007 read buffer. Test 21 (TEST_PPU_OPENBUS_FROM_WRITE2001_MUST_NOT_WRITETO_READBUFFER): A write to $2001 must not overwrite the $2007 read buffer. Test 22 (TEST_PPU_OPENBUS_FROM_WRITE2002_MUST_NOT_WRITETO_READBUFFER): A write to $2002 must not overwrite the $2007 read buffer. Test 23 (TEST_PPU_OPENBUS_FROM_WRITE2003_MUST_NOT_WRITETO_READBUFFER): A write to $2003 must not overwrite the $2007 read buffer. Test 24 (TEST_PPU_OPENBUS_FROM_WRITE2004_MUST_NOT_WRITETO_READBUFFER): A write to $2004 must not overwrite the $2007 read buffer. Test 25 (TEST_PPU_OPENBUS_FROM_WRITE2005_MUST_NOT_WRITETO_READBUFFER): A write to $2005 must not overwrite the $2007 read buffer. Test 26 (TEST_PPU_OPENBUS_FROM_WRITE2006_MUST_NOT_WRITETO_READBUFFER): A write to $2006 must not overwrite the $2007 read buffer. Test 27 (TEST_PPU_OPENBUS_FROM_WRITE2007_MUST_NOT_WRITETO_READBUFFER): A write to $2007 must not overwrite the $2007 read buffer. Test 28 (TEST_PPU_OPENBUS_FROM_READ2000_MUST_NOT_WRITETO_READBUFFER): A read from $2000 must not overwrite the $2007 read buffer. Test 29 (TEST_PPU_OPENBUS_FROM_READ2001_MUST_NOT_WRITETO_READBUFFER): A read from $2001 must not overwrite the $2007 read buffer. Test 30 (TEST_PPU_OPENBUS_FROM_READ2002_MUST_NOT_WRITETO_READBUFFER): A read from $2002 must not overwrite the $2007 read buffer. Test 31 (TEST_PPU_OPENBUS_FROM_READ2003_MUST_NOT_WRITETO_READBUFFER): A read from $2003 must not overwrite the $2007 read buffer. Test 32 (TEST_PPU_OPENBUS_FROM_READ2004_MUST_NOT_WRITETO_READBUFFER): A read from $2004 must not overwrite the $2007 read buffer. Test 33 (TEST_PPU_OPENBUS_FROM_READ2005_MUST_NOT_WRITETO_READBUFFER): A read from $2005 must not overwrite the $2007 read buffer. Test 34 (TEST_PPU_OPENBUS_FROM_READ2006_MUST_NOT_WRITETO_READBUFFER): A read from $2006 must not overwrite the $2007 read buffer. Test 35 (TEST_PPU_OPENBUS_INDEXED): STA $2000,Y with Y=7 must issue a dummy read to $2007. Test 36 (TEST_PPU_OPENBUS_INDEXED2): STA $1FF0,Y with Y=$17 mustn't issue a dummy read to $2007. Test 37 (TEST_PPU_OPENBUS_FROM_READ_MIRROR_MUST_WRITETO_READBUFFER): A read from a mirrored copy of $2007 must act as if $2007 was read, and update the same read buffer. Test 38 (TEST_PPU_READ_WITH_AND): The AND instruction must be usable for reading $2007 or any other I/O port. Test 39 (TEST_PPU_READ_WITH_ORA): The ORA instruction must be usable for reading $2007 or any other I/O port. Test 40 (TEST_PPU_READ_WITH_EOR): The EOR instruction must be usable for reading $2007 or any other I/O port. Test 41 (TEST_PPU_READ_WITH_CMP): The CMP instruction must be usable for reading $2007 or any other I/O port. Test 42 (TEST_PPU_READ_WITH_CPX): The CPX instruction must be usable for reading $2007 or any other I/O port. Test 43 (TEST_PPU_READ_WITH_CPY): The CPY instruction must be usable for reading $2007 or any other I/O port. Test 44 (TEST_PPU_READ_WITH_ADC): The ADC instruction must be usable for reading $2007 or any other I/O port. Test 45 (TEST_PPU_READ_WITH_SBC): The SBC instruction must be usable for reading $2007 or any other I/O port. Test 46 (TEST_ONEBYTEBUFFER_PALETTE): Palette reads from PPU should not have one-byte buffer. Test 47 (TEST_PALETTE_READS): Palette reads from PPU do not seem to be working at all. Test 48 (TEST_PALETTE_READS_UNRELIABLE): Palette reads from PPU seem to work randomly. Test 49 (TEST_PALETTE_MIRRORS): Palette indexes $3F1x should be mirrors of $3F0x when x is 0, 4, 8, or C. Test 50 (TEST_PALETTE_UNIQUE): It must be possible to store unique data in each of $3F00, $3F04, $3F08 and $3F0C. Test 51 (TEST_PPU_PALETTE_WRAP): PPU addresses 3F00-3F1F should be mirrored within the whole 3F00-3FFF region, for a total of 8 times. Test 52 (TEST_PPU_MEMORY_14BIT_A): Failed sub-test 1 of: The two MSB within the PPU memory address should be completely ignored in all circumstances, effectively mirroring the 0000-3FFF address range within the whole 0000-FFFF region, for a total of 4 times. Test 53 (TEST_PPU_MEMORY_14BIT_B): Failed sub-test 2 of: The two MSB within the PPU memory address should be completely ignored in all circumstances, effectively mirroring the 0000-3FFF address range within the whole 0000-FFFF region, for a total of 4 times. Test 54 (TEST_PPU_MIRROR_3000): PPU memory range 3000-3EFE should be a mirror of the PPU memory range 2000-2EFE. Test 55 (TEST_PPU_READ_3EFF): Setting PPU address to 3EFF and reading $2007 twice should give the data at $3F00, not the data at $2EFF. Test 56 (TEST_PPU_MIRROR_2F): Reading PPU memory range 3Fxx should put contents of 2Fxx into the read buffer. Test 57 (TEST_PPU_SEQ_READ_WRAP): Setting PPU address to 3FFF & reading $2007 thrice should give the contents of $0000. Test 58 (SEQ_READ_INTERNAL): Unexpected: VROM contents at $0000 and $1FFF read the same. This should never happen in this test ROM. Test 59 (TEST_VADDR): Relationship between $2005 and $2006 is not implemented properly. Here is a guide. It explains which registers use which parts of the address. Note that only the second write to $2006 updates the address really used by $2007. FEDCBA9876543210ZYX: bit pos. ^^^^^^^^^^^^^^------ =$2007 zz543210-------------- $2006#1 76543210------ $2006#2 76543210--- $2005#1 210--76543----------- $2005#2 10---------------- $2000 Test 60 (TEST_RAM_MIRRORING): CPU RAM at 0000-07FF should be mirrored 4 times, in the following address ranges: - 0000-07FF - 0800-0FFF - 1000-17FF - 1800-1FFF Test 61 (TEST_PPUIO_MIRRORING): PPU I/O memory at 2000-2007 should be mirrored within the whole 2000-3FFF region, for a total of 1024 times. Test 62 (TEST_SPHIT_AND_VBLANK): Sprite 0 hit flag should not read as set during vblank. Test 63 (TEST_SPHIT_DIRECT): Sprite 0 hit test by poking data directly into $2003-4 ^ Possible causes for failure: - $2003/$2004 not implemented - No sprite 0 hit tests - Way too long vblank period Test 64 (TEST_SPHIT_DIRECT_READBUFFER): Sending 5 bytes of data into $2003 and $2004 must not overwrite the $2007 read buffer. Test 65 (TEST_SPHIT_DMA_ROM): Sprite 0 hit test using DMA ($4014) using ROM as source ^ Possible causes for failure: - $4014 DMA cannot read from anything other than RAM Test 66 (TEST_SPHIT_DMA_READBUFFER): Invoking a $4014 DMA with a non-$20 value must not overwrite the $2007 read buffer. Test 67 (TEST_SPHIT_DMA_PPU_BUS): Sprite 0 hit test using DMA ($4014) using PPU I/O bus as source ^ In this test, $4014 <- #$20. Possible causes for failure: - DMA does not do proper reads - PPU bus does not preserve last transferred values - $2002 read returned a value that differs from expected - $2004 read modifies the OAM Test 68 (TEST_DMA_PPU_SIDEEFFECT): Writing $20 into $4014 should generate 32 reads into $2007 as a side-effect, each time incrementing the PPU read address. Test 69 (TEST_SPHIT_DMA_RAM): Sprite 0 hit test using DMA. All internal RAM pages are tested, including mirrored addresses. Failing the test may imply faulty mirroring. Test 70 (TEST_CHRROM_READ_BANKED): CHR ROM read through $2007 does not honor mapper 3 (CNROM) bank switching Test 71 (TEST_CHRROM_READ_BANKED_BUFFER): The $2007 read buffer should not retroactively react to changes in VROM mapping. When you read $2007, the data is stored in a buffer ("latch"), and the previous content of the buffer is returned. It is not a delayed read request. Test 72 (TEST_CHRROM_WRITE): CHR ROM on mapper 3 (CNROM boards) must not be writable. Test 73 (TEST_BUFFER_DELAY_BLANK_1FRAME): The PPU read buffer should survive 1 frame of idle with rendering disabled. Test 74 (TEST_BUFFER_DELAY_BLANK_2SECONDS): The PPU read buffer should survive 2 seconds of idle with rendering disabled. Test 75 (TEST_BUFFER_DELAY_INTERNAL): Unexpected: VROM contents at $1Bxx did not match what was hardcoded into the program. Test 76 (TEST_BUFFER_DELAY_VISIBLE_1FRAME): The PPU read buffer should survive 1 frame of idle with rendering enabled. Test 77 (TEST_BUFFER_DELAY_VISIBLE_1SECOND): The PPU read buffer should survive 1 second of idle with rendering enabled. Test 78 (TEST_BUFFER_DELAY_VISIBLE_3SECONDS): The PPU read buffer should survive 3 seconds of idle with rendering enabled. Test 79 (TEST_BUFFER_DELAY_VISIBLE_7SECONDS): The PPU read buffer should survive 7 seconds of idle with rendering enabled. Expected output: TEST:test_ppu_read_buffer :) ------------------------------- Testing basic PPU memory I/O. Performing tests that combine sprite 0 hit flag, $4014 DMA and the RAM mirroring... Graphical artifacts during this test are OK and expected. Hit No-Hit Direct poke OK OK DMA with ROM OK OK DMA + PPU bus OK OK DMA with RAM OK OK ------------------------------- This next test will take a while. In order to distract you with entertainment, art is provided. Contemplate on the art while the test is in progress. Passed The ":)" should be blue/purple; the "OK" should be brownish orange, and the "Graphical artifacts" paragraph should also be brownish orange. Everything else should be white. In the painting by Thomas Kinkade that is shown before the "Passed" text appears, the ground should be pleasantly green. The text outputted to the $6000 console is slightly different than the reference shown above, because parts of the text above are placed on the screen directly, and for other reasons. Because this ROM contains a large amount of text and some graphics data, portions of the ROM had to be compressed to avoid increasing the ROM size too much. Should one want to rebuild the ROM, a particular set of tools will be needed; including nasm, gcc, and php. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The text output may include ANSI color codes, which take the form of an esc character ($1B), an opening bracket ('['), and a sequence of numbers and semicolon characters, terminated by a non-digit character ('m'). The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. -- Joel Yliluoma Shay Green ================================================ FILE: tetanes-core/test_roms/ppu/spr_hit.txt ================================================ NTSC NES PPU Sprite 0 Test ROMs ------------------------------- These ROMs test much of sprite 0 hit behavior on a NTSC NES PPU. They have been tested on an actual NES and all give a passing result. I wrote them to verify that my NES emulator's sprite 0 hit emulation was working properly. Each test ROM runs several tests and reports the result on screen and by beeping a number of times. See below for the meaning of failure codes for each test. It's best to run the tests in order, because some earlier ROMs test things that later ones assume will work properly. The main source code for each test is included, and most tests are clearly divided into sections. All the asm source is included, but it runs on a custom devcart and assembler so it will require some effort to assemble. Contact me if you'd assistance porting them to your setup. 01.basics --------- Tests basic sprite 0 hit behavior (nothing timing related). 2) Sprite hit isn't working at all 3) Should hit even when completely behind background 4) Should miss when background rendering is off 5) Should miss when sprite rendering is off 6) Should miss when all rendering is off 7) All-transparent sprite should miss 8) Only low two palette index bits are relevant 9) Any non-zero palette index should hit with any other 10) Should miss when background is all transparent 11) Should always miss other sprites 02.alignment ------------ Tests alignment of sprite hit with background. Places a solid background tile in the middle of the screen and places the sprite on all four edges both overlapping and non-overlapping. 2) Basic sprite-background alignment is way off 3) Sprite should miss left side of bg tile 4) Sprite should hit left side of bg tile 5) Sprite should miss right side of bg tile 6) Sprite should hit right side of bg tile 7) Sprite should miss top of bg tile 8) Sprite should hit top of bg tile 9) Sprite should miss bottom of bg tile 10) Sprite should hit bottom of bg tile 03.corners ---------- Tests sprite 0 hit using a sprite with a single pixel set, for each of the four corners. 2) Lower-right pixel should hit 3) Lower-left pixel should hit 4) Upper-right pixel should hit 5) Upper-left pixel should hit 04.flip ------- Tests sprite 0 hit for single pixel sprite and background. 2) Horizontal flipping doesn't work 3) Vertical flipping doesn't work 4) Horizontal + Vertical flipping doesn't work 05.left_clip ------------ Tests sprite 0 hit with regard to clipping of left 8 pixels of screen. 2) Should miss when entirely in left-edge clipping 3) Left-edge clipping occurs when $2001 is not $1e 4) Left-edge clipping is off when $2001 = $1e 5) Left-edge clipping blocks all hits only when X = 0 6) Should miss; sprite pixel covered by left-edge clip 7) Should hit; sprite pixel outside left-edge clip 8) Should hit; sprite pixel outside left-edge clip 06.right_edge ------------- Tests sprite 0 hit with regard to column 255 (ignored) and off right edge of screen. 2) Should always miss when X = 255 3) Should hit; sprite has pixels < 255 4) Should miss; sprite pixel is at 255 5) Should hit; sprite pixel is at 254 6) Should also hit; sprite pixel is at 254 07.screen_bottom ---------------- Tests sprite 0 hit with regard to bottom of screen. 2) Should always miss when Y >= 239 3) Can hit when Y < 239 4) Should always miss when Y = 255 5) Should hit; sprite pixel is at 238 6) Should miss; sprite pixel is at 239 7) Should hit; sprite pixel is at 238 08.double_height ---------------- Tests basic sprite 0 hit double-height operation. 2) Lower sprite tile should miss bottom of bg tile 3) Lower sprite tile should hit bottom of bg tile 3) Lower sprite tile should miss top of bg tile 4) Lower sprite tile should hit top of bg tile 09.timing_basics ---------------- Tests sprite 0 hit timing to within 12 or so PPU clocks. Tests flag timing for upper-left corner, upper-right corner, lower-right corner, and time flag is cleared (at end of VBL). Depends on proper PPU frame length (less than 29781 CPU clocks). 2) Upper-left corner too soon 3) Upper-left corner too late 4) Upper-right corner too soon 5) Upper-right corner too late 6) Lower-left corner too soon 7) Lower-left corner too late 8) Cleared at end of VBL too soon 9) Cleared at end of VBL too late 10.timing_order --------------- Tests sprite 0 hit timing for which pixel it first reports hit on. Each test hits at the same location on screen, though different relative to the position of the sprite. 2) Upper-left corner too soon 3) Upper-left corner too late 4) Upper-right corner too soon 5) Upper-right corner too late 6) Lower-left corner too soon 7) Lower-left corner too late 8) Lower-right corner too soon 9) Lower-right corner too late 11.edge_timing -------------- Tests sprite 0 hit timing for which pixel it first reports hit on when some pixels are under clip, or at or beyond right edge. 2) Hit time shouldn't be based on pixels under left clip 3) Hit time shouldn't be based on pixels at X=255 4) Hit time shouldn't be based on pixels off right edge -- Shay Green (swap to e-mail) ================================================ FILE: tetanes-core/test_roms/ppu/spr_overflow.txt ================================================ NTSC NES PPU Sprite Overflow Flag Test ROMs ------------------------------------------- These ROMs test the sprite overflow flag in bit 5 of $2002. When run on a NES they all give a passing result. Each ROM runs several tests and reports the result on screen and by beeping a number of times. See below for the meaning of failure codes for each test. THE TESTS MUST BE RUN (*AND* *PASS*) IN ORDER, because some earlier ROMs test things that later ones assume will work properly. Source code for each test is included, and most tests are clearly divided into sections. Support code is also included, but it runs on a custom devcart and assembler so it will require some effort to assemble. Contact me if you'd like assistance porting them to your setup. 1.Basics -------- Tests basic operation of sprite overflow flag. 2) Should be set when 9 sprites are on a scanline 3) Reading $2002 shouldn't clear flag 4) Shouldn't be cleared at the beginning of VBL 5) Should be cleared at the end of VBL 6) Shouldn't be set when all rendering is off 7) Should work normally when $2001 = $08 (bg rendering only) 8) Should work normally when $2001 = $10 (sprite rendering only) 2.Details --------- Tests more detailed operation. 2) Should be set even when sprites are under left clip (X = 0) 3) Disabling rendering shouldn't clear flag 4) Should be cleared at the end of VBL even when rendering is off 5) Should be set when sprite Y coordinates are 239 6) Shouldn't be set when sprite Y coordinates are 240 (off screen) 7) Shouldn't be set when sprite Y coordinates are 255 (off screen) 8) Should be set regardless of which sprites are involved 9) Shouldn't be set when all scanlines have 7 or fewer sprites 10) Double-height sprites aren't handled properly 3.Timing -------- Tests timing of sprite overflow flag. The tests fail if timing is off by more than a CPU clock or two. 2) Cleared too late/3)too early at end of VBL 4) Set too early/5)too late for first scanline 6) Sprite horizontal positions should have no effect on timing 7) Set too early/8)late for last sprites on first scanline 9) Set too early/10)too late for last scanline 11) Set too early/12)too late when 9th sprite # is way after 8th 13) Overflow on second scanline occurs too early/14)too late 4.Obscure --------- Tests the pathological behavior when 8 sprites are on a scanline and the one just after the 8th is not on the scanline. In that case, the PPU interprets different bytes of each following sprite as the Y coordinate. For the following setup of any consecutive range of sprites (that is, sprite 1 below could be the PPU's 25th sprite, sprite 2 the 26th, etc.): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 If 1-8 are on the same scanline but 9 isn't, then the second byte of 10, the third byte of 11, fourth byte of 12, first byte of 13, second byte of 14, etc. are treated as those sprites' Y coordinates for the purpose of determining whether overflow occurs on that scanline. This search continues until one of the (erroneously interpreted) Y coordinates places the sprite within the scanline, or all sprites have been scanned. Refer to the NESdevWiki for further information about this behavior. 2) Checks that second byte of sprite #10 is treated as its Y 3) Checks that third byte of sprite #11 is treated as its Y 4) Checks that fourth byte of sprite #12 is treated as its Y 5) Checks that first byte of sprite #13 is treated as its Y 6) Checks that second byte of sprite #14 is treated as its Y 7) Checks that search stops at the last sprite without overflow 8) Same as test #2 but using a different range of sprites 5.Emulator ---------- Tests things that an emulator with predictive overflow flag handling is likely to get wrong. 2) Didn't calculate overflow when there was no $2002 read for frame 3) Disabling rendering didn't recalculate flag time 4) Changing sprite RAM didn't recalculate flag time 5) Changing sprite height didn't recalculate time -- Shay Green ================================================ FILE: tetanes-core/test_roms/ppu/tests.json ================================================ [ { "name": "ntsc_torture", "frames": [ { "number": 0, "action": { "SetVideoFilter": "Ntsc" } }, { "number": 10, "hash": 6478544483085266183 }, { "number": 11, "hash": 13464778097346924262 } ] }, { "name": "oam_read", "frames": [ { "number": 40, "hash": 11005166623255668487 } ] }, { "name": "oam_stress", "frames": [ { "number": 1709, "hash": 987545031310641328 } ] }, { "name": "open_bus", "frames": [ { "number": 260, "hash": 11491804135423135712 } ] }, { "name": "palette_ram", "frames": [ { "number": 20, "hash": 951336095730925361 } ] }, { "name": "read_buffer", "frames": [ { "number": 1350, "hash": 11280523076083011669 } ] }, { "name": "spr_hit_alignment", "frames": [ { "number": 50, "hash": 8064486199559266391 } ] }, { "name": "spr_hit_basics", "frames": [ { "number": 50, "hash": 2843100842160455037 } ] }, { "name": "spr_hit_corners", "frames": [ { "number": 35, "hash": 10774006242499790677 } ] }, { "name": "spr_hit_double_height", "frames": [ { "number": 30, "hash": 10299510358892663483 } ] }, { "name": "spr_hit_flip", "frames": [ { "number": 30, "hash": 3446883211880305229 } ] }, { "name": "spr_hit_left_clip", "frames": [ { "number": 50, "hash": 15407546689908417177 } ] }, { "name": "spr_hit_right_edge", "frames": [ { "number": 40, "hash": 15660187972799557083 } ] }, { "name": "spr_hit_screen_bottom", "frames": [ { "number": 40, "hash": 7853612209953886180 } ] }, { "name": "spr_hit_timing_basics", "frames": [ { "number": 70, "hash": 5150672749382335376 } ] }, { "name": "spr_hit_timing_order", "frames": [ { "number": 75, "hash": 14951931037155501762 } ] }, { "name": "spr_hit_edge_timing", "frames": [ { "number": 80, "hash": 10213563398947658894 } ] }, { "name": "spr_overflow_basics", "frames": [ { "number": 20, "hash": 4512213194270824995 } ] }, { "name": "spr_overflow_details", "frames": [ { "number": 30, "hash": 7453336004261551379 } ] }, { "name": "spr_overflow_emulator", "frames": [ { "number": 20, "hash": 10339336858702076057 } ] }, { "name": "spr_overflow_obscure", "frames": [ { "number": 22, "hash": 7930864775209885475 } ] }, { "name": "spr_overflow_timing", "frames": [ { "number": 141, "hash": 14539354705593009798 } ] }, { "name": "sprite_ram", "frames": [ { "number": 30, "hash": 951336095730925361 } ] }, { "name": "vbl_nmi_basics", "frames": [ { "number": 150, "hash": 16562782044648304193 } ] }, { "name": "vbl_nmi_clear_timing", "frames": [ { "number": 120, "hash": 11463851157594709500 } ] }, { "name": "vbl_nmi_control", "frames": [ { "number": 35, "hash": 16934587399174399058 } ] }, { "name": "vbl_nmi_disable", "frames": [ { "number": 115, "hash": 1189096324891626908 } ] }, { "name": "vbl_nmi_even_odd_frames", "frames": [ { "number": 100, "hash": 15425322074654675101 } ] }, { "name": "vbl_nmi_even_odd_timing", "frames": [ { "number": 100, "hash": 0 } ] }, { "name": "vbl_nmi_frame_basics", "frames": [ { "number": 180, "hash": 382282417179176914 } ] }, { "name": "vbl_nmi_off_timing", "frames": [ { "number": 230, "hash": 2137824251974549592 } ] }, { "name": "vbl_nmi_on_timing", "frames": [ { "number": 210, "hash": 6158031964237457839 } ] }, { "name": "vbl_nmi_set_time", "frames": [ { "number": 200, "hash": 5554918882984212301 } ] }, { "name": "vbl_nmi_suppression", "frames": [ { "number": 165, "hash": 11565747234567092543 } ] }, { "name": "vbl_nmi_timing", "frames": [ { "number": 115, "hash": 12609704643621948295 } ] }, { "name": "vbl_timing", "frames": [ { "number": 153, "hash": 8016893805681573651 } ] }, { "name": "vram_access", "frames": [ { "number": 20, "hash": 951336095730925361 } ] }, { "name": "palette", "frames": [ { "number": 9, "name": "no_filter", "hash": 6997757400193095267 }, { "number": 10, "action": { "SetVideoFilter": "Ntsc" } }, { "number": 11, "name": "blue_green_red", "hash": 8071574123308103670 }, { "number": 12, "action": { "Joypad": [ "One", "Left" ] } }, { "number": 15, "name": "green_red", "hash": 13623772725600068525 }, { "number": 16, "action": { "Joypad": [ "One", "Left" ] } }, { "number": 18, "action": { "Joypad": [ "One", "Up" ] } }, { "number": 21, "name": "blue_red", "hash": 15501427923254063958 }, { "number": 22, "action": { "Joypad": [ "One", "Left" ] } }, { "number": 25, "name": "red", "hash": 10357388817602375796 }, { "number": 26, "action": { "Joypad": [ "One", "Left" ] } }, { "number": 28, "action": { "Joypad": [ "One", "Up" ] } }, { "number": 30, "action": { "Joypad": [ "One", "Right" ] } }, { "number": 33, "name": "blue_green", "hash": 18249686492888207164 }, { "number": 34, "action": { "Joypad": [ "One", "Left" ] } }, { "number": 37, "name": "green", "hash": 14921198101292999649 }, { "number": 38, "action": { "Joypad": [ "One", "Left" ] } }, { "number": 40, "action": { "Joypad": [ "One", "Up" ] } }, { "number": 43, "name": "blue", "hash": 3927332191806565676 }, { "number": 44, "action": { "Joypad": [ "One", "Left" ] } }, { "number": 47, "name": "no_emphasis", "hash": 12673552706454460131 } ] }, { "name": "scanline", "frames": [ { "number": 10, "hash": 5656951408102020933 }, { "number": 11, "hash": 5898495566341282124 }, { "number": 12, "hash": 5656951408102020933 }, { "number": 13, "hash": 5656951408102020933 } ] }, { "name": "color", "frames": [ { "number": 10, "hash": 12341758549234391147 }, { "number": 12, "hash": 12341758549234391147 } ] }, { "name": "tv", "frames": [ { "number": 0, "action": { "SetVideoFilter": "Ntsc" } }, { "number": 10, "hash": 6270529304864431060 }, { "number": 11, "action": { "Joypad": [ "One", "Start" ] } }, { "number": 14, "hash": 18284547599417793041 }, { "number": 15, "hash": 16880644943449212899 } ] }, { "name": "_240pee", "frames": [ { "number": 30, "hash": 8240044214565782238 }, { "number": 32, "hash": 8240044214565782238 } ] } ] ================================================ FILE: tetanes-core/test_roms/ppu/tv.txt ================================================ TV pass or fail? This program is designed for NES and tests various aspects of the display it is connected to. Press the A Button to switch screens. _____________________________________________________________________ NTSC chroma/luma crosstalk The PPU in the PlayChoice arcade system generates RGB video, with red, green, and blue color information on separate cables. The PPU in the NES generates composite video, with chroma (color) and luma (brightness) information carried on one cable at different frequency bands. To keep the circuit cheap, it does not perform proper filtering to keep the chroma from bleeding into the luma. This especially has an effect on 45 degree diagonal lines. But an accurate emulator must preserve the same artifacts, as games such as Blaster Master rely on them to create the richest color palette. This screen displays something noticeably different on an NTSC NES PPU vs. the RGB PPU that most PC based NES emulators emulate. Display on RGB system: Display on NTSC system: ,---------. ,---------. | ===== | | ===== | |%%%%%%%%%| | PASS! | | | | | | PRESS A | | PRESS A | `---------' `---------' _____________________________________________________________________ Pixel aspect ratio PC displays most commonly generate square pixels. A square pixel on an NTSC display is 7/12 of a chroma cycle wide, but the NES PPU did not generate square pixels. Instead, it generated pixels 8/12 of a chroma cycle wide, which are somewhat wider than they are tall. This made games' graphics appear stretched. If they are displayed with square pixels on a PC based emulator, graphics will not appear with the intended proportions. This screen shows three rectangles. One is a square on NTSC NES and PlayChoice, one is a square on PAL NES, and one is a square with square pixels. _____________________________________________________________________ Legal Copyright 2007 Damian Yerrick Do not distribute this quick and dirty preview version to the public until it has been tested on an NES. ================================================ FILE: tetanes-core/test_roms/ppu/vbl_nmi.txt ================================================ NES PPU Tests ------------- These tests verify the behavior and timing of the NTSC PPU's VBL flag, NMI enable, and NMI interrupt. Timing is tested to an accuracy of one PPU clock. Note that often the NES starts up with a different value in the clock divider, causing PPU timing to be slightly different and fail some of the tests. These test the timings that have been most fully documented and emulated. 01-vbl_basics ------------- Tests basic VBL operation and VBL period. 2) VBL period is way off 3) Reading VBL flag should clear it 4) Writing $2002 shouldn't affect VBL flag 5) $2002 should be mirrored at $200A 6) $2002 should be mirrored every 8 bytes up to $2FFA 7) VBL period is too short with BG off 8) VBL period is too long with BG off 02-vbl_set_time --------------- Verifies time VBL flag is set. Reads $2002 twice and prints VBL flags from them. Test is run one PPU clock later each time, around the time the flag is set. 00 - V 01 - V 02 - V 03 - V ; after some resets this is - - 04 - - ; flag setting is suppressed 05 V - 06 V - 07 V - 08 V - 03-vbl_clear_time ----------------- Tests time VBL flag is cleared. Reads $2002 and prints VBL flag. Test is run one PPU clock later each line, around the time the flag is cleared. 00 V 01 V 02 V 03 V 04 V 05 V 06 - 07 - 08 - 04-nmi_control -------------- Tests immediate NMI behavior when enabling while VBL flag is already set 2) Shouldn't occur when disabled 3) Should occur when enabled and VBL begins 4) $2000 should be mirrored every 8 bytes 5) Should occur immediately if enabled while VBL flag is set 6) Shouldn't occur if enabled while VBL flag is clear 7) Shouldn't occur again if writing $80 when already enabled 8) Shouldn't occur again if writing $80 when already enabled 2 9) Should occur again if enabling after disabled 10) Should occur again if enabling after disabled 2 11) Immediate occurence should be after NEXT instruction 05-nmi_timing ------------- Tests NMI timing. Prints which instruction NMI occurred after. Test is run one PPU clock later each line. 00 4 01 4 02 4 03 3 04 3 05 3 06 3 07 3 08 3 09 2 06-suppression -------------- Tests behavior when $2002 is read near time VBL flag is set. Reads $2002 one PPU clock later each time. Prints whether VBL flag read back as set, and whether NMI occurred. 00 - N 01 - N 02 - N 03 - N ; normal behavior 04 - - ; flag never set, no NMI 05 V - ; flag read back as set, but no NMI 06 V - 07 V N ; normal behavior 08 V N 09 V N 07-nmi_on_timing ---------------- Tests NMI occurrence when enabled near time VBL flag is cleared. Enables NMI one PPU clock later on each line. Prints whether NMI occurred. 00 N 01 N 02 N 03 N 04 N 05 - 06 - 07 - 08 - 08-nmi_off_timing ----------------- Tests NMI occurrence when disabled near time VBL flag is set. Disables NMI one PPU clock later on each line. Prints whether NMI occurred. 03 - 04 - 05 - 06 - 07 N 08 N 09 N 0A N 0B N 0C N 09-even_odd_frames ------------------ Tests clock skipped on every other PPU frame when BG rendering is enabled. Tries pattern of BG enabled/disabled during a sequence of 5 frames, then finds how many clocks were skipped. Prints number skipped clocks to help find problems. Correct output: 00 01 01 02 10-even_odd_timing ------------------ Tests timing of skipped clock every other frame when BG is enabled. Output: 08 08 09 07 2) Clock is skipped too soon, relative to enabling BG 3) Clock is skipped too late, relative to enabling BG 4) Clock is skipped too soon, relative to disabling BG 5) Clock is skipped too late, relative to disabling BG Multi-tests ----------- The NES/NSF builds in the main directory consist of multiple sub-tests. When run, they list the subtests as they are run. The final result code refers to the first sub-test that failed. For more information about any failed subtests, run them individually from rom_singles/ and nsf_singles/. Flashes, clicks, other glitches ------------------------------- If a test prints "passed", it passed, even if there were some flashes or odd sounds. Only a test which prints "done" at the end requires that you watch/listen while it runs in order to determine whether it passed. Such tests involve things which the CPU cannot directly test. Alternate output ---------------- Tests generally print information on screen, but also report the final result audibly, and output text to memory, in case the PPU doesn't work or there isn't one, as in an NSF or a NES emulator early in development. After the tests are done, the final result is reported as a series of beeps (see below). For NSF builds, any important diagnostic bytes are also reported as beeps, before the final result. Output at $6000 --------------- All text output is written starting at $6004, with a zero-byte terminator at the end. As more text is written, the terminator is moved forward, so an emulator can print the current text at any time. The test status is written to $6000. $80 means the test is running, $81 means the test needs the reset button pressed, but delayed by at least 100 msec from now. $00-$7F means the test has completed and given that result code. To allow an emulator to know when one of these tests is running and the data at $6000+ is valid, as opposed to some other NES program, $DE $B0 $G1 is written to $6001-$6003. Audible output -------------- A byte is reported as a series of tones. The code is in binary, with a low tone for 0 and a high tone for 1, and with leading zeroes skipped. The first tone is always a zero. A final code of 0 means passed, 1 means failure, and 2 or higher indicates a specific reason. See the source code of the test for more information about the meaning of a test code. They are found after the set_test macro. For example, the cause of test code 3 would be found in a line containing set_test 3. Examples: Tones Binary Decimal Meaning - - - - - - - - - - - - - - - - - - - - low 0 0 passed low high 01 1 failed low high low 010 2 error 2 NSF versions ------------ Many NSF-based tests require that the NSF player either not interrupt the init routine with the play routine, or if it does, not interrupt the play routine again if it hasn't returned yet. This is because many tests need to run for a while without returning. NSF versions also make periodic clicks to prevent the NSF player from thinking the track is silent and thus ending the track before it's done testing. -- Shay Green ================================================ FILE: tetanes-core/test_roms/ppu/vbl_nmi_timing.txt ================================================ NTSC NES PPU VBL/NMI Timing Tests --------------------------------- These ROMs test the timing of the VBL flag and NMI to an accuracy of a single PPU clock, and also check special cases. They have been tested on an actual NES and all give a passing result. Sometimes the NES starts up with a different PPU timing that causes some of the tests to fail; these tests don't check that timing arrangement. Each ROM runs several tests and reports the result on screen and by beeping a number of times. See below for the meaning of failure codes for each test. It's best to run the tests in order, because later ROMs depend on things tested by earlier ROMs and will give erroneous results if any earlier ones failed. Source code for each test is included, and most tests are clearly divided into sections. Support code is also included, but it runs on a custom devcart and assembler so it will require some effort to assemble. Contact me if you'd like assistance porting them to your setup. 1.frame_basics -------------- Tests basic VBL flag operation and general timing of PPU frames. 2) VBL flag isn't being set 3) VBL flag should be cleared after being read 4) PPU frame with BG enabled is too short 5) PPU frame with BG enabled is too long 6) PPU frame with BG disabled is too short 7) PPU frame with BG disabled is too long 2.vbl_timing ------------ Tests timing of VBL being set, and special case where reading VBL flag as it would be set causes it to not be set for that frame. 2) Flag should read as clear 3 PPU clocks before VBL 3) Flag should read as set 0 PPU clocks after VBL 4) Flag should read as clear 2 PPU clocks before VBL 5) Flag should read as set 1 PPU clock after VBL 6) Flag should read as clear 1 PPU clock before VBL 7) Flag should read as set 2 PPU clocks after VBL 8) Reading 1 PPU clock before VBL should suppress setting 3.even_odd_frames ----------------- Test clock skipped when BG is enabled on odd PPU frames. Tests enable/disable BG during 5 consecutive frames, then see how many clocks were skipped. Patterns are shown as XXXXX, where each X can either be B (BG enabled) or - (BG disabled). 2) Pattern ----- should not skip any clocks 3) Pattern BB--- should skip 1 clock 4) Pattern B--B- (one even, one odd) should skip 1 clock 5) Pattern -B--B (one odd, one even) should skip 1 clock 6) Pattern BB-BB (two pairs) should skip 2 clocks 4.vbl_clear_timing ------------------ Tests timing of VBL flag clearing. 2) Cleared 3 or more PPU clocks too early 3) Cleared 2 PPU clocks too early 4) Cleared 1 PPU clock too early 5) Cleared 3 or more PPU clocks too late 6) Cleared 2 PPU clocks too late 7) Cleared 1 PPU clock too late 5.nmi_suppression ----------------- Tests timing of NMI suppression when reading VBL flag just as it's set, and that this doesn't occur when reading one clock before or after. 2) Reading flag 3 PPU clocks before set shouldn't suppress NMI 3) Reading flag when it's set should suppress NMI 4) Reading flag 3 PPU clocks after set shouldn't suppress NMI 5) Reading flag 2 PPU clocks before set shouldn't suppress NMI 6) Reading flag 1 PPU clock after set should suppress NMI 7) Reading flag 4 PPU clocks after set shouldn't suppress NMI 8) Reading flag 4 PPU clocks before set shouldn't suppress NMI 9) Reading flag 1 PPU clock before set should suppress NMI 10)Reading flag 2 PPU clocks after set shouldn't suppress NMI 432101234 ---+?+--- 6.nmi_disable ------------- Tests NMI occurrence when disabling NMI just as VBL flag is set, and just after. 2) NMI shouldn't occur when disabled 0 PPU clocks after VBL 3) NMI should occur when disabled 3 PPU clocks after VBL 4) NMI shouldn't occur when disabled 1 PPU clock after VBL 5) NMI should occur when disabled 4 PPU clocks after VBL 6) NMI shouldn't occur when disabled 1 PPU clock before VBL 7) NMI should occur when disabled 2 PPU clocks after VBL 7.nmi_timing ------------ Tests timing of NMI and immediate occurrence when enabled with VBL flag already set. 2) NMI occurred 3 or more PPU clocks too early 3) NMI occurred 2 PPU clocks too early 4) NMI occurred 1 PPU clock too early 5) NMI occurred 3 or more PPU clocks too late 6) NMI occurred 2 PPU clocks too late 7) NMI occurred 1 PPU clock too late 8) NMI should occur if enabled when VBL already set 9) NMI enabled when VBL already set should delay 1 instruction 10)NMI should be possible multiple times in VBL -- Shay Green (swap to e-mail) ================================================ FILE: tetanes-utils/Cargo.toml ================================================ [package] name = "tetanes-utils" version.workspace = true edition.workspace = true license.workspace = true authors.workspace = true readme.workspace = true repository.workspace = true homepage.workspace = true publish = false [[bin]] name = "list_boards" test = false bench = false [[bin]] name = "generate_db" test = false bench = false [dependencies] anyhow.workspace = true clap.workspace = true tetanes-core.workspace = true ================================================ FILE: tetanes-utils/src/bin/generate_db.rs ================================================ use anyhow::Context; use clap::Parser; use std::{ env, ffi::OsStr, fs::File, io::{BufWriter, Write}, path::{Path, PathBuf}, }; use tetanes_core::{ cart::{Cart, GameInfo}, common::NesRegion, fs, mem::RamState, ppu::Mirroring, }; const GAME_DB_TXT: &str = "tetanes-core/game_database.txt"; const GAME_DB: &str = "tetanes-core/game_db.dat"; fn main() -> anyhow::Result<()> { let opt = Opt::parse(); let path = opt .path .unwrap_or_else(|| env::current_dir().unwrap_or_default()); let header = "# CRC, Region, Mapper, Sub-Mapper, ChrBanks, PrgRomBanks, PrgRamBanks, Battery, Mirroring, SubMapper, Title"; if path.is_dir() { let mut db_txt_file = BufWriter::new( File::create(GAME_DB_TXT).with_context(|| format!("failed to open {GAME_DB_TXT}"))?, ); let mut games = path .read_dir() .unwrap_or_else(|err| panic!("unable read directory {path:?}: {err}")) .filter_map(Result::ok) .filter(|f| f.path().extension() == Some(OsStr::new("nes"))) .map(|f| f.path()) .map(Game::new) .filter_map(Result::ok) .collect::>(); games.sort_by_key(|game| game.crc32); let mut entries = Vec::with_capacity(games.len()); writeln!(db_txt_file, "{header}")?; for game in &mut games { apply_corrections(game); let Game { crc32, region, mapper, submapper, chr_banks, prg_rom_banks, prg_ram_banks, battery, mirroring, title, } = game; writeln!( db_txt_file, " {crc32:8X}, {region}, {mapper}, {submapper}, {chr_banks}, {prg_rom_banks}, {prg_ram_banks}, {battery}, {mirroring:?}, {title:?}", )?; entries.push(GameInfo { crc32: *crc32, region: *region, mapper_num: *mapper, submapper_num: *submapper, }); } fs::save(GAME_DB, &entries)?; } else if path.is_file() { todo!("adding individual games is not yet supported"); } Ok(()) } fn apply_corrections(game: &mut Game) { match game.crc32 { // Mapper 210 games incorrectly marked as Mapper 19 0x808606F0 | 0x81B7F1A8 | 0xC247CC80 | 0xC47946D => { // Famista '91 // Heisei Tensai Bakabon // Family Circuit '91 // Chibi Maruko-chan: Uki Uki Shopping // Dream Master - TODO: Missing crc game.mapper = 210; game.submapper = 1; } 0x1DC0F740 | 0x429103C9 | 0x46FD7843 | 0x47232739 | 0x6EC51DE5 | 0xADFFD64F | 0xD323B806 => { // Famista '92 // Famista '93 // Famista '94 // Splatterhouse: Wanpaku Graffiti // Top Striker // Wagyan Land 2 // Wagyan Land 3 game.mapper = 210; game.submapper = 2; } 0x5CAA3E61 => { // Death Race: game.mapper = 144; } 0xD1691028 => { // Devil Man: game.mirroring = Mirroring::Horizontal; game.mapper = 154; } _ => (), } } #[derive(Debug)] #[must_use] pub struct Game { crc32: u32, region: NesRegion, mapper: u16, submapper: u8, chr_banks: usize, prg_rom_banks: usize, prg_ram_banks: usize, battery: bool, mirroring: Mirroring, title: String, } impl Game { fn new>(path: P) -> anyhow::Result { let path = path.as_ref(); let cart = Cart::from_path(path, RamState::default())?; let mut crc32 = fs::compute_crc32(&cart.prg_rom); if !cart.chr_rom.is_empty() { crc32 = fs::compute_combine_crc32(crc32, &cart.chr_rom); } let filename = path.file_name().unwrap_or_default(); let region = match filename.to_str() { Some(filename) => { if filename.contains("Europe") || filename.contains("PAL") { NesRegion::Pal } else { NesRegion::Ntsc } } None => NesRegion::Ntsc, }; let chr_size = cart.chr_size(); let chr_banks = chr_size / (8 * 1024); let prg_rom_banks = cart.prg_rom_size / (16 * 1024); let prg_ram_banks = cart.prg_ram_size / (16 * 1024); let mirroring = cart.mirroring(); Ok(Game { crc32, region, mapper: cart.mapper_num(), submapper: cart.submapper_num(), chr_banks, prg_rom_banks, prg_ram_banks, battery: cart.battery_backed(), mirroring, title: filename.to_string_lossy().to_string(), }) } } #[derive(Parser, Debug)] #[must_use] struct Opt { /// The NES ROM or a directory containing `.nes` ROM files. [default: current directory] path: Option, } ================================================ FILE: tetanes-utils/src/bin/list_boards.rs ================================================ use clap::Parser; use std::{ env, ffi::OsStr, path::{Path, PathBuf}, }; use tetanes_core::{cart::Cart, mem::RamState}; fn main() -> anyhow::Result<()> { let opt = Opt::parse(); let path = opt .path .unwrap_or_else(|| env::current_dir().unwrap_or_default()); let board = opt.board.map(|b| b.to_lowercase()); if path.is_dir() { let paths: Vec = path .read_dir() .unwrap_or_else(|err| panic!("unable read directory {path:?}: {err}")) .filter_map(Result::ok) .filter(|f| f.path().extension() == Some(OsStr::new("nes"))) .map(|f| f.path()) .collect(); let mut boards: Vec = paths .iter() .map(get_mapper) .filter_map(Result::ok) .filter(|b| match &board { Some(board) => b.to_lowercase().contains(board), None => true, }) .collect(); boards.sort(); for board in &boards { println!("{board}"); } } else if path.is_file() { println!("{}", get_mapper(&path)?); } Ok(()) } fn get_mapper>(path: P) -> anyhow::Result { let cart = Cart::from_path(path, RamState::default())?; Ok(format!("{:<50} {:?}", cart.mapper_board(), cart.name())) } #[derive(Parser, Debug)] #[must_use] struct Opt { /// The NES ROM or a directory containing `.nes` ROM files. [default: current directory] path: Option, /// The NES Mapper Board to filter by. board: Option, } ================================================ FILE: vendored/linuxdeploy-x86_64.AppImage ================================================ [File too large to display: 12.8 MB]