From 30131cd32c1c4551a710d0e7b6521085d9f33ef3 Mon Sep 17 00:00:00 2001 From: ZengGengSen <54354608+ZengGengSen@users.noreply.github.com> Date: Thu, 20 Aug 2026 23:35:50 +0800 Subject: [PATCH] Fc/vs unisystem support (#2594) ## Overview Adds Nintendo Vs. UniSystem support to the ares FC core. Games remain in MAME ZIP format and load through the existing Arcade path, without going through the iNES detection path used by regular Famicom software. The implementation covers MAME metadata generation, parent ZIP lookup, Vs. PPU and palette behavior, cartridge boards, protection logic, cabinet I/O, DIP switches, controller wiring, Zapper input, and save states. A total of 41 Vs. UniSystem sets have been validated. Vs. DualSystem, non-standard bootleg hardware, and other configurations that have not yet been implemented are explicitly marked as unsupported instead of being started with the UniSystem model. ## Why `vsnes.py` is separate MAME groups three substantially different hardware families in `nintendo/vsnes.cpp`: - Vs. UniSystem with one CPU and one PPU; - Vs. DualSystem with two CPUs and two PPUs; - non-standard bootleg hardware built around a Z80 and SN76489. The generic MAME parser reads common data such as machine entries, ROM regions, and DIP switches. Those fields alone are not enough to determine which Vs. hardware model a game requires. The three families have different runtime structures and cannot share a single emulation path. Configurations also differ between Vs. UniSystem games. The PPU, palette, cartridge board, protection logic, controller wiring, Zapper support, and DIP settings may all vary by set. The core needs explicit configuration data rather than inferring hardware from a game title. `vsnes.py` reuses the MAME XML and ROM-region information produced by the generic parser, then adds hardware classification and per-game configuration. It generates a deterministic `VsSystem.bml` database. At runtime, supported sets select the corresponding FC hardware model from this metadata, while DualSystem and bootleg sets are rejected with a clear explanation. ## Video The combined video shows the attract modes of all 41 supported games. Each game is shown for approximately ten seconds, with its name in the upper-right corner. https://github.com/user-attachments/assets/46a59b2b-f64a-40ce-a870-1351d5df3948 https://github.com/user-attachments/assets/7927d1d0-4349-4f4b-9092-feeb7b0ac127 https://github.com/user-attachments/assets/d162c834-b227-497b-afe7-2f766f3ab42a https://github.com/user-attachments/assets/12d7b58e-7cd5-4bbf-b040-9d615e654e0c https://github.com/user-attachments/assets/a8708059-4369-48e1-be1f-42d15099d7a8 ## Screenshots The contact sheet contains one attract-mode screenshot for each game, composed at the original capture resolution. contact-sheet ## Supported sets The following 41 Vs. UniSystem MAME sets have been validated for loading and attract-mode operation: | No. | MAME short name | Game | | ---: | --- | --- | | 1 | `btlecity` | Vs. Battle City | | 2 | `cluclu` | Vs. Clu Clu Land | | 3 | `cstlevna` | Vs. Castlevania | | 4 | `drmario` | Vs. Dr. Mario | | 5 | `duckhunt` | Vs. Duck Hunt (set DH3 E) | | 6 | `excitebk` | Vs. Excitebike (set EB4-3 E) | | 7 | `excitebkj` | Vs. Excitebike (set EB4-4 A) | | 8 | `excitebko` | Vs. Excitebike (set EB4-3 ?) | | 9 | `goonies` | Vs. The Goonies (set E) | | 10 | `hogalley` | Vs. Hogan's Alley (set HA4-1 E-1) | | 11 | `iceclimb` | Vs. Ice Climber (set IC4-4 B-1) | | 12 | `iceclimba` | Vs. Ice Climber (set IC4-4 ?) | | 13 | `jajamaru` | Vs. Ninja Jajamaru Kun (Japan) | | 14 | `ladygolf` | Vs. Stroke & Match Golf (Ladies Version, set LG4 ?) | | 15 | `ladygolfe` | Vs. Stroke & Match Golf (Ladies Version, set LG4 E) | | 16 | `nvs_machrider` | Vs. Mach Rider (Endurance Course Version) | | 17 | `nvs_machridera` | Vs. Mach Rider (Fighting Course Version, set MR4-1 A) | | 18 | `nvs_mightybj` | Vs. Mighty Bomb Jack (Japan) | | 19 | `nvs_platoon` | Vs. Platoon | | 20 | `rbibb` | Vs. Atari R.B.I. Baseball (set 1) | | 21 | `rbibba` | Vs. Atari R.B.I. Baseball (set 2) | | 22 | `skatekds` | Vs. Skate Kids. (Graphic hack of Super Mario Bros.) | | 23 | `smgolf` | Vs. Stroke & Match Golf (Men Version, set GF4-2 F) | | 24 | `smgolfb` | Vs. Stroke & Match Golf (Men Version, set GF4-2 ?) | | 25 | `smgolfj` | Vs. Stroke & Match Golf (Men Version) (Japan, set GF3 B) | | 26 | `starlstr` | Vs. Star Luster | | 27 | `suprmrio` | Vs. Super Mario Bros. (set SM4-4 E) | | 28 | `suprmrioa` | Vs. Super Mario Bros. (set ?, harder) | | 29 | `supxevs` | Vs. Super Xevious | | 30 | `tkoboxng` | Vs. T.K.O. Boxing | | 31 | `topgun` | Vs. Top Gun | | 32 | `vsfdf` | Vs. Freedom Force | | 33 | `vsgradus` | Vs. Gradius (US, set GR E) | | 34 | `vsgshoe` | Vs. Gumshoe (set GM5) | | 35 | `vspinbal` | Vs. Pinball (US, set PN4 E-1) | | 36 | `vspinbalj` | Vs. Pinball (Japan, set PN3 B) | | 37 | `vsskykid` | Vs. Super SkyKid | | 38 | `vsslalom` | Vs. Slalom | | 39 | `vssoccer` | Vs. Soccer (set SC4-2 A) | | 40 | `vssoccera` | Vs. Soccer (set SC4-3 ?) | | 41 | `vstetris` | Vs. Tetris | --- ares/ares/resource/resource.bml | 2 + ares/ares/resource/sprite/fc/crosshair.png | Bin 0 -> 200 bytes ares/fc/CMakeLists.txt | 32 +- ares/fc/cartridge/board/board.cpp | 4 + ares/fc/cartridge/board/vs-unisystem.cpp | 78 + ares/fc/cartridge/board/vs/board.cpp | 31 + ares/fc/cartridge/board/vs/board.hpp | 62 + ares/fc/cartridge/board/vs/mmc1.cpp | 73 + ares/fc/cartridge/board/vs/namco-108.cpp | 42 + ares/fc/cartridge/board/vs/protection.hpp | 88 + ares/fc/cartridge/board/vs/standard.cpp | 35 + ares/fc/cartridge/board/vs/sunsoft-3.cpp | 40 + ares/fc/cartridge/board/vs/unrom.cpp | 27 + ares/fc/cartridge/board/vs/vrc1.cpp | 33 + ares/fc/cartridge/cartridge.cpp | 1 + ares/fc/controller/controller.hpp | 1 + ares/fc/controller/port.hpp | 1 + ares/fc/controller/zapper/zapper.cpp | 133 +- ares/fc/controller/zapper/zapper.hpp | 35 +- ares/fc/cpu/memory.cpp | 3 + ares/fc/fc.cpp | 1 + ares/fc/fc.hpp | 1 + ares/fc/ppu/color.cpp | 50 - ares/fc/ppu/memory.cpp | 9 +- ares/fc/ppu/model.cpp | 209 + ares/fc/ppu/model.hpp | 40 + ares/fc/ppu/ppu.cpp | 31 +- ares/fc/ppu/ppu.hpp | 21 +- ares/fc/ppu/render.cpp | 14 +- ares/fc/system/serialization.cpp | 14 +- ares/fc/system/system.cpp | 39 +- ares/fc/system/system.hpp | 3 + ares/fc/vs/controls.cpp | 42 + ares/fc/vs/dip-switches.cpp | 116 + ares/fc/vs/io.cpp | 60 + ares/fc/vs/serialization.cpp | 29 + ares/fc/vs/standard-input.cpp | 65 + ares/fc/vs/unisystem.cpp | 43 + ares/fc/vs/unisystem.hpp | 135 + ares/fc/vs/zapper-input.cpp | 27 + desktop-ui/emulator/arcade.cpp | 39 +- desktop-ui/emulator/emulator.cpp | 4 + desktop-ui/emulator/emulator.hpp | 6 + desktop-ui/game-browser/game-browser.cpp | 57 +- desktop-ui/game-browser/game-browser.hpp | 5 +- mia/Database/VsSystem.bml | 6443 ++++++++++++++++++++ mia/medium/arcade.cpp | 204 +- mia/medium/mame.cpp | 383 +- mia/medium/medium.hpp | 2 +- mia/medium/neo-geo.cpp | 46 +- mia/mia.hpp | 1 + tools/mamedb/.gitignore | 2 + tools/mamedb/__init__.py | 1 + tools/mamedb/__main__.py | 89 + tools/mamedb/mame.py | 191 + tools/mamedb/vsnes.py | 368 ++ 56 files changed, 9241 insertions(+), 270 deletions(-) create mode 100644 ares/ares/resource/sprite/fc/crosshair.png create mode 100644 ares/fc/cartridge/board/vs-unisystem.cpp create mode 100644 ares/fc/cartridge/board/vs/board.cpp create mode 100644 ares/fc/cartridge/board/vs/board.hpp create mode 100644 ares/fc/cartridge/board/vs/mmc1.cpp create mode 100644 ares/fc/cartridge/board/vs/namco-108.cpp create mode 100644 ares/fc/cartridge/board/vs/protection.hpp create mode 100644 ares/fc/cartridge/board/vs/standard.cpp create mode 100644 ares/fc/cartridge/board/vs/sunsoft-3.cpp create mode 100644 ares/fc/cartridge/board/vs/unrom.cpp create mode 100644 ares/fc/cartridge/board/vs/vrc1.cpp delete mode 100644 ares/fc/ppu/color.cpp create mode 100644 ares/fc/ppu/model.cpp create mode 100644 ares/fc/ppu/model.hpp create mode 100644 ares/fc/vs/controls.cpp create mode 100644 ares/fc/vs/dip-switches.cpp create mode 100644 ares/fc/vs/io.cpp create mode 100644 ares/fc/vs/serialization.cpp create mode 100644 ares/fc/vs/standard-input.cpp create mode 100644 ares/fc/vs/unisystem.cpp create mode 100644 ares/fc/vs/unisystem.hpp create mode 100644 ares/fc/vs/zapper-input.cpp create mode 100644 mia/Database/VsSystem.bml create mode 100644 tools/mamedb/.gitignore create mode 100644 tools/mamedb/__init__.py create mode 100644 tools/mamedb/__main__.py create mode 100644 tools/mamedb/mame.py create mode 100644 tools/mamedb/vsnes.py diff --git a/ares/ares/resource/resource.bml b/ares/ares/resource/resource.bml index 1b4f14c1a..91430f149 100644 --- a/ares/ares/resource/resource.bml +++ b/ares/ares/resource/resource.bml @@ -5,6 +5,8 @@ namespace name=Resource binary name=Logo1x file=logo.png binary name=Logo2x file=logo@2x.png namespace name=Sprite + namespace name=Famicom + binary name=Crosshair file=sprite/fc/crosshair.png namespace name=SuperFamicom binary name=CrosshairBlue file=sprite/sfc/crosshair-blue.png binary name=CrosshairGreen file=sprite/sfc/crosshair-green.png diff --git a/ares/ares/resource/sprite/fc/crosshair.png b/ares/ares/resource/sprite/fc/crosshair.png new file mode 100644 index 0000000000000000000000000000000000000000..2c8c40372bc6ae5ee9f7b91346460f128187c787 GIT binary patch literal 200 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc!B|fh#}EtuslAST42m4gKmV`(mi~@YdFd4^ z&dyt!Zay^|Pu)lmbZ@%!Hco=ssrYG{)93h8C*N(!We=OiE^@wnYUHF-D<<5ysiNuS s{`bg&x@v(Qy}W&QH8p*clf~+nT%EMGec_)EK#LeWUHx3vIVCg!0EMST{{R30 literal 0 HcmV?d00001 diff --git a/ares/fc/CMakeLists.txt b/ares/fc/CMakeLists.txt index 00608060d..82e82f2e4 100644 --- a/ares/fc/CMakeLists.txt +++ b/ares/fc/CMakeLists.txt @@ -14,6 +14,7 @@ ares_add_sources( UNITY # fc.cpp PRIMARY # + vs/unisystem.cpp system/system.cpp controller/controller.cpp expansion/expansion.cpp @@ -26,6 +27,18 @@ ares_add_sources( ares_add_sources(CORE fc INCLUDED fc.hpp CMakeLists.txt) +ares_add_sources( + CORE # + fc + INCLUDED # + vs/unisystem.hpp + vs/controls.cpp + vs/dip-switches.cpp + vs/io.cpp + vs/standard-input.cpp + vs/zapper-input.cpp +) + ares_add_sources( CORE # fc @@ -121,6 +134,22 @@ ares_add_sources( cartridge/board/unl-sc127.cpp cartridge/board/unl-txc.cpp cartridge/board/unrom-512.cpp + cartridge/board/vs-unisystem.cpp +) + +ares_add_sources( + CORE # + fc + INCLUDED # + cartridge/board/vs/board.cpp + cartridge/board/vs/board.hpp + cartridge/board/vs/mmc1.cpp + cartridge/board/vs/namco-108.cpp + cartridge/board/vs/protection.hpp + cartridge/board/vs/standard.cpp + cartridge/board/vs/sunsoft-3.cpp + cartridge/board/vs/unrom.cpp + cartridge/board/vs/vrc1.cpp ) ares_add_sources( @@ -212,9 +241,10 @@ ares_add_sources( CORE # fc INCLUDED # - ppu/color.cpp ppu/debugger.cpp ppu/memory.cpp + ppu/model.cpp + ppu/model.hpp ppu/ppu.hpp ppu/render.cpp ppu/scroll.cpp diff --git a/ares/fc/cartridge/board/board.cpp b/ares/fc/cartridge/board/board.cpp index 7212f6c8e..23fa73eae 100644 --- a/ares/fc/cartridge/board/board.cpp +++ b/ares/fc/cartridge/board/board.cpp @@ -57,6 +57,8 @@ namespace Board { #include "taito-x1-017.cpp" #include "tengen-80003x.cpp" #include "unrom-512.cpp" +#include "vs/board.cpp" +#include "vs-unisystem.cpp" #include "unl-bmc.cpp" #include "unl-jy.cpp" @@ -132,6 +134,8 @@ auto Interface::create(string board) -> Interface* { if(!p) p = Sc127::create(board); if(!p) p = TXC::create(board); + if(!p) p = VsUniSystem::create(board); + if(!p) p = new Interface; return p; } diff --git a/ares/fc/cartridge/board/vs-unisystem.cpp b/ares/fc/cartridge/board/vs-unisystem.cpp new file mode 100644 index 000000000..03fad2d3b --- /dev/null +++ b/ares/fc/cartridge/board/vs-unisystem.cpp @@ -0,0 +1,78 @@ +struct VsUniSystem : Interface { + static auto create(string id) -> Interface* { + if(id == "nintendo/vs") return new VsUniSystem; + return nullptr; + } + +private: + Memory::Readable programROM; + Memory::Readable characterROM; + Memory::Writable characterRAM; + Memory::Writable workRAM; + Memory::Writable nametableRAM; + std::unique_ptr board; + std::unique_ptr protection; + n8 coinLatch; + +public: + auto load() -> void override { + Interface::load(programROM, "program.rom"); + Interface::load(characterROM, "character.rom"); + if(!characterROM) characterRAM.allocate(0x2000); + workRAM.allocate(0x0800, 0x00); + nametableRAM.allocate(0x1000); + + auto characterSize = characterROM ? characterROM.size() : characterRAM.size(); + board.reset(Vs::Interface::create(pak->attribute("mapper"), programROM.size(), characterSize)); + protection.reset(Vs::Protection::create(pak->attribute("protection"))); + } + + auto readPRG(n32 address, n8 data) -> n8 override { + if(address >= 0x4020 && address <= 0x5fff) return protection->readPRG(address, coinLatch); + if(address >= 0x6000 && address <= 0x7fff) return workRAM.read(address & 0x07ff); + if(address >= 0x8000) { + if(auto mapped = board->addressPRG(address)) return programROM.read(*mapped); + } + return data; + } + + auto writePRG(n32 address, n8 data) -> void override { + if(address == 0x4016) return board->writePRG(address, data); + if(address >= 0x4020 && address <= 0x5fff) { + coinLatch = data; + return; + } + if(address >= 0x6000 && address <= 0x7fff) return workRAM.write(address & 0x07ff, data); + if(address >= 0x8000) return board->writePRG(address, data); + } + + auto readCHR(n32 address, n8 data) -> n8 override { + if(address & 0x2000) return nametableRAM.read(address & 0x0fff); + if(auto mapped = board->addressCHR(address)) { + if(characterROM) return characterROM.read(*mapped); + if(characterRAM) return characterRAM.read(*mapped); + } + return data; + } + + auto writeCHR(n32 address, n8 data) -> void override { + if(address & 0x2000) return nametableRAM.write(address & 0x0fff, data); + if(!characterRAM) return; + if(auto mapped = board->addressCHR(address)) return characterRAM.write(*mapped, data); + } + + auto power() -> void override { + coinLatch = 0; + board->power(); + protection->power(); + } + + auto serialize(serializer& s) -> void override { + s(characterRAM); + s(workRAM); + s(nametableRAM); + s(coinLatch); + board->serialize(s); + protection->serialize(s); + } +}; diff --git a/ares/fc/cartridge/board/vs/board.cpp b/ares/fc/cartridge/board/vs/board.cpp new file mode 100644 index 000000000..5e122ce66 --- /dev/null +++ b/ares/fc/cartridge/board/vs/board.cpp @@ -0,0 +1,31 @@ +namespace Vs { + +#include "board.hpp" +#include "standard.cpp" +#include "vrc1.cpp" +#include "unrom.cpp" +#include "mmc1.cpp" +#include "namco-108.cpp" +#include "sunsoft-3.cpp" +#include "protection.hpp" + +auto Interface::create(string id, u32 programSize, u32 characterSize) -> Interface* { + if(id == "standard" && (programSize == 0x8000 || programSize == 0xa000)) { + return new Standard(programSize, characterSize); + } + if(id == "vrc1") return new VRC1(programSize, characterSize); + if(id == "unrom") return new UNROM(programSize, characterSize); + if(id == "mmc1") return new MMC1(programSize, characterSize); + if(id == "namco-108") return new Namco108(programSize, characterSize); + if(id == "sunsoft-3") return new Sunsoft3(programSize, characterSize); + return new Interface(programSize, characterSize); +} + +auto Protection::create(string id) -> Protection* { + if(id == "rbi-baseball") return new RBIBaseball; + if(id == "tko-boxing") return new TKOBoxing; + if(id == "super-xevious") return new SuperXevious; + return new Protection; +} + +} diff --git a/ares/fc/cartridge/board/vs/board.hpp b/ares/fc/cartridge/board/vs/board.hpp new file mode 100644 index 000000000..e6b9cde09 --- /dev/null +++ b/ares/fc/cartridge/board/vs/board.hpp @@ -0,0 +1,62 @@ +struct Interface { + Interface(u32 programSize, u32 characterSize) + : programBanks(programSize / 0x2000), characterBanks(characterSize / 0x0400) {} + virtual ~Interface() = default; + + static auto create(string id, u32 programSize, u32 characterSize) -> Interface*; + + virtual auto addressPRG(n32 address) -> maybe { return {}; } + virtual auto writePRG(n32 address, n8 data) -> void {} + virtual auto addressCHR(n32 address) -> maybe { return {}; } + virtual auto power() -> void {} + virtual auto serialize(serializer& s) -> void {} + +protected: + auto mapPRG(n32 bank, n32 address) -> maybe { + if(!programBanks) return {}; + return {(bank % programBanks) * 0x2000 | (address & 0x1fff)}; + } + + auto mapCHR(n32 bank, n32 address) -> maybe { + if(!characterBanks) return {}; + return {(bank % characterBanks) * 0x0400 | (address & 0x03ff)}; + } + + auto initializePRG(n8 banks[4]) -> void { + for(auto slot : range(4)) banks[slot] = 0; + if(programBanks < 4) return; + for(auto slot : range(4)) banks[slot] = programBanks - 4 + slot; + } + + auto initializeCHR(n8 banks[8]) -> void { + for(auto slot : range(8)) banks[slot] = slot; + } + + auto selectPRG8(n8 banks[4], u32 slot, n32 bank) -> void { + if(!programBanks) return; + banks[slot & 3] = bank & (programBanks - 1); + } + + auto selectPRG16(n8 banks[4], u32 slot, n32 bank) -> void { + if(!programBanks) return; + bank = bank * 2 & (programBanks - 1); + slot = (slot & 1) * 2; + banks[slot + 0] = bank + 0; + banks[slot + 1] = bank + 1; + } + + auto selectPRG32(n8 banks[4], n32 bank) -> void { + if(!programBanks) return; + bank = bank * 4 & (programBanks - 1); + for(auto slot : range(4)) banks[slot] = bank + slot; + } + + auto selectCHR(n8 banks[8], u32 start, u32 count, n32 bank) -> void { + if(!characterBanks) return; + bank &= characterBanks - 1; + for(auto slot : range(count)) banks[start + slot] = bank + slot; + } + + u32 programBanks; + u32 characterBanks; +}; diff --git a/ares/fc/cartridge/board/vs/mmc1.cpp b/ares/fc/cartridge/board/vs/mmc1.cpp new file mode 100644 index 000000000..b877b085f --- /dev/null +++ b/ares/fc/cartridge/board/vs/mmc1.cpp @@ -0,0 +1,73 @@ +struct MMC1 : Interface { + using Interface::Interface; + + auto addressPRG(n32 address) -> maybe override { + if(address < 0x8000) return {}; + return mapPRG(programBank[address >> 13 & 3], address); + } + + auto writePRG(n32 address, n8 data) -> void override { + if(address < 0x8000) return; + if(data.bit(7)) { + shiftCount = 0; + program16K = 1; + switchLow = 1; + return; + } + + shiftRegister = shiftRegister >> 1 | data.bit(0) << 4; + shiftCount = (shiftCount + 1) % 5; + if(shiftCount) return; + + switch(address >> 13 & 3) { + case 0: + character4K = shiftRegister.bit(4); + program16K = shiftRegister.bit(3); + switchLow = shiftRegister.bit(2); + break; + case 1: + if(character4K) selectCHR(characterBank, 0, 4, shiftRegister * 4); + else selectCHR(characterBank, 0, 8, (shiftRegister & ~1) * 4); + break; + case 2: + if(character4K) selectCHR(characterBank, 4, 4, shiftRegister * 4); + break; + case 3: + if(program16K) selectPRG16(programBank, !switchLow, shiftRegister); + else selectPRG32(programBank, shiftRegister >> 1); + break; + } + } + + auto addressCHR(n32 address) -> maybe override { + return mapCHR(characterBank[address >> 10 & 7], address); + } + + auto power() -> void override { + initializePRG(programBank); + initializeCHR(characterBank); + shiftRegister = 0; + shiftCount = 0; + program16K = 0; + switchLow = 0; + character4K = 0; + } + + auto serialize(serializer& s) -> void override { + s(programBank); + s(characterBank); + s(shiftRegister); + s(shiftCount); + s(program16K); + s(switchLow); + s(character4K); + } + + n8 programBank[4]; + n8 characterBank[8]; + n5 shiftRegister; + n3 shiftCount; + n1 program16K; + n1 switchLow; + n1 character4K; +}; diff --git a/ares/fc/cartridge/board/vs/namco-108.cpp b/ares/fc/cartridge/board/vs/namco-108.cpp new file mode 100644 index 000000000..3ff5e49e4 --- /dev/null +++ b/ares/fc/cartridge/board/vs/namco-108.cpp @@ -0,0 +1,42 @@ +struct Namco108 : Interface { + using Interface::Interface; + + auto addressPRG(n32 address) -> maybe override { + if(address < 0x8000) return {}; + return mapPRG(programBank[address >> 13 & 3], address); + } + + auto writePRG(n32 address, n8 data) -> void override { + if(address < 0x8000) return; + switch(address & 0x6001) { + case 0x0000: + bankSelect = data.bit(0, 2); + break; + case 0x0001: + if(bankSelect <= 1) selectCHR(characterBank, bankSelect * 2, 2, data); + if(bankSelect >= 2 && bankSelect <= 5) selectCHR(characterBank, bankSelect + 2, 1, data); + if(bankSelect >= 6) selectPRG8(programBank, bankSelect - 6, data); + break; + } + } + + auto addressCHR(n32 address) -> maybe override { + return mapCHR(characterBank[address >> 10 & 7], address); + } + + auto power() -> void override { + initializePRG(programBank); + initializeCHR(characterBank); + bankSelect = 0; + } + + auto serialize(serializer& s) -> void override { + s(programBank); + s(characterBank); + s(bankSelect); + } + + n8 programBank[4]; + n8 characterBank[8]; + n3 bankSelect; +}; diff --git a/ares/fc/cartridge/board/vs/protection.hpp b/ares/fc/cartridge/board/vs/protection.hpp new file mode 100644 index 000000000..46e4c41df --- /dev/null +++ b/ares/fc/cartridge/board/vs/protection.hpp @@ -0,0 +1,88 @@ +struct Protection { + virtual ~Protection() = default; + + static auto create(string id) -> Protection*; + + virtual auto readPRG(n32 address, n8 data) -> n8 { return data; } + virtual auto power() -> void {} + virtual auto serialize(serializer& s) -> void {} +}; + +struct RBIBaseball : Protection { + auto readPRG(n32 address, n8 data) -> n8 override { + if(address != 0x5600 && address != 0x5601 && address != 0x5e00 && address != 0x5e01) return data; + if(!(address & 1)) { + index = 0; + return 0; + } + return sequence[index++ & 0x1f]; + } + + auto power() -> void override { + index = 0; + } + + auto serialize(serializer& s) -> void override { + s(index); + } + + static constexpr u8 sequence[32] = { + 0xff, 0xfd, 0xf5, 0xf4, 0xb4, 0xb4, 0xa6, 0x2e, + 0x2f, 0x6f, 0x6f, 0x7d, 0xd5, 0xd4, 0x94, 0x94, + 0x86, 0x2e, 0x2f, 0x6f, 0x6b, 0x79, 0xd1, 0xd0, + 0x92, 0x92, 0x8d, 0x65, 0x64, 0x34, 0xb0, 0xa2, + }; + + n8 index; +}; + +struct TKOBoxing : Protection { + auto readPRG(n32 address, n8 data) -> n8 override { + if(address != 0x5e00 && address != 0x5e01) return data; + if(!(address & 1)) { + index = 0; + return 0; + } + return sequence[index++ & 0x1f]; + } + + auto power() -> void override { + index = 0; + } + + auto serialize(serializer& s) -> void override { + s(index); + } + + static constexpr u8 sequence[32] = { + 0xff, 0xbf, 0xb7, 0x97, 0x97, 0x17, 0x57, 0x4f, + 0x6f, 0x6b, 0xeb, 0xa9, 0xb1, 0x90, 0x94, 0x14, + 0x56, 0x4e, 0x6f, 0x6b, 0xeb, 0xa9, 0xb1, 0x90, + 0xd4, 0x5c, 0x3e, 0x26, 0x87, 0x83, 0x13, 0x00, + }; + + n8 index; +}; + +struct SuperXevious : Protection { + auto readPRG(n32 address, n8 data) -> n8 override { + if(address == 0x54ff) { + index ^= 1; + return 0x05; + } + if(address == 0x5678) return index ? 0x01 : 0x00; + if(address == 0x578f) return index ? 0x89 : 0xd1; + if(address == 0x5567) return index ? 0x37 : 0x3e; + return data; + } + + auto power() -> void override { + index = 0; + } + + auto serialize(serializer& s) -> void override { + s(index); + } + + n1 index; +}; diff --git a/ares/fc/cartridge/board/vs/standard.cpp b/ares/fc/cartridge/board/vs/standard.cpp new file mode 100644 index 000000000..7056e7682 --- /dev/null +++ b/ares/fc/cartridge/board/vs/standard.cpp @@ -0,0 +1,35 @@ +struct Standard : Interface { + using Interface::Interface; + + auto addressPRG(n32 address) -> maybe override { + if(address < 0x8000) return {}; + n32 slot = (address - 0x8000) >> 13; + n32 bank = slot; + if(programBanks == 5) { + bank = programBank; + if(slot) bank = slot + 1; + } + return mapPRG(bank, address); + } + + auto writePRG(n32 address, n8 data) -> void override { + if(address == 0x4016) programBank = characterBank = data.bit(2); + } + + auto addressCHR(n32 address) -> maybe override { + return mapCHR(characterBank * 8 + (address >> 10 & 7), address); + } + + auto power() -> void override { + programBank = programBanks == 5; + characterBank = 0; + } + + auto serialize(serializer& s) -> void override { + s(programBank); + s(characterBank); + } + + n1 programBank; + n1 characterBank; +}; diff --git a/ares/fc/cartridge/board/vs/sunsoft-3.cpp b/ares/fc/cartridge/board/vs/sunsoft-3.cpp new file mode 100644 index 000000000..45ab17ade --- /dev/null +++ b/ares/fc/cartridge/board/vs/sunsoft-3.cpp @@ -0,0 +1,40 @@ +struct Sunsoft3 : Interface { + using Interface::Interface; + + auto addressPRG(n32 address) -> maybe override { + if(address < 0x8000) return {}; + return mapPRG(programBank[address >> 13 & 3], address); + } + + auto writePRG(n32 address, n8 data) -> void override { + if(address < 0x8000) return; + switch(address & 0x7800) { + case 0x0800: + case 0x1800: + case 0x2800: + case 0x3800: + selectCHR(characterBank, address >> 11 & 6, 2, data * 2); + break; + case 0x7800: + selectPRG16(programBank, 0, data); + break; + } + } + + auto addressCHR(n32 address) -> maybe override { + return mapCHR(characterBank[address >> 10 & 7], address); + } + + auto power() -> void override { + initializePRG(programBank); + initializeCHR(characterBank); + } + + auto serialize(serializer& s) -> void override { + s(programBank); + s(characterBank); + } + + n8 programBank[4]; + n8 characterBank[8]; +}; diff --git a/ares/fc/cartridge/board/vs/unrom.cpp b/ares/fc/cartridge/board/vs/unrom.cpp new file mode 100644 index 000000000..70e7b9296 --- /dev/null +++ b/ares/fc/cartridge/board/vs/unrom.cpp @@ -0,0 +1,27 @@ +struct UNROM : Interface { + using Interface::Interface; + + auto addressPRG(n32 address) -> maybe override { + if(address < 0x8000) return {}; + return mapPRG(programBank[address >> 13 & 3], address); + } + + auto writePRG(n32 address, n8 data) -> void override { + if(address >= 0x8000) selectPRG16(programBank, 0, data); + } + + auto addressCHR(n32 address) -> maybe override { + return mapCHR(address >> 10 & 7, address); + } + + auto power() -> void override { + initializePRG(programBank); + selectPRG16(programBank, 0, 0); + } + + auto serialize(serializer& s) -> void override { + s(programBank); + } + + n8 programBank[4]; +}; diff --git a/ares/fc/cartridge/board/vs/vrc1.cpp b/ares/fc/cartridge/board/vs/vrc1.cpp new file mode 100644 index 000000000..8f9b31f5e --- /dev/null +++ b/ares/fc/cartridge/board/vs/vrc1.cpp @@ -0,0 +1,33 @@ +struct VRC1 : Interface { + using Interface::Interface; + + auto addressPRG(n32 address) -> maybe override { + if(address < 0x8000) return {}; + return mapPRG(programBank[address >> 13 & 3], address); + } + + auto writePRG(n32 address, n8 data) -> void override { + if(address < 0x8000) return; + auto index = address >> 12 & 7; + if(index == 0 || index == 2 || index == 4) selectPRG8(programBank, index >> 1, data); + if(index == 6) selectCHR(characterBank, 0, 4, data * 4); + if(index == 7) selectCHR(characterBank, 4, 4, data * 4); + } + + auto addressCHR(n32 address) -> maybe override { + return mapCHR(characterBank[address >> 10 & 7], address); + } + + auto power() -> void override { + initializePRG(programBank); + initializeCHR(characterBank); + } + + auto serialize(serializer& s) -> void override { + s(programBank); + s(characterBank); + } + + n8 programBank[4]; + n8 characterBank[8]; +}; diff --git a/ares/fc/cartridge/cartridge.cpp b/ares/fc/cartridge/cartridge.cpp index bd91852a5..ac220634e 100644 --- a/ares/fc/cartridge/cartridge.cpp +++ b/ares/fc/cartridge/cartridge.cpp @@ -38,6 +38,7 @@ auto Cartridge::disconnect() -> void { board->pak.reset(); board.reset(); pak.reset(); + information = {}; node.reset(); } diff --git a/ares/fc/controller/controller.hpp b/ares/fc/controller/controller.hpp index 39f159573..fa7cbfe84 100644 --- a/ares/fc/controller/controller.hpp +++ b/ares/fc/controller/controller.hpp @@ -24,6 +24,7 @@ struct Controller { virtual ~Controller() = default; + virtual auto frame() -> void {} virtual auto data() -> n3 { return 0b000; } virtual auto latch(n1 data) -> void {} virtual auto serialize(serializer&) -> void {} diff --git a/ares/fc/controller/port.hpp b/ares/fc/controller/port.hpp index 8635da674..cc7227afb 100644 --- a/ares/fc/controller/port.hpp +++ b/ares/fc/controller/port.hpp @@ -7,6 +7,7 @@ struct ControllerPort { auto unload() -> void; auto allocate(string name) -> Node::Peripheral; + auto frame() -> void { if(device) device->frame(); } auto data() -> n3 { if(device) return device->data(); return 0b000; } auto latch(n1 data) -> void { if(device) return device->latch(data); } diff --git a/ares/fc/controller/zapper/zapper.cpp b/ares/fc/controller/zapper/zapper.cpp index 2aca737b3..5f2d632f3 100644 --- a/ares/fc/controller/zapper/zapper.cpp +++ b/ares/fc/controller/zapper/zapper.cpp @@ -1,81 +1,126 @@ Zapper::Zapper(Node::Port port) { - node = port->append("Zapper"); + node = port->append("Zapper"); - x = node->append("X"); - y = node->append("Y"); + x = node->append("X"); + y = node->append("Y"); trigger = node->append("Trigger"); - - sprite = node->append("Crosshair"); - sprite->setImage(Resource::Sprite::SuperFamicom::CrosshairGreen); - ppu.screen->attach(sprite); + + lightGun.load(node, x, y, trigger); } Zapper::~Zapper() { - if(ppu.screen) ppu.screen->detach(sprite); + lightGun.unload(); } -auto Zapper::data() -> n3 { - u32 next = ppu.io.ly * 283 + ppu.io.lx; +auto LightGun::load(Node::Object parent, Node::Input::Axis x, Node::Input::Axis y, + Node::Input::Button trigger) -> void { + unload(); + this->parent = parent; + this->x = x; + this->y = y; + this->trigger = trigger; + cx = 256 / 2; + cy = 240 / 2; + px = 0; + py = 0; + nx = 256 / 2; + ny = 240 / 2; + + sprite = parent->append("Crosshair"); + sprite->setImage(Resource::Sprite::Famicom::Crosshair); + ppu.screen->attach(sprite); +} + +auto LightGun::unload() -> void { + if(ppu.screen && sprite) ppu.screen->detach(sprite); + if(parent && sprite) parent->remove(sprite); + parent.reset(); + x.reset(); + y.reset(); + trigger.reset(); + sprite.reset(); +} + +auto LightGun::frame() -> void { + platform->input(x); //-n = left, 0 = center, +n = right + platform->input(y); //-n = up, 0 = center, +n = down + + px = x->value(); + py = y->value(); + if(!px && !py) return; + + cx = max(-8, min(256 + 8, px + cx)); + cy = max(-8, min(240 + 8, py + cy)); + + sprite->setPosition(cx, cy); + sprite->setVisible(true); + + nx = cx + 8; + ny = cy + 8; +} + +auto LightGun::data() -> n3 { n3 result = 0b000; platform->input(trigger); - if (trigger->value()) result.bit(2) = 1; - - if (next < previous) { - platform->input(x); //-n = left, 0 = center, +n = right - platform->input(y); //-n = up, 0 = center, +n = down - - if (x->value() != px || y->value() != py) { - px = x->value(); - py = y->value(); - - cx = max(-8, min(256 + 8, px + cx)); - cy = max(-8, min(240 + 8, py + cy)); - - sprite->setPosition(cx, cy); - sprite->setVisible(true); - - nx = cx + 8; - ny = cy + 8; - } - } - previous = next; + if(trigger->value()) result.bit(2) = 1; bool offscreen = nx < 0 || ny < 0 || nx >= 256 || ny >= 240; - if (offscreen) { + if(offscreen) { result.bit(1) = 1; return result; } - u32 *target = ppu.screen->pixels().data(); + u32* target = ppu.screen->pixels().data(); u32 count = 0; for(int y = ny - 8; y <= ny + 8; y++) { - if (y < 0) continue; - if (y <= ppu.io.ly - 18) continue; - if (y > ppu.io.ly) break; + if(y < 0) continue; + if(y <= ppu.io.ly - 18) continue; + if(y > ppu.io.ly) break; for(int x = nx - 8; x <= nx + 8; x++) { - if (x < 0) continue; - if (x >= 256) break; + if(x < 0) continue; + if(x >= 256) break; - n32 offset = y * 283 + x;// y * 283 + x + Region::PAL() ? 18 : 16; + n32 offset = y * 283 + x; //y * 283 + x + Region::PAL() ? 18 : 16; n64 color = ppu.color(target[offset]); f64 brightness = 0; brightness += ((color >> 32) & 0xffff) * 0.299; brightness += ((color >> 16) & 0xffff) * 0.587; brightness += ((color >> 0) & 0xffff) * 0.114; - if (brightness > 0x80) - count++; + if(brightness > 0x80) count++; } } - if (count < 0x40) - result.bit(1) = 1; - + if(count < 0x40) result.bit(1) = 1; return result; } +auto LightGun::serialize(serializer& s) -> void { + s(cx); + s(cy); + s(px); + s(py); + s(nx); + s(ny); + u32 legacyRaster = 0; + s(legacyRaster); + + if(s.reading() && sprite) { + sprite->setPosition(cx, cy); + sprite->setVisible(true); + } +} + +auto Zapper::frame() -> void { + lightGun.frame(); +} + +auto Zapper::data() -> n3 { + return lightGun.data(); +} + auto Zapper::latch(n1 data) -> void { } diff --git a/ares/fc/controller/zapper/zapper.hpp b/ares/fc/controller/zapper/zapper.hpp index 2b45ffa47..5e316edb0 100644 --- a/ares/fc/controller/zapper/zapper.hpp +++ b/ares/fc/controller/zapper/zapper.hpp @@ -1,23 +1,36 @@ -struct Zapper : Controller { +struct LightGun { + Node::Object parent; Node::Input::Axis x; Node::Input::Axis y; Node::Input::Button trigger; Node::Video::Sprite sprite; - Zapper(Node::Port); - ~Zapper(); - - auto main() -> void; - auto data() -> n3 override; - auto latch(n1 data) -> void override; - auto serialize(serializer&) -> void override; + auto load(Node::Object parent, Node::Input::Axis x, Node::Input::Axis y, Node::Input::Button trigger) -> void; + auto unload() -> void; + auto frame() -> void; + auto data() -> n3; + auto serialize(serializer&) -> void; private: - i32 cx = 256 / 2; //x-coordinate - i32 cy = 240 / 2; //y-coordinate + i32 cx = 256 / 2; + i32 cy = 240 / 2; i32 px = 0; i32 py = 0; u32 nx = 256 / 2; u32 ny = 240 / 2; - u32 previous = 0; +}; + +struct Zapper : Controller { + Node::Input::Axis x; + Node::Input::Axis y; + Node::Input::Button trigger; + LightGun lightGun; + + Zapper(Node::Port); + ~Zapper(); + + auto frame() -> void override; + auto data() -> n3 override; + auto latch(n1 data) -> void override; + auto serialize(serializer&) -> void override; }; diff --git a/ares/fc/cpu/memory.cpp b/ares/fc/cpu/memory.cpp index 3d8a8ddc7..5ed433114 100644 --- a/ares/fc/cpu/memory.cpp +++ b/ares/fc/cpu/memory.cpp @@ -33,6 +33,7 @@ auto CPU::readIO(n16 address) -> n8 { switch(address) { case 0x4016: { + if(system.model() == System::Model::VsUniSystem) return vsUniSystem.io.read(address, data); auto port1 = controllerPort1.data(); auto port3 = expansionPort.read1(); if(system.controls.microphone) platform->input(system.controls.microphone); @@ -45,6 +46,7 @@ auto CPU::readIO(n16 address) -> n8 { } case 0x4017: { + if(system.model() == System::Model::VsUniSystem) return vsUniSystem.io.read(address, data); auto port2 = controllerPort2.data(); auto port3 = expansionPort.read2(); data.bit(0) = port3.bit(0) | port2.bit(0); @@ -70,6 +72,7 @@ auto CPU::writeIO(n16 address, n8 data) -> void { } case 0x4016: { + if(system.model() == System::Model::VsUniSystem) return vsUniSystem.io.write(data); controllerPort1.latch(data.bit(0)); controllerPort2.latch(data.bit(0)); expansionPort.write(data); diff --git a/ares/fc/fc.cpp b/ares/fc/fc.cpp index 1feede2ca..f1f992bb8 100644 --- a/ares/fc/fc.cpp +++ b/ares/fc/fc.cpp @@ -1,5 +1,6 @@ //started: 2011-09-05 +#include #include #include #include diff --git a/ares/fc/fc.hpp b/ares/fc/fc.hpp index e81d6f311..d15d7abfe 100644 --- a/ares/fc/fc.hpp +++ b/ares/fc/fc.hpp @@ -25,6 +25,7 @@ namespace ares::Famicom { #include #include + #include #include #include #include diff --git a/ares/fc/ppu/color.cpp b/ares/fc/ppu/color.cpp deleted file mode 100644 index b1dd6b021..000000000 --- a/ares/fc/ppu/color.cpp +++ /dev/null @@ -1,50 +0,0 @@ -auto PPU::color(n32 n) -> n64 { - f64 saturation = 1.5; - f64 hue = 0.0; - f64 contrast = 1.0; - f64 brightness = 1.0; - f64 gamma = 2.2; - - i32 color = (n & 0x0f), level = color < 0xe ? int(n >> 4 & 3) : 1; - - static constexpr f64 black = 0.518, white = 1.962, attenuation = 0.746; - static constexpr f64 levels[8] = { - 0.350, 0.518, 0.962, 1.550, - 1.094, 1.506, 1.962, 1.962, - }; - - f64 lo_and_hi[2] = { - levels[level + 4 * (color == 0x0)], - levels[level + 4 * (color < 0xd)], - }; - - f64 y = 0.0, i = 0.0, q = 0.0; - auto wave = [](i32 p, i32 color) { return (color + p + 8) % 12 < 6; }; - for(int p : range(12)) { - f64 spot = lo_and_hi[wave(p, color)]; - - if(((n & 0x040) && wave(p, 12)) - || ((n & 0x080) && wave(p, 4)) - || ((n & 0x100) && wave(p, 8)) - ) spot *= attenuation; - - f64 v = (spot - black) / (white - black); - - v = (v - 0.5) * contrast + 0.5; - v *= brightness / 12.0; - - y += v; - i += v * cos((Math::Pi / 6.0) * (p + hue)); - q += v * sin((Math::Pi / 6.0) * (p + hue)); - } - - i *= saturation; - q *= saturation; - - auto gammaAdjust = [=](f64 f) { return f < 0.0 ? 0.0 : pow(f, 2.2 / gamma); }; - n64 r = uclamp<16>(65535.0 * gammaAdjust(y + 0.946882 * i + 0.623557 * q)); - n64 g = uclamp<16>(65535.0 * gammaAdjust(y + -0.274788 * i + -0.635691 * q)); - n64 b = uclamp<16>(65535.0 * gammaAdjust(y + -1.108545 * i + 1.709007 * q)); - - return r << 32 | g << 16 | b << 0; -} diff --git a/ares/fc/ppu/memory.cpp b/ares/fc/ppu/memory.cpp index e1893c90e..f4d8b4a86 100644 --- a/ares/fc/ppu/memory.cpp +++ b/ares/fc/ppu/memory.cpp @@ -30,6 +30,7 @@ auto PPU::readIO(n16 address) -> n8 { result.bit(5) = sprite.spriteOverflow; result.bit(6) = io.spriteZeroHit; result.bit(7) = io.nmiFlag; + result = model->status(result); scroll.latch = 0; io.nmiHold = 0; cpu.nmiLine(io.nmiFlag = 0); @@ -39,8 +40,7 @@ auto PPU::readIO(n16 address) -> n8 { case 4: result = oam[sprite.oamAddress]; - if (io.ly < 240 || io.ly == vlines() - 1 || - (Region::PAL() && io.ly >= 264 && io.ly <= vlines() - 2)) + if(model->raster.restrictsOAM(io.ly)) if (enable()) result = sprite.oamData; break; @@ -79,7 +79,7 @@ auto PPU::readIO(n16 address) -> n8 { auto PPU::writeIO(n16 address, n8 data) -> void { io.mdr = data; - switch(address.bit(0,2)) { + switch(model->mapWriteRegister(address.bit(0,2))) { //PPUCTRL case 0: @@ -122,8 +122,7 @@ auto PPU::writeIO(n16 address, n8 data) -> void { // sprite or background rendering is enabled) do not // modify values in OAM, but do perform a glitchy // increment of OAMADDR, bumping only the high 6 bits - if (io.ly < 240 || io.ly == vlines() - 1 || - (Region::PAL() && io.ly >= 264 && io.ly <= vlines() - 2)) { + if(model->raster.restrictsOAM(io.ly)) { if (enable()) { ++sprite.oamMainCounterIndex; sprite.oamMainCounterTiming = 0; diff --git a/ares/fc/ppu/model.cpp b/ares/fc/ppu/model.cpp new file mode 100644 index 000000000..ff567af96 --- /dev/null +++ b/ares/fc/ppu/model.cpp @@ -0,0 +1,209 @@ +auto PPU::Model::Raster::evaluatesSprites(u32 y) const -> bool { + return y < 240 || (y >= spriteEvaluationStart && y <= spriteEvaluationEnd); +} + +auto PPU::Model::Raster::restrictsOAM(u32 y) const -> bool { + return evaluatesSprites(y) || y == scanlines - 1; +} + +auto PPU::Model::Raster::pixelVisible(u32 x, u32 y) const -> bool { + return y >= visibleTop && x >= visibleLeft && x <= visibleRight; +} + +auto PPU::Model::Raster::backdrop(n9 color) const -> n9 { + return fixedBackdrop ? backdropColor : color; +} + +PPU::Model::Model(const Raster& raster) : raster(raster) { +} + +auto PPU::Model::color(n9 input) const -> n64 { + f64 saturation = 1.5; + f64 hue = 0.0; + f64 contrast = 1.0; + f64 brightness = 1.0; + f64 gamma = 2.2; + + i32 color = (input & 0x0f), level = color < 0xe ? int(input >> 4 & 3) : 1; + + static constexpr f64 black = 0.518, white = 1.962, attenuation = 0.746; + static constexpr f64 levels[8] = { + 0.350, 0.518, 0.962, 1.550, + 1.094, 1.506, 1.962, 1.962, + }; + + f64 loAndHi[2] = { + levels[level + 4 * (color == 0x0)], + levels[level + 4 * (color < 0xd)], + }; + + f64 y = 0.0, i = 0.0, q = 0.0; + auto wave = [](i32 phase, i32 color) { return (color + phase + 8) % 12 < 6; }; + for(auto phase : range(12)) { + f64 spot = loAndHi[wave(phase, color)]; + + if(((input & 0x040) && wave(phase, 12)) + || ((input & 0x080) && wave(phase, 4)) + || ((input & 0x100) && wave(phase, 8)) + ) spot *= attenuation; + + f64 value = (spot - black) / (white - black); + value = (value - 0.5) * contrast + 0.5; + value *= brightness / 12.0; + + y += value; + i += value * cos((Math::Pi / 6.0) * (phase + hue)); + q += value * sin((Math::Pi / 6.0) * (phase + hue)); + } + + i *= saturation; + q *= saturation; + + auto gammaAdjust = [=](f64 value) { return value < 0.0 ? 0.0 : pow(value, 2.2 / gamma); }; + n64 red = uclamp<16>(65535.0 * gammaAdjust(y + 0.946882 * i + 0.623557 * q)); + n64 green = uclamp<16>(65535.0 * gammaAdjust(y + -0.274788 * i + -0.635691 * q)); + n64 blue = uclamp<16>(65535.0 * gammaAdjust(y + -1.108545 * i + 1.709007 * q)); + + return red << 32 | green << 16 | blue; +} + +auto PPU::Model::mapWriteRegister(n3 address) const -> n3 { + return address; +} + +auto PPU::Model::status(n8 data) const -> n8 { + return data; +} + +namespace { + +auto rp2c02Raster() -> PPU::Model::Raster { + return { + 4, 262, 241, 242, + 16, 0, 256, 240, 16, + 1, 0, + 0, 0, 255, + 8.0, 7.0, + true, false, 0 + }; +} + +auto rp2c07Raster() -> PPU::Model::Raster { + return { + 5, 312, 241, 288, + 18, 0, 256, 240, 18, + 264, 310, + 1, 2, 253, + 55.0, 43.0, + false, true, 0x3f + }; +} + +auto ua6538Raster() -> PPU::Model::Raster { + return { + 5, 312, 291, 242, + 16, 0, 256, 240, 18, + 1, 0, + 0, 0, 255, + 55.0, 43.0, + false, false, 0 + }; +} + +struct RP2C02 final : PPU::Model { + RP2C02() : Model(rp2c02Raster()) {} +}; + +struct RP2C07 final : PPU::Model { + RP2C07() : Model(rp2c07Raster()) {} +}; + +struct UA6538 final : PPU::Model { + UA6538() : Model(ua6538Raster()) {} +}; + +struct RGBModel : PPU::Model { + RGBModel(const Palette& palette) : Model(rp2c02Raster()), palette(palette) {} + + auto color(n9 color) const -> n64 override { + u32 address = (color & 0x3f) * 3; + n64 red = color.bit(6) ? 7 : palette[address + 0]; + n64 green = color.bit(7) ? 7 : palette[address + 1]; + n64 blue = color.bit(8) ? 7 : palette[address + 2]; + return red * 0xffff / 7 << 32 | green * 0xffff / 7 << 16 | blue * 0xffff / 7; + } + + const Palette palette; +}; + +struct RP2C04 final : RGBModel { + RP2C04(const Palette& palette) : RGBModel(palette) {} +}; + +struct RC2C05 : RGBModel { + RC2C05(const Palette& palette, n6 securityCode) : RGBModel(palette), securityCode(securityCode) {} + + auto mapWriteRegister(n3 address) const -> n3 override { + if(address == 0) return 1; + if(address == 1) return 0; + return address; + } + + auto status(n8 data) const -> n8 override { + return data & 0xc0 | securityCode; + } + + const n6 securityCode; +}; + +struct RC2C05_01 final : RC2C05 { + RC2C05_01(const Palette& palette) : RC2C05(palette, 0x1b) {} +}; + +struct RC2C05_02 final : RC2C05 { + RC2C05_02(const Palette& palette) : RC2C05(palette, 0x3d) {} +}; + +struct RC2C05_03 final : RC2C05 { + RC2C05_03(const Palette& palette) : RC2C05(palette, 0x1c) {} +}; + +struct RC2C05_04 final : RC2C05 { + RC2C05_04(const Palette& palette) : RC2C05(palette, 0x1b) {} +}; + +} + +auto PPU::createBaseModel() -> std::unique_ptr { + switch(system.region()) { + case System::Region::NTSCJ: return std::make_unique(); + case System::Region::NTSCU: return std::make_unique(); + case System::Region::PAL: return std::make_unique(); + case System::Region::Dendy: return std::make_unique(); + } + return std::make_unique(); +} + +auto PPU::createVsModel(string identifier, const Model::Palette& palette) -> std::unique_ptr { + if(identifier == "ppu2c04") return std::make_unique(palette); + if(identifier == "ppu2c05_01") return std::make_unique(palette); + if(identifier == "ppu2c05_02") return std::make_unique(palette); + if(identifier == "ppu2c05_03") return std::make_unique(palette); + if(identifier == "ppu2c05_04") return std::make_unique(palette); + return {}; +} + +auto PPU::createVsModel() -> std::unique_ptr { + if(!cartridge.pak) return {}; + + auto paletteFile = cartridge.pak->read("palette.rom"); + if(!paletteFile || paletteFile->size() != Model::PaletteSize) return {}; + + Model::Palette palette; + paletteFile->read(palette.data(), palette.size()); + for(auto channel : palette) { + if(channel > 7) return {}; + } + + return createVsModel(cartridge.pak->attribute("ppu"), palette); +} diff --git a/ares/fc/ppu/model.hpp b/ares/fc/ppu/model.hpp new file mode 100644 index 000000000..852df3513 --- /dev/null +++ b/ares/fc/ppu/model.hpp @@ -0,0 +1,40 @@ +struct Model { + struct Raster { + u32 clockDivider; + u32 scanlines; + u32 vblankScanline; + u32 canvasHeight; + u32 viewportX; + u32 viewportY; + u32 viewportWidth; + u32 viewportHeight; + u32 outputOffset; + u32 spriteEvaluationStart; + u32 spriteEvaluationEnd; + u32 visibleTop; + u32 visibleLeft; + u32 visibleRight; + f64 aspectX; + f64 aspectY; + bool oddFrameCycleSkip; + bool fixedBackdrop; + n9 backdropColor; + + auto evaluatesSprites(u32 y) const -> bool; + auto restrictsOAM(u32 y) const -> bool; + auto pixelVisible(u32 x, u32 y) const -> bool; + auto backdrop(n9 color) const -> n9; + }; + + static constexpr u32 PaletteSize = 192; + using Palette = std::array; + + Model(const Raster& raster); + virtual ~Model() = default; + + virtual auto color(n9 color) const -> n64; + virtual auto mapWriteRegister(n3 address) const -> n3; + virtual auto status(n8 data) const -> n8; + + const Raster raster; +}; diff --git a/ares/fc/ppu/ppu.cpp b/ares/fc/ppu/ppu.cpp index c285add7d..741f88bfc 100644 --- a/ares/fc/ppu/ppu.cpp +++ b/ares/fc/ppu/ppu.cpp @@ -3,15 +3,17 @@ namespace ares::Famicom { PPU ppu; +#include "model.cpp" #include "memory.cpp" #include "render.cpp" -#include "color.cpp" #include "debugger.cpp" #include "sprite.cpp" #include "scroll.cpp" #include "serialization.cpp" auto PPU::load(Node::Object parent) -> void { + model = createBaseModel(); + ciram.allocate(2048); cgram.allocate(32); oam.allocate(256); @@ -23,7 +25,7 @@ auto PPU::load(Node::Object parent) -> void { screen->colors(1 << 9, std::bind_front(&PPU::color, this)); screen->setSize(283, displayHeight()); screen->setScale(1.0, 1.0); - (Region::PAL() || Region::Dendy()) ? screen->setAspect(55.0, 43.0) : screen->setAspect(8.0, 7.0); + screen->setAspect(model->raster.aspectX, model->raster.aspectY); screen->refreshRateHint(system.frequency() / rate(), 341, vlines()); debugger.load(node); @@ -39,6 +41,7 @@ auto PPU::unload() -> void { cgram.reset(); oam.reset(); soam.reset(); + model.reset(); } auto PPU::main() -> void { @@ -55,7 +58,7 @@ auto PPU::step(u32 clocks) -> void { if (enable() && io.ly == L - 1) cyclePrepareSpriteEvaluation(); - if (enable() && (io.ly < 240 || (Region::PAL() && io.ly >= 264 && io.ly <= L - 2))) + if(enable() && model->raster.evaluatesSprites(io.ly)) cycleSpriteEvaluation(); if (enable() && (io.ly < 240 || io.ly == L - 1)) @@ -97,18 +100,9 @@ auto PPU::frame() -> void { screen->setSize(283, displayHeight()); screen->setViewport(0, 0, 283, displayHeight()); } else { - int x = 16; - int y = 0; - int width = 283 - 27; - int height = displayHeight() - 2; - - if(Region::PAL()) { - x += 2; - height -= 46; - } - - screen->setSize(width, height); - screen->setViewport(x, y, width, height); + auto& raster = model->raster; + screen->setSize(raster.viewportWidth, raster.viewportHeight); + screen->setViewport(raster.viewportX, raster.viewportY, raster.viewportWidth, raster.viewportHeight); } screen->frame(); @@ -116,6 +110,13 @@ auto PPU::frame() -> void { } auto PPU::power(bool reset) -> void { + if(!reset && system.model() == System::Model::VsUniSystem) { + auto candidate = createVsModel(); + if(!candidate) return; + model = std::move(candidate); + screen->resetPalette(); + } + Thread::create(system.frequency(), std::bind_front(&PPU::main, this)); screen->power(); diff --git a/ares/fc/ppu/ppu.hpp b/ares/fc/ppu/ppu.hpp index ee6d27545..d9a31b958 100644 --- a/ares/fc/ppu/ppu.hpp +++ b/ares/fc/ppu/ppu.hpp @@ -1,4 +1,6 @@ struct PPU : Thread { + #include "model.hpp" + Node::Object node; Node::Video::Screen screen; Memory::Writable ciram; @@ -19,10 +21,10 @@ struct PPU : Thread { } memory; } debugger; - auto rate() const -> u32 { return Region::PAL() || Region::Dendy() ? 5 : 4; } - auto vlines() const -> u32 { return Region::PAL() || Region::Dendy() ? 312 : 262; } - auto displayHeight() const -> u32 { return Region::PAL() ? 288 : 242; } - auto vblankScanline() const -> u32 { return Region::Dendy() ? 291 : 241; } + auto rate() const -> u32 { return model->raster.clockDivider; } + auto vlines() const -> u32 { return model->raster.scanlines; } + auto displayHeight() const -> u32 { return model->raster.canvasHeight; } + auto vblankScanline() const -> u32 { return model->raster.vblankScanline; } //ppu.cpp auto load(Node::Object) -> void; @@ -54,8 +56,8 @@ struct PPU : Thread { auto renderPixel() -> void; auto renderScanline() -> void; - //color.cpp - auto color(n32) -> n64; + //model.cpp + auto color(n32 c) -> n64 { return model->color(c); } //serialization.cpp auto serialize(serializer&) -> void; @@ -207,6 +209,13 @@ struct PPU : Thread { 0x09, 0x01, 0x34, 0x03, 0x00, 0x04, 0x00, 0x14, 0x08, 0x3A, 0x00, 0x02, 0x00, 0x20, 0x2C, 0x08, }; + +private: + auto createBaseModel() -> std::unique_ptr; + auto createVsModel() -> std::unique_ptr; + auto createVsModel(string identifier, const Model::Palette& palette) -> std::unique_ptr; + + std::unique_ptr model; }; extern PPU ppu; diff --git a/ares/fc/ppu/render.cpp b/ares/fc/ppu/render.cpp index f7dd9e1c2..fcae31c9b 100644 --- a/ares/fc/ppu/render.cpp +++ b/ares/fc/ppu/render.cpp @@ -25,8 +25,7 @@ auto PPU::renderPixel() -> void { u32 objectPalette = 0; bool objectPriority = 0; - //PAL systems technically blank the topmost scanline and a 2px column on each side of active display. - if(Region::PAL()) if(io.ly == 0 || x < 2 || x > 253) return; + if(!model->raster.pixelVisible(x, io.ly)) return; palette |= latch.tiledataLo & mask ? 1 : 0; palette |= latch.tiledataHi & mask ? 2 : 0; @@ -72,17 +71,14 @@ auto PPU::renderPixel() -> void { color = io.emphasis << 6 | readCGRAM((n5)var.address); } - if(Region::PAL() || Region::Dendy()) - output[(x + 18) % 283] = color; - else - output[(x + 16) % 283] = color; + output[(x + model->raster.outputOffset) % 283] = color; } auto PPU::renderScanline() -> void { if(io.ly < screen->canvasHeight()) { output = screen->pixels().data() + io.ly * 283; - for (auto n : range(283)) - output[n] = Region::PAL() ? 0x3f : io.emphasis << 6 | readCGRAM(0); + auto backdrop = model->raster.backdrop(io.emphasis << 6 | readCGRAM(0)); + for(auto n : range(283)) output[n] = backdrop; } //Vblank @@ -188,7 +184,7 @@ auto PPU::renderScanline() -> void { //337-338 loadCHR(0x2000 | (n12)var.address); - bool skip = !Region::PAL() && !Region::Dendy() && enable() && io.field == 1 && io.ly == vlines() - 1; + bool skip = model->raster.oddFrameCycleSkip && enable() && io.field == 1 && io.ly == vlines() - 1; step(2); //339 diff --git a/ares/fc/system/serialization.cpp b/ares/fc/system/serialization.cpp index 034b40ae7..db0ae6df7 100644 --- a/ares/fc/system/serialization.cpp +++ b/ares/fc/system/serialization.cpp @@ -1,4 +1,4 @@ -static const string SerializerVersion = "v153"; +static const string SerializerVersion = "v154"; auto System::serialize(bool synchronize) -> serializer { if(synchronize) scheduler.enter(Scheduler::Mode::Synchronize); @@ -43,8 +43,12 @@ auto System::serialize(serializer& s, bool synchronize) -> void { s(cpu); s(apu); s(ppu); - if(fds.present) s(fds); - s(controllerPort1); - s(controllerPort2); - expansionPort.serialize(s); + if(model() == Model::VsUniSystem) { + s(vsUniSystem); + } else { + if(fds.present) s(fds); + s(controllerPort1); + s(controllerPort2); + expansionPort.serialize(s); + } } diff --git a/ares/fc/system/system.cpp b/ares/fc/system/system.cpp index 412f27388..9b45f33b6 100644 --- a/ares/fc/system/system.cpp +++ b/ares/fc/system/system.cpp @@ -8,6 +8,7 @@ auto enumerate() -> std::vector { "[Nintendo] Famicom (NTSC-J)", "[Nintendo] Famicom (NTSC-U)", "[Nintendo] Famicom (PAL)", + "[Nintendo] Vs. UniSystem", "[Dendy] Dendy", }; } @@ -38,6 +39,9 @@ auto System::game() -> string { auto System::run() -> void { scheduler.enter(); + if(model() == Model::VsUniSystem) vsUniSystem.frame(); + controllerPort1.frame(); + controllerPort2.frame(); auto reset = controls.reset->value(); platform->input(controls.reset); if(!reset && controls.reset->value()) power(true); @@ -71,6 +75,13 @@ auto System::load(Node::System& root, string name) -> bool { information.frequency = Constants::Colorburst::PAL * 6.0; information.cpuDivider = 15; } + if(name == "[Nintendo] Vs. UniSystem") { + information.model = Model::VsUniSystem; + information.name = "Famicom"; + information.region = Region::NTSCU; + information.frequency = Constants::Colorburst::NTSC * 6.0; + information.cpuDivider = 12; + } node = std::make_shared(information.name); node->setAttribute("configuration", name); @@ -89,9 +100,13 @@ auto System::load(Node::System& root, string name) -> bool { apu.load(node); ppu.load(node); cartridgeSlot.load(node); - controllerPort1.load(node); - controllerPort2.load(node); - expansionPort.load(node); + if(model() == Model::VsUniSystem) { + vsUniSystem.load(node); + } else { + controllerPort1.load(node); + controllerPort2.load(node); + expansionPort.load(node); + } return true; } @@ -107,17 +122,23 @@ auto System::unload() -> void { controls.unload(); cpu.unload(); apu.unload(); - ppu.unload(); - cartridgeSlot.unload(); - controllerPort1.unload(); - controllerPort2.unload(); - expansionPort.unload(); + if(model() == Model::VsUniSystem) { + cartridgeSlot.unload(); + vsUniSystem.unload(); + ppu.unload(); + } else { + ppu.unload(); + cartridgeSlot.unload(); + controllerPort1.unload(); + controllerPort2.unload(); + expansionPort.unload(); + } node = {}; } auto System::power(bool reset) -> void { + if(model() == Model::VsUniSystem) vsUniSystem.power(reset); for(auto& setting : node->find()) setting->setLatch(); - random.entropy(Random::Entropy::Low); // The apu should run before the cpu apu.power(reset); diff --git a/ares/fc/system/system.hpp b/ares/fc/system/system.hpp index d66102caf..40d21dcba 100644 --- a/ares/fc/system/system.hpp +++ b/ares/fc/system/system.hpp @@ -14,8 +14,10 @@ struct System { auto poll() -> void; } controls; + enum class Model : u32 { Famicom, VsUniSystem }; enum class Region : u32 { NTSCJ, NTSCU, PAL, Dendy }; + auto model() const -> Model { return information.model; } auto name() const -> string { return information.name; } auto region() const -> Region { return information.region; } auto frequency() const -> f64 { return information.frequency; } @@ -36,6 +38,7 @@ struct System { private: struct Information { + Model model = Model::Famicom; string name = "Famicom"; Region region = Region::NTSCJ; f64 frequency = Constants::Colorburst::NTSC * 6.0; diff --git a/ares/fc/vs/controls.cpp b/ares/fc/vs/controls.cpp new file mode 100644 index 000000000..8314ba732 --- /dev/null +++ b/ares/fc/vs/controls.cpp @@ -0,0 +1,42 @@ +auto VsUniSystem::Controls::load(Node::Object parent) -> bool { + unload(); + if(!cartridge.pak) return false; + + auto mode = cartridge.pak->attribute("input"); + if(mode != "standard" && mode != "swapped" && mode != "swap-ab" && mode != "zapper") return false; + + node = parent->append("Controls"); + service = node->append("Service"); + coins[0] = node->append("Player 1 Coin"); + coins[1] = node->append("Player 2 Coin"); + + n8 fixedBits = cartridge.pak->attribute("protection") == "ice-climber" ? 0x08 : 0x00; + if(mode == "standard") input = std::make_unique(node, StandardInput::Wiring::Standard, fixedBits); + if(mode == "swapped") input = std::make_unique(node, StandardInput::Wiring::Swapped, fixedBits); + if(mode == "swap-ab") input = std::make_unique(node, StandardInput::Wiring::SwapAB, fixedBits); + if(mode == "zapper") input = std::make_unique(node); + return true; +} + +auto VsUniSystem::Controls::unload() -> void { + input.reset(); + service.reset(); + coins[0].reset(); + coins[1].reset(); + if(auto parent = Node::parent(node)) parent->remove(node); + node.reset(); +} + +auto VsUniSystem::Controls::frame() -> void { + if(input) input->frame(); +} + +auto VsUniSystem::Controls::data(u32 stream) -> n1 { + if(input) return input->data(stream); + return 0; +} + +auto VsUniSystem::Controls::latch() -> std::array { + if(input) return input->latch(); + return {}; +} diff --git a/ares/fc/vs/dip-switches.cpp b/ares/fc/vs/dip-switches.cpp new file mode 100644 index 000000000..c492bedf4 --- /dev/null +++ b/ares/fc/vs/dip-switches.cpp @@ -0,0 +1,116 @@ +static auto parseDipByte(string text, u8& value) -> bool { + if(!text) return false; + + u32 base = 10; + u32 offset = 0; + if(text.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) { + base = 16; + offset = 2; + } + if(offset == text.size()) return false; + + u32 result = 0; + for(auto index : range(offset, text.size())) { + u32 digit; + auto character = text[(u32)index]; + if(character >= '0' && character <= '9') digit = character - '0'; + else if(character >= 'a' && character <= 'f') digit = character - 'a' + 10; + else if(character >= 'A' && character <= 'F') digit = character - 'A' + 10; + else return false; + if(digit >= base || result > (255 - digit) / base) return false; + result = result * base + digit; + } + + value = result; + return true; +} + +auto VsUniSystem::DIPSwitches::load(Node::Object parent) -> bool { + unload(); + if(!cartridge.pak) return false; + + auto manifestFile = cartridge.pak->read("manifest.bml"); + if(!manifestFile) return false; + auto document = BML::unserialize(manifestFile->reads()); + auto games = document.find("game"); + if(games.size() != 1) return false; + + u8 usedMasks = 0; + std::vector definitions; + for(auto node : games.front()) { + if(node.name() != "dip") continue; + + Definition definition; + definition.name = node["name"].string(); + if(!definition.name) return false; + if(node["port"].string() != "DSW0") return false; + if(!parseDipByte(node["mask"].string(), definition.mask) || !definition.mask) return false; + if(usedMasks & definition.mask) return false; + if(!parseDipByte(node["default"].string(), definition.defaultValue)) return false; + if(definition.defaultValue & ~definition.mask) return false; + + bool hasDefault = false; + for(auto optionNode : node) { + if(optionNode.name() != "option") continue; + + Option option; + option.name = optionNode["name"].string(); + if(!option.name) return false; + if(!parseDipByte(optionNode["value"].string(), option.value)) return false; + if(option.value & ~definition.mask) return false; + for(auto& existing : definition.options) { + if(existing.name == option.name) return false; + } + if(option.value == definition.defaultValue) hasDefault = true; + definition.options.push_back(std::move(option)); + } + + if(definition.options.empty() || !hasDefault) return false; + usedMasks |= definition.mask; + definitions.push_back(std::move(definition)); + } + + create(parent, std::move(definitions)); + return true; +} + +auto VsUniSystem::DIPSwitches::create(Node::Object parent, std::vector definitions) -> void { + node = parent->append("DIP Switches"); + settings.reserve(definitions.size()); + + for(auto& definition : definitions) { + value |= definition.defaultValue; + auto index = settings.size(); + + std::vector allowedValues; + for(auto& option : definition.options) allowedValues.push_back(option.name); + string defaultValue; + for(auto& option : definition.options) { + if(option.value == definition.defaultValue) defaultValue = option.name; + } + + settings.push_back({{}, definition.mask, std::move(definition.options)}); + auto& setting = settings.back(); + setting.node = node->append(definition.name, defaultValue, [index](auto value) { + vsUniSystem.dipSwitches.update(index, value); + }); + setting.node->setAllowedValues(std::move(allowedValues)); + } +} + +auto VsUniSystem::DIPSwitches::unload() -> void { + if(auto parent = Node::parent(node)) parent->remove(node); + node.reset(); + settings.clear(); + value = 0; +} + +auto VsUniSystem::DIPSwitches::update(u32 index, string value) -> void { + if(index >= settings.size()) return; + auto& setting = settings[index]; + for(auto& option : setting.options) { + if(option.name != value) continue; + this->value = (this->value & ~setting.mask) | (option.value & setting.mask); + return; + } +} diff --git a/ares/fc/vs/io.cpp b/ares/fc/vs/io.cpp new file mode 100644 index 000000000..eab7db9e4 --- /dev/null +++ b/ares/fc/vs/io.cpp @@ -0,0 +1,60 @@ +auto VsUniSystem::IO::readStream(u32 stream) -> n1 { + if(strobe) return vsUniSystem.controls.data(stream); + if(positions[stream] >= 8) return 1; + return streams[stream].bit(positions[stream]++); +} + +auto VsUniSystem::IO::read(n16 address, n8 data) -> n8 { + data.bit(0) = readStream(address == 0x4017); + data.bit(1) = 0; + + if(address == 0x4016) { + data.bit(2) = servicePulse.frames != 0; + data.bit(3, 4) = vsUniSystem.dipSwitches.value.bit(0, 1); + data.bit(5) = coinPulses[0].frames != 0; + data.bit(6) = coinPulses[1].frames != 0; + data.bit(7) = 0; + } else { + data.bit(2, 7) = vsUniSystem.dipSwitches.value.bit(2, 7); + } + + return data; +} + +auto VsUniSystem::IO::write(n8 data) -> void { + n1 nextStrobe = data.bit(0); + if(strobe && !nextStrobe) { + auto latched = vsUniSystem.controls.latch(); + streams[0] = latched[0]; + streams[1] = latched[1]; + positions[0] = 0; + positions[1] = 0; + } + strobe = nextStrobe; +} + +auto VsUniSystem::IO::pollPulse(Node::Input::Button input, Pulse& pulse) -> void { + if(pulse.frames) pulse.frames--; + platform->input(input); + bool pressed = input->value(); + if(pressed && !pulse.held) pulse.frames = 3; + pulse.held = pressed; +} + +auto VsUniSystem::IO::frame() -> void { + auto& controls = vsUniSystem.controls; + pollPulse(controls.service, servicePulse); + pollPulse(controls.coins[0], coinPulses[0]); + pollPulse(controls.coins[1], coinPulses[1]); +} + +auto VsUniSystem::IO::power() -> void { + streams[0] = 0; + streams[1] = 0; + positions[0] = 0; + positions[1] = 0; + strobe = 0; + servicePulse = {}; + coinPulses[0] = {}; + coinPulses[1] = {}; +} diff --git a/ares/fc/vs/serialization.cpp b/ares/fc/vs/serialization.cpp new file mode 100644 index 000000000..d8b838115 --- /dev/null +++ b/ares/fc/vs/serialization.cpp @@ -0,0 +1,29 @@ +auto VsUniSystem::Controls::ZapperInput::serialize(serializer& s) -> void { + lightGun.serialize(s); +} + +auto VsUniSystem::Controls::serialize(serializer& s) -> void { + if(input) input->serialize(s); +} + +auto VsUniSystem::DIPSwitches::serialize(serializer& s) -> void { + s(value); +} + +auto VsUniSystem::IO::serialize(serializer& s) -> void { + s(streams); + s(positions); + s(strobe); + s(servicePulse.held); + s(servicePulse.frames); + s(coinPulses[0].held); + s(coinPulses[0].frames); + s(coinPulses[1].held); + s(coinPulses[1].frames); +} + +auto VsUniSystem::serialize(serializer& s) -> void { + s(controls); + s(dipSwitches); + s(io); +} diff --git a/ares/fc/vs/standard-input.cpp b/ares/fc/vs/standard-input.cpp new file mode 100644 index 000000000..8f434e93f --- /dev/null +++ b/ares/fc/vs/standard-input.cpp @@ -0,0 +1,65 @@ +VsUniSystem::Controls::StandardInput::StandardInput(Node::Object parent, Wiring wiring, n8 fixedBits) + : wiring(wiring), fixedBits(fixedBits) { + loadPlayer(parent, players[0], 1); + loadPlayer(parent, players[1], 2); + + if(wiring == Wiring::Standard) live[0] = players[1].a, live[1] = players[0].a; + if(wiring == Wiring::Swapped) live[0] = players[0].a, live[1] = players[1].a; + if(wiring == Wiring::SwapAB) live[0] = players[1].b, live[1] = players[0].b; +} + +auto VsUniSystem::Controls::StandardInput::loadPlayer(Node::Object parent, Player& player, u32 id) -> void { + auto prefix = string{"Player ", id, " "}; + + player.up = parent->append(string{prefix, "Up" }); + player.down = parent->append(string{prefix, "Down" }); + player.left = parent->append(string{prefix, "Left" }); + player.right = parent->append(string{prefix, "Right" }); + player.a = parent->append(string{prefix, "Button 1"}); + player.b = parent->append(string{prefix, "Button 2"}); + player.start = parent->append(string{prefix, "Start" }); +} + +auto VsUniSystem::Controls::StandardInput::pollPlayer(u32 player) -> n8 { + auto& controls = players[player]; + platform->input(controls.a); + platform->input(controls.b); + platform->input(controls.start); + platform->input(controls.up); + platform->input(controls.down); + platform->input(controls.left); + platform->input(controls.right); + + n8 data = fixedBits; + data.bit(0) = controls.a->value(); + data.bit(1) = controls.b->value(); + data.bit(2) = controls.start->value(); + data.bit(4) = controls.up->value(); + data.bit(5) = controls.down->value(); + data.bit(6) = controls.left->value(); + data.bit(7) = controls.right->value(); + return data; +} + +auto VsUniSystem::Controls::StandardInput::data(u32 stream) -> n1 { + platform->input(live[stream]); + return live[stream]->value(); +} + +auto VsUniSystem::Controls::StandardInput::latch() -> std::array { + auto player1 = pollPlayer(0); + auto player2 = pollPlayer(1); + if(wiring == Wiring::Swapped) return {player1, player2}; + + player2.bit(2) = players[0].start->value(); + player1.bit(2) = players[1].start->value(); + if(wiring == Wiring::SwapAB) { + n1 player1A = player1.bit(0); + n1 player2A = player2.bit(0); + player1.bit(0) = player1.bit(1); + player1.bit(1) = player1A; + player2.bit(0) = player2.bit(1); + player2.bit(1) = player2A; + } + return {player2, player1}; +} diff --git a/ares/fc/vs/unisystem.cpp b/ares/fc/vs/unisystem.cpp new file mode 100644 index 000000000..66cb34891 --- /dev/null +++ b/ares/fc/vs/unisystem.cpp @@ -0,0 +1,43 @@ +#include + +namespace ares::Famicom { + +VsUniSystem vsUniSystem; + +#include "standard-input.cpp" +#include "zapper-input.cpp" +#include "controls.cpp" +#include "dip-switches.cpp" +#include "io.cpp" +#include "serialization.cpp" + +auto VsUniSystem::load(Node::Object parent) -> void { + this->parent = parent; + node = parent->append("Arcade"); +} + +auto VsUniSystem::unload() -> void { + dipSwitches.unload(); + controls.unload(); + io.power(); + node.reset(); + parent.reset(); +} + +auto VsUniSystem::power(bool reset) -> void { + io.power(); + if(reset) return; + + if(!controls.load(node)) return; + if(!dipSwitches.load(parent)) { + controls.unload(); + return; + } +} + +auto VsUniSystem::frame() -> void { + controls.frame(); + io.frame(); +} + +} diff --git a/ares/fc/vs/unisystem.hpp b/ares/fc/vs/unisystem.hpp new file mode 100644 index 000000000..798d67aa4 --- /dev/null +++ b/ares/fc/vs/unisystem.hpp @@ -0,0 +1,135 @@ +struct VsUniSystem { + struct Controls { + Node::Object node; + Node::Input::Button service; + Node::Input::Button coins[2]; + + auto load(Node::Object parent) -> bool; + auto unload() -> void; + auto frame() -> void; + auto data(u32 stream) -> n1; + auto latch() -> std::array; + auto serialize(serializer&) -> void; + + private: + struct Input { + virtual ~Input() = default; + virtual auto frame() -> void {} + virtual auto data(u32 stream) -> n1 = 0; + virtual auto latch() -> std::array = 0; + virtual auto serialize(serializer&) -> void {} + }; + + struct StandardInput : Input { + enum class Wiring : u32 { + Standard, + Swapped, + SwapAB, + } wiring; + n8 fixedBits; + + struct Player { + Node::Input::Button up; + Node::Input::Button down; + Node::Input::Button left; + Node::Input::Button right; + Node::Input::Button a; + Node::Input::Button b; + Node::Input::Button start; + } players[2]; + Node::Input::Button live[2]; + + StandardInput(Node::Object parent, Wiring wiring, n8 fixedBits); + + auto data(u32 stream) -> n1 override; + auto latch() -> std::array override; + + private: + auto loadPlayer(Node::Object parent, Player& player, u32 id) -> void; + auto pollPlayer(u32 player) -> n8; + }; + + struct ZapperInput : Input { + Node::Input::Axis x; + Node::Input::Axis y; + Node::Input::Button trigger; + LightGun lightGun; + + ZapperInput(Node::Object parent); + ~ZapperInput(); + + auto frame() -> void override; + auto data(u32 stream) -> n1 override; + auto latch() -> std::array override; + auto serialize(serializer&) -> void override; + }; + + std::unique_ptr input; + } controls; + + struct DIPSwitches { + struct Option { + string name; + u8 value = 0; + }; + + struct Setting { + Node::Setting::String node; + u8 mask = 0; + std::vector