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.
## 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