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 000000000..2c8c40372 Binary files /dev/null and b/ares/ares/resource/sprite/fc/crosshair.png differ 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