548 lines
18 KiB
C++
548 lines
18 KiB
C++
// blatantly stolen from target-libretro
|
|
|
|
#include <heuristics/heuristics.hpp>
|
|
#include <heuristics/heuristics.cpp>
|
|
#include <heuristics/super-famicom.cpp>
|
|
#include <heuristics/game-boy.cpp>
|
|
#include <heuristics/bs-memory.cpp>
|
|
|
|
#include "resources.hpp"
|
|
#include <nall/vfs/biz_file.hpp>
|
|
#include <vector>
|
|
|
|
static Emulator::Interface *emulator;
|
|
static std::vector<short> audioBuffer;
|
|
|
|
struct Program : Emulator::Platform
|
|
{
|
|
Program();
|
|
|
|
auto open(uint id, string name, vfs::file::mode mode, bool required) -> shared_pointer<vfs::file> override;
|
|
auto load(uint id, string name, string type, vector<string> options = {}) -> Emulator::Platform::Load override;
|
|
auto videoFrame(const uint16* data, uint pitch, uint width, uint height, uint scale) -> void override;
|
|
auto audioFrame(const double* samples, uint channels) -> void override;
|
|
auto inputPoll(uint port, uint device, uint input) -> int16 override;
|
|
auto inputRumble(uint port, uint device, uint input, bool enable) -> void override;
|
|
auto notify(string text) -> void override;
|
|
auto getBackdropColor() -> uint16 override;
|
|
auto cpuTrace(vector<string>) -> void override;
|
|
auto readHook(uint address) -> void override;
|
|
auto writeHook(uint address, uint8 value) -> void override;
|
|
auto execHook(uint address) -> void override;
|
|
auto time() -> int64 override;
|
|
|
|
auto load() -> void;
|
|
auto loadSuperFamicom() -> bool;
|
|
auto loadGameBoy() -> bool;
|
|
auto loadBSMemory() -> bool;
|
|
|
|
auto save() -> void;
|
|
|
|
auto openFileSuperFamicom(string name, vfs::file::mode mode, bool required) -> shared_pointer<vfs::file>;
|
|
auto openFileGameBoy(string name, vfs::file::mode mode, bool required) -> shared_pointer<vfs::file>;
|
|
|
|
auto hackPatchMemory(vector<uint8_t>& data) -> void;
|
|
|
|
bool overscan = false;
|
|
uint16_t backdropColor;
|
|
int regionOverride = 0;
|
|
bool breakOnLatch;
|
|
|
|
public:
|
|
struct Game {
|
|
string option;
|
|
string manifest;
|
|
Markup::Node document;
|
|
boolean patched;
|
|
boolean verified;
|
|
};
|
|
|
|
struct SuperFamicom : Game {
|
|
vector<uint8_t> raw_data;
|
|
string title;
|
|
string region;
|
|
vector<uint8_t> program;
|
|
vector<uint8_t> data;
|
|
vector<uint8_t> expansion;
|
|
vector<uint8_t> firmware;
|
|
} superFamicom;
|
|
|
|
struct GameBoy : Game {
|
|
vector<uint8_t> program;
|
|
} gameBoy;
|
|
|
|
struct BSMemory : Game {
|
|
vector<uint8_t> program;
|
|
} bsMemory;
|
|
};
|
|
|
|
static Program *program = nullptr;
|
|
|
|
Program::Program()
|
|
{
|
|
platform = this;
|
|
}
|
|
|
|
auto Program::save() -> void
|
|
{
|
|
if(!emulator->loaded()) return;
|
|
emulator->save();
|
|
}
|
|
|
|
auto Program::open(uint id, string name, vfs::file::mode mode, bool required) -> shared_pointer<vfs::file>
|
|
{
|
|
fprintf(stderr, "name \"%s\" was requested\n", name.data());
|
|
|
|
shared_pointer<vfs::file> result;
|
|
|
|
if (name == "ipl.rom" && mode == File::Read) {
|
|
result = vfs::memory::file::open(iplrom, sizeof(iplrom));
|
|
}
|
|
|
|
if (name == "boards.bml" && mode == File::Read) {
|
|
result = vfs::memory::file::open(Boards, sizeof(Boards));
|
|
}
|
|
|
|
if (id == 1) { //Super Famicom
|
|
if (name == "manifest.bml" && mode == File::Read) {
|
|
result = vfs::memory::file::open(superFamicom.manifest.data<uint8_t>(), superFamicom.manifest.size());
|
|
}
|
|
else if (name == "program.rom" && mode == File::Read) {
|
|
result = vfs::memory::file::open(superFamicom.program.data(), superFamicom.program.size());
|
|
}
|
|
else if (name == "data.rom" && mode == File::Read) {
|
|
result = vfs::memory::file::open(superFamicom.data.data(), superFamicom.data.size());
|
|
}
|
|
else if (name == "expansion.rom" && mode == File::Read) {
|
|
result = vfs::memory::file::open(superFamicom.expansion.data(), superFamicom.expansion.size());
|
|
}
|
|
else {
|
|
result = openFileSuperFamicom(name, mode, required);
|
|
}
|
|
}
|
|
else if (id == 2) { //Game Boy
|
|
if (name == "manifest.bml" && mode == File::Read) {
|
|
result = vfs::memory::file::open(gameBoy.manifest.data<uint8_t>(), gameBoy.manifest.size());
|
|
}
|
|
else if (name == "program.rom" && mode == File::Read) {
|
|
result = vfs::memory::file::open(gameBoy.program.data(), gameBoy.program.size());
|
|
}
|
|
else {
|
|
result = openFileGameBoy(name, mode, required);
|
|
}
|
|
}
|
|
else if (id == 3) { //BS Memory
|
|
if (name == "manifest.bml" && mode == File::Read) {
|
|
result = vfs::memory::file::open(bsMemory.manifest.data<uint8_t>(), bsMemory.manifest.size());
|
|
}
|
|
else if (name == "program.rom" && mode == File::Read) {
|
|
result = vfs::memory::file::open(bsMemory.program.data(), bsMemory.program.size());
|
|
}
|
|
else if(name == "program.flash") {
|
|
//writes are not flushed to disk in bsnes
|
|
result = vfs::memory::file::open(bsMemory.program.data(), bsMemory.program.size());
|
|
}
|
|
else {
|
|
result = {};
|
|
}
|
|
// sufami turbo would be id 4 and 5 and is ignored for reasons? do we support it in bizhawk? TODO check this
|
|
}
|
|
return result;
|
|
}
|
|
|
|
auto Program::openFileSuperFamicom(string name, vfs::file::mode mode, bool required) -> shared_pointer<vfs::file>
|
|
{
|
|
// TODO: the original bsnes code handles a lot more paths; *.data.ram, time.rtc and download.ram
|
|
// I believe none of these can currently be correctly served by bizhawk and I therefor ignore them here
|
|
// This should probably be changed? Not sure how much can break from not having them
|
|
if(name == "msu1/data.rom" || name == "save.ram")
|
|
{
|
|
return vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, name, required), mode);
|
|
}
|
|
|
|
if(name.match("msu1/track*.pcm"))
|
|
{
|
|
snesCallbacks.snes_msu_open(strtol(name.data() + 11, nullptr, 10));
|
|
return shared_pointer<vfs::biz_file>{new vfs::biz_file};
|
|
}
|
|
|
|
if(name == "arm6.program.rom" && mode == File::Read) {
|
|
if(superFamicom.firmware.size() == 0x28000) {
|
|
return vfs::memory::file::open(&superFamicom.firmware.data()[0x00000], 0x20000);
|
|
}
|
|
if(auto memory = superFamicom.document["game/board/memory(type=ROM,content=Program,architecture=ARM6)"]) {
|
|
return vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, memory["identifier"].text().downcase(), required), mode);
|
|
}
|
|
}
|
|
|
|
if(name == "arm6.data.rom" && mode == File::Read) {
|
|
if(superFamicom.firmware.size() == 0x28000) {
|
|
return vfs::memory::file::open(&superFamicom.firmware.data()[0x20000], 0x08000);
|
|
}
|
|
if(auto memory = superFamicom.document["game/board/memory(type=ROM,content=Data,architecture=ARM6)"]) {
|
|
auto file = vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, memory["identifier"].text().downcase(), required), mode);
|
|
if (file) file->seek(0x20000);
|
|
return file;
|
|
}
|
|
}
|
|
|
|
if(name == "hg51bs169.data.rom" && mode == File::Read) {
|
|
if(superFamicom.firmware.size() == 0xc00) {
|
|
return vfs::memory::file::open(superFamicom.firmware.data(), superFamicom.firmware.size());
|
|
}
|
|
if(auto memory = superFamicom.document["game/board/memory(type=ROM,content=Data,architecture=HG51BS169)"]) {
|
|
return vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, memory["identifier"].text().downcase(), required), mode);
|
|
}
|
|
}
|
|
|
|
if(name == "lr35902.boot.rom" && mode == File::Read) {
|
|
if(superFamicom.firmware.size() == 0x100) {
|
|
return vfs::memory::file::open(superFamicom.firmware.data(), superFamicom.firmware.size());
|
|
}
|
|
if(auto memory = superFamicom.document["game/board/memory(type=ROM,content=Boot,architecture=LR35902)"]) {
|
|
return vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, memory["identifier"].text().downcase(), required), mode);
|
|
}
|
|
}
|
|
|
|
if(name == "upd7725.program.rom" && mode == File::Read) {
|
|
if(superFamicom.firmware.size() == 0x2000) {
|
|
return vfs::memory::file::open(&superFamicom.firmware.data()[0x0000], 0x1800);
|
|
}
|
|
if(auto memory = superFamicom.document["game/board/memory(type=ROM,content=Program,architecture=uPD7725)"]) {
|
|
auto file = vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, memory["identifier"].text().downcase(), required), mode);
|
|
return file;
|
|
}
|
|
}
|
|
|
|
if(name == "upd7725.data.rom" && mode == File::Read) {
|
|
if(superFamicom.firmware.size() == 0x2000) {
|
|
return vfs::memory::file::open(&superFamicom.firmware.data()[0x1800], 0x0800);
|
|
}
|
|
if(auto memory = superFamicom.document["game/board/memory(type=ROM,content=Data,architecture=uPD7725)"]) {
|
|
auto file = vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, memory["identifier"].text().downcase(), required), mode);
|
|
if (file) file->seek(0x1800);
|
|
return file;
|
|
}
|
|
}
|
|
|
|
if(name == "upd96050.program.rom" && mode == File::Read) {
|
|
if(superFamicom.firmware.size() == 0xd000) {
|
|
return vfs::memory::file::open(&superFamicom.firmware.data()[0x0000], 0xc000);
|
|
}
|
|
if(auto memory = superFamicom.document["game/board/memory(type=ROM,content=Program,architecture=uPD96050)"]) {
|
|
auto file = vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, memory["identifier"].text().downcase(), required), mode);
|
|
return file;
|
|
}
|
|
}
|
|
|
|
if(name == "upd96050.data.rom" && mode == File::Read) {
|
|
if(superFamicom.firmware.size() == 0xd000) {
|
|
return vfs::memory::file::open(&superFamicom.firmware.data()[0xc000], 0x1000);
|
|
}
|
|
if(auto memory = superFamicom.document["game/board/memory(type=ROM,content=Data,architecture=uPD96050)"]) {
|
|
auto file = vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, memory["identifier"].text().downcase(), required), mode);
|
|
if (file) file->seek(0xc000);
|
|
return file;
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
auto Program::openFileGameBoy(string name, vfs::file::mode mode, bool required) -> shared_pointer<vfs::file>
|
|
{
|
|
if(name == "save.ram")
|
|
{
|
|
return vfs::fs::file::open(snesCallbacks.snes_path_request(ID::GameBoy, name, required), mode);
|
|
}
|
|
|
|
if(name == "time.rtc")
|
|
{
|
|
return vfs::fs::file::open(snesCallbacks.snes_path_request(ID::GameBoy, name, required), mode);
|
|
}
|
|
|
|
if(name == "sgb")
|
|
{
|
|
return vfs::fs::file::open(snesCallbacks.snes_path_request(ID::GameBoy, name, required), mode);
|
|
}
|
|
|
|
if(name == "sgb2")
|
|
{
|
|
return vfs::fs::file::open(snesCallbacks.snes_path_request(ID::GameBoy, name, required), mode);
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
auto Program::load() -> void {
|
|
emulator->unload();
|
|
emulator->load();
|
|
|
|
// per-game hack overrides
|
|
auto title = superFamicom.title;
|
|
auto region = superFamicom.region;
|
|
|
|
//relies on mid-scanline rendering techniques
|
|
if(title == "AIR STRIKE PATROL" || title == "DESERT FIGHTER") emulator->configure("Hacks/PPU/Fast", false);
|
|
|
|
//the dialogue text is blurry due to an issue in the scanline-based renderer's color math support
|
|
if(title == "マーヴェラス") emulator->configure("Hacks/PPU/Fast", false);
|
|
|
|
//stage 2 uses pseudo-hires in a way that's not compatible with the scanline-based renderer
|
|
if(title == "SFC クレヨンシンチャン") emulator->configure("Hacks/PPU/Fast", false);
|
|
|
|
//title screen game select (after choosing a game) changes OAM tiledata address mid-frame
|
|
//this is only supported by the cycle-based PPU renderer
|
|
if(title == "Winter olympics") emulator->configure("Hacks/PPU/Fast", false);
|
|
|
|
//title screen shows remnants of the flag after choosing a language with the scanline-based renderer
|
|
if(title == "WORLD CUP STRIKER") emulator->configure("Hacks/PPU/Fast", false);
|
|
|
|
//relies on cycle-accurate writes to the echo buffer
|
|
if(title == "KOUSHIEN_2") emulator->configure("Hacks/DSP/Fast", false);
|
|
|
|
//will hang immediately
|
|
if(title == "RENDERING RANGER R2") emulator->configure("Hacks/DSP/Fast", false);
|
|
|
|
//will hang sometimes in the "Bach in Time" stage
|
|
if(title == "BUBSY II" && region == "PAL") emulator->configure("Hacks/DSP/Fast", false);
|
|
|
|
//fixes an errant scanline on the title screen due to writing to PPU registers too late
|
|
if(title == "ADVENTURES OF FRANKEN" && region == "PAL") emulator->configure("Hacks/PPU/RenderCycle", 32);
|
|
|
|
//fixes an errant scanline on the title screen due to writing to PPU registers too late
|
|
if(title == "FIREPOWER 2000" || title == "SUPER SWIV") emulator->configure("Hacks/PPU/RenderCycle", 32);
|
|
|
|
//fixes an errant scanline on the title screen due to writing to PPU registers too late
|
|
if(title == "NHL '94" || title == "NHL PROHOCKEY'94") emulator->configure("Hacks/PPU/RenderCycle", 32);
|
|
|
|
//fixes an errant scanline on the title screen due to writing to PPU registers too late
|
|
if(title == "Sugoro Quest++") emulator->configure("Hacks/PPU/RenderCycle", 128);
|
|
|
|
if (emulator->configuration("Hacks/Hotfixes")) {
|
|
//this game transfers uninitialized memory into video RAM: this can cause a row of invalid tiles
|
|
//to appear in the background of stage 12. this one is a bug in the original game, so only enable
|
|
//it if the hotfixes option has been enabled.
|
|
if(title == "The Hurricanes") emulator->configure("Hacks/Entropy", "None");
|
|
|
|
//Frisky Tom attract sequence sometimes hangs when WRAM is initialized to pseudo-random patterns
|
|
if (title == "ニチブツ・アーケード・クラシックス") emulator->configure("Hacks/Entropy", "None");
|
|
}
|
|
|
|
emulator->power();
|
|
bus.lock();
|
|
}
|
|
|
|
auto Program::load(uint id, string name, string type, vector<string> options) -> Emulator::Platform::Load {
|
|
// This needs to occur here rather than snes_init, as callbacks aren't set yet then
|
|
emulator->synchronize(time());
|
|
|
|
if (id == 1)
|
|
{
|
|
if (loadSuperFamicom())
|
|
{
|
|
return {id, superFamicom.region};
|
|
}
|
|
}
|
|
else if (id == 2)
|
|
{
|
|
if (loadGameBoy())
|
|
{
|
|
return { id, NULL };
|
|
}
|
|
}
|
|
else if (id == 3)
|
|
{
|
|
if (loadBSMemory())
|
|
{
|
|
return { id, NULL };
|
|
}
|
|
}
|
|
return { id, options(0) };
|
|
}
|
|
|
|
auto Program::loadSuperFamicom() -> bool
|
|
{
|
|
vector<uint8_t>& rom = superFamicom.raw_data;
|
|
fprintf(stderr, "rom size: %ld\n", rom.size());
|
|
|
|
if(rom.size() < 0x8000) return false;
|
|
|
|
auto heuristics = Heuristics::SuperFamicom(rom, "");
|
|
|
|
superFamicom.title = heuristics.title();
|
|
switch (regionOverride) {
|
|
case 0: superFamicom.region = heuristics.videoRegion(); break;
|
|
case 1: superFamicom.region = "NTSC"; break;
|
|
case 2: superFamicom.region = "PAL"; break;
|
|
}
|
|
if (auto fp = vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, "manifest.bml", false), File::Read)) {
|
|
superFamicom.manifest = fp->reads();
|
|
} else {
|
|
superFamicom.manifest = heuristics.manifest();
|
|
}
|
|
|
|
hackPatchMemory(rom);
|
|
superFamicom.document = BML::unserialize(superFamicom.manifest);
|
|
fprintf(stderr, "loaded game manifest: \"\n%s\"\n", superFamicom.manifest.data());
|
|
|
|
uint offset = 0;
|
|
if(auto size = heuristics.programRomSize()) {
|
|
superFamicom.program.acquire(rom.data() + offset, size);
|
|
offset += size;
|
|
}
|
|
if(auto size = heuristics.dataRomSize()) {
|
|
superFamicom.data.acquire(rom.data() + offset, size);
|
|
offset += size;
|
|
}
|
|
if(auto size = heuristics.expansionRomSize()) {
|
|
superFamicom.expansion.acquire(rom.data() + offset, size);
|
|
offset += size;
|
|
}
|
|
if(auto size = heuristics.firmwareRomSize()) {
|
|
superFamicom.firmware.acquire(rom.data() + offset, size);
|
|
offset += size;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
auto Program::loadGameBoy() -> bool {
|
|
if (gameBoy.program.size() < 0x4000) return false;
|
|
|
|
auto heuristics = Heuristics::GameBoy(gameBoy.program, "");
|
|
|
|
gameBoy.manifest = heuristics.manifest();
|
|
gameBoy.document = BML::unserialize(gameBoy.manifest);
|
|
|
|
return true;
|
|
}
|
|
|
|
auto Program::loadBSMemory() -> bool {
|
|
if (bsMemory.program.size() < 0x8000) return false;
|
|
|
|
auto heuristics = Heuristics::BSMemory(bsMemory.program, "");
|
|
|
|
bsMemory.manifest = heuristics.manifest();
|
|
bsMemory.document = BML::unserialize(bsMemory.manifest);
|
|
|
|
return true;
|
|
}
|
|
|
|
auto Program::videoFrame(const uint16* data, uint pitch, uint width, uint height, uint scale) -> void {
|
|
|
|
// note: scale is not used currently, but as bsnes has builtin scaling support (something something mode 7)
|
|
// we might actually wanna make use of that?
|
|
pitch >>= 1;
|
|
if (!overscan)
|
|
{
|
|
uint multiplier = height / 240;
|
|
data += 8 * pitch * multiplier;
|
|
height -= 16 * multiplier;
|
|
}
|
|
|
|
// fprintf(stderr, "got a video frame with dimensions h: %d, w: %d, p: %d, overscan: %d, scale: %d\n", height, width, pitch, overscan, scale);
|
|
|
|
snesCallbacks.snes_video_frame(data, width, height, pitch);
|
|
}
|
|
|
|
// Double the fun!
|
|
static int16_t d2i16(double v)
|
|
{
|
|
v *= 0x8000;
|
|
if (v > 0x7fff)
|
|
v = 0x7fff;
|
|
else if (v < -0x8000)
|
|
v = -0x8000;
|
|
return int16_t(floor(v + 0.5));
|
|
}
|
|
|
|
auto Program::audioFrame(const double* samples, uint channels) -> void
|
|
{
|
|
audioBuffer.push_back(d2i16(samples[0]));
|
|
audioBuffer.push_back(d2i16(samples[1]));
|
|
}
|
|
|
|
auto Program::notify(string message) -> void
|
|
{
|
|
if (message == "NO_LAG")
|
|
snesCallbacks.snes_no_lag(false);
|
|
else if (message == "NO_LAG_SGB")
|
|
snesCallbacks.snes_no_lag(true);
|
|
else if (message == "LATCH") {
|
|
if (breakOnLatch) {
|
|
scheduler.StepOnce = true;
|
|
breakOnLatch = false;
|
|
}
|
|
snesCallbacks.snes_controller_latch();
|
|
}
|
|
}
|
|
|
|
auto Program::cpuTrace(vector<string> parts) -> void
|
|
{
|
|
snesCallbacks.snes_trace(parts[0], parts[1]);
|
|
}
|
|
|
|
auto Program::readHook(uint address) -> void
|
|
{
|
|
snesCallbacks.snes_read_hook(address);
|
|
}
|
|
|
|
auto Program::writeHook(uint address, uint8 value) -> void
|
|
{
|
|
snesCallbacks.snes_write_hook(address, value);
|
|
}
|
|
|
|
auto Program::execHook(uint address) -> void
|
|
{
|
|
snesCallbacks.snes_exec_hook(address);
|
|
}
|
|
|
|
auto Program::time() -> int64
|
|
{
|
|
return snesCallbacks.snes_time();
|
|
}
|
|
|
|
auto Program::getBackdropColor() -> uint16
|
|
{
|
|
return backdropColor;
|
|
}
|
|
|
|
auto Program::inputPoll(uint port, uint device, uint input) -> int16
|
|
{
|
|
int index = 0;
|
|
int id = input;
|
|
if (device == ID::Device::SuperMultitap) {
|
|
index = input / 12;
|
|
id = input % 12;
|
|
} else if (device == ID::Device::Payload) {
|
|
index = input / 16;
|
|
id = input % 16;
|
|
} else if (device == ID::Device::Justifiers) {
|
|
index = input / 4;
|
|
id = input % 4;
|
|
}
|
|
|
|
return snesCallbacks.snes_input_poll(port, index, id);
|
|
}
|
|
|
|
auto Program::inputRumble(uint port, uint device, uint input, bool enable) -> void
|
|
{
|
|
}
|
|
|
|
|
|
auto Program::hackPatchMemory(vector<uint8_t>& data) -> void
|
|
{
|
|
auto title = superFamicom.title;
|
|
|
|
if(title == "Satellaview BS-X" && data.size() >= 0x100000) {
|
|
//BS-X: Sore wa Namae o Nusumareta Machi no Monogatari (JPN) (1.1)
|
|
//disable limited play check for BS Memory flash cartridges
|
|
//benefit: allow locked out BS Memory flash games to play without manual header patching
|
|
//detriment: BS Memory ROM cartridges will cause the game to hang in the load menu
|
|
if(data[0x4a9b] == 0x10) data[0x4a9b] = 0x80;
|
|
if(data[0x4d6d] == 0x10) data[0x4d6d] = 0x80;
|
|
if(data[0x4ded] == 0x10) data[0x4ded] = 0x80;
|
|
if(data[0x4e9a] == 0x10) data[0x4e9a] = 0x80;
|
|
}
|
|
}
|