diff --git a/higan/audio/audio.cpp b/higan/audio/audio.cpp index 22132005..dd3ba81e 100644 --- a/higan/audio/audio.cpp +++ b/higan/audio/audio.cpp @@ -39,7 +39,7 @@ auto Audio::createStream(uint channels, double frequency) -> shared_pointer void { - while(true) { + while(streams) { for(auto& stream : streams) { if(!stream->pending()) return; } diff --git a/higan/audio/audio.hpp b/higan/audio/audio.hpp index 7597e256..53cf50fc 100644 --- a/higan/audio/audio.hpp +++ b/higan/audio/audio.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -37,12 +38,13 @@ private: }; struct Filter { - enum class Order : uint { First, Second }; - enum class Type : uint { LowPass, HighPass }; + enum class Mode : uint { DCRemoval, OnePole, Biquad } mode; + enum class Type : uint { None, LowPass, HighPass } type; + enum class Order : uint { None, First, Second } order; - Order order; - DSP::IIR::OnePole onePole; //first-order - DSP::IIR::Biquad biquad; //second-order + DSP::IIR::DCRemoval dcRemoval; + DSP::IIR::OnePole onePole; + DSP::IIR::Biquad biquad; }; struct Stream { @@ -50,7 +52,9 @@ struct Stream { auto setFrequency(double inputFrequency, maybe outputFrequency = nothing) -> void; - auto addFilter(Filter::Order order, Filter::Type type, double cutoffFrequency, uint passes = 1) -> void; + auto addDCRemovalFilter() -> void; + auto addLowPassFilter(double cutoffFrequency, Filter::Order order, uint passes = 1) -> void; + auto addHighPassFilter(double cutoffFrequency, Filter::Order order, uint passes = 1) -> void; auto pending() const -> bool; auto read(double samples[]) -> uint; diff --git a/higan/audio/stream.cpp b/higan/audio/stream.cpp index 587a644d..b9817fb9 100644 --- a/higan/audio/stream.cpp +++ b/higan/audio/stream.cpp @@ -1,14 +1,12 @@ -auto Stream::reset(uint channels_, double inputFrequency, double outputFrequency) -> void { - this->inputFrequency = inputFrequency; - this->outputFrequency = outputFrequency; - +auto Stream::reset(uint channelCount, double inputFrequency, double outputFrequency) -> void { channels.reset(); - channels.resize(channels_); + channels.resize(channelCount); for(auto& channel : channels) { channel.filters.reset(); - channel.resampler.reset(inputFrequency, outputFrequency); } + + setFrequency(inputFrequency, outputFrequency); } auto Stream::setFrequency(double inputFrequency, maybe outputFrequency) -> void { @@ -35,27 +33,46 @@ auto Stream::setFrequency(double inputFrequency, maybe outputFrequency) } } -auto Stream::addFilter(Filter::Order order, Filter::Type type, double cutoffFrequency, uint passes) -> void { +auto Stream::addDCRemovalFilter() -> void { + return; //todo: test to ensure this is desirable before enabling + for(auto& channel : channels) { + Filter filter{Filter::Mode::DCRemoval, Filter::Type::None, Filter::Order::None}; + channel.filters.append(filter); + } +} + +auto Stream::addLowPassFilter(double cutoffFrequency, Filter::Order order, uint passes) -> void { for(auto& channel : channels) { for(uint pass : range(passes)) { - Filter filter{order}; - if(order == Filter::Order::First) { - DSP::IIR::OnePole::Type _type; - if(type == Filter::Type::LowPass) _type = DSP::IIR::OnePole::Type::LowPass; - if(type == Filter::Type::HighPass) _type = DSP::IIR::OnePole::Type::HighPass; - filter.onePole.reset(_type, cutoffFrequency, inputFrequency); + Filter filter{Filter::Mode::OnePole, Filter::Type::LowPass, Filter::Order::First}; + filter.onePole.reset(DSP::IIR::OnePole::Type::LowPass, cutoffFrequency, inputFrequency); + channel.filters.append(filter); } - if(order == Filter::Order::Second) { - DSP::IIR::Biquad::Type _type; - if(type == Filter::Type::LowPass) _type = DSP::IIR::Biquad::Type::LowPass; - if(type == Filter::Type::HighPass) _type = DSP::IIR::Biquad::Type::HighPass; + Filter filter{Filter::Mode::Biquad, Filter::Type::LowPass, Filter::Order::Second}; double q = DSP::IIR::Biquad::butterworth(passes * 2, pass); - filter.biquad.reset(_type, cutoffFrequency, inputFrequency, q); + filter.biquad.reset(DSP::IIR::Biquad::Type::LowPass, cutoffFrequency, inputFrequency, q); + channel.filters.append(filter); } + } + } +} - channel.filters.append(filter); +auto Stream::addHighPassFilter(double cutoffFrequency, Filter::Order order, uint passes) -> void { + for(auto& channel : channels) { + for(uint pass : range(passes)) { + if(order == Filter::Order::First) { + Filter filter{Filter::Mode::OnePole, Filter::Type::HighPass, Filter::Order::First}; + filter.onePole.reset(DSP::IIR::OnePole::Type::HighPass, cutoffFrequency, inputFrequency); + channel.filters.append(filter); + } + if(order == Filter::Order::Second) { + Filter filter{Filter::Mode::Biquad, Filter::Type::HighPass, Filter::Order::Second}; + double q = DSP::IIR::Biquad::butterworth(passes * 2, pass); + filter.biquad.reset(DSP::IIR::Biquad::Type::HighPass, cutoffFrequency, inputFrequency, q); + channel.filters.append(filter); + } } } } @@ -73,9 +90,10 @@ auto Stream::write(const double samples[]) -> void { for(auto c : range(channels.size())) { double sample = samples[c] + 1e-25; //constant offset used to suppress denormals for(auto& filter : channels[c].filters) { - switch(filter.order) { - case Filter::Order::First: sample = filter.onePole.process(sample); break; - case Filter::Order::Second: sample = filter.biquad.process(sample); break; + switch(filter.mode) { + case Filter::Mode::DCRemoval: sample = filter.dcRemoval.process(sample); break; + case Filter::Mode::OnePole: sample = filter.onePole.process(sample); break; + case Filter::Mode::Biquad: sample = filter.biquad.process(sample); break; } } for(auto& filter : channels[c].nyquist) { diff --git a/higan/emulator/emulator.hpp b/higan/emulator/emulator.hpp index f4038825..6f9416fe 100644 --- a/higan/emulator/emulator.hpp +++ b/higan/emulator/emulator.hpp @@ -28,7 +28,7 @@ using namespace nall; namespace Emulator { static const string Name = "higan"; - static const string Version = "106.66"; + static const string Version = "106.67"; static const string Author = "byuu"; static const string License = "GPLv3"; static const string Website = "https://byuu.org/"; diff --git a/higan/fc/apu/apu.cpp b/higan/fc/apu/apu.cpp index 433cfba2..45a55189 100644 --- a/higan/fc/apu/apu.cpp +++ b/higan/fc/apu/apu.cpp @@ -74,9 +74,10 @@ auto APU::setSample(int16 sample) -> void { auto APU::power(bool reset) -> void { create(APU::Enter, system.frequency()); stream = Emulator::audio.createStream(1, frequency() / rate()); - stream->addFilter(Emulator::Filter::Order::First, Emulator::Filter::Type::HighPass, 90.0); - stream->addFilter(Emulator::Filter::Order::First, Emulator::Filter::Type::HighPass, 440.0); - stream->addFilter(Emulator::Filter::Order::First, Emulator::Filter::Type::LowPass, 14000.0); + stream->addHighPassFilter( 90.0, Emulator::Filter::Order::First); + stream->addHighPassFilter( 440.0, Emulator::Filter::Order::First); + stream->addLowPassFilter (14000.0, Emulator::Filter::Order::First); + stream->addDCRemovalFilter(); pulse[0].power(); pulse[1].power(); diff --git a/higan/gb/apu/apu.cpp b/higan/gb/apu/apu.cpp index 347c96ff..851459f3 100644 --- a/higan/gb/apu/apu.cpp +++ b/higan/gb/apu/apu.cpp @@ -55,7 +55,8 @@ auto APU::power() -> void { create(Enter, 2 * 1024 * 1024); if(!Model::SuperGameBoy()) { stream = Emulator::audio.createStream(2, frequency()); - stream->addFilter(Emulator::Filter::Order::First, Emulator::Filter::Type::HighPass, 20.0); + stream->addHighPassFilter(20.0, Emulator::Filter::Order::First); + stream->addDCRemovalFilter(); } for(uint n = 0xff10; n <= 0xff3f; n++) bus.mmio[n] = this; diff --git a/higan/gba/apu/apu.cpp b/higan/gba/apu/apu.cpp index 61aa3a56..b44b64d4 100644 --- a/higan/gba/apu/apu.cpp +++ b/higan/gba/apu/apu.cpp @@ -77,7 +77,8 @@ auto APU::step(uint clocks) -> void { auto APU::power() -> void { create(APU::Enter, system.frequency()); stream = Emulator::audio.createStream(2, frequency() / 64.0); - stream->addFilter(Emulator::Filter::Order::First, Emulator::Filter::Type::HighPass, 20.0); + stream->addHighPassFilter(20.0, Emulator::Filter::Order::First); + stream->addDCRemovalFilter(); clock = 0; square1.power(); diff --git a/higan/md/psg/psg.cpp b/higan/md/psg/psg.cpp index f9f6b6ca..391b7282 100644 --- a/higan/md/psg/psg.cpp +++ b/higan/md/psg/psg.cpp @@ -37,8 +37,9 @@ auto PSG::step(uint clocks) -> void { auto PSG::power(bool reset) -> void { create(PSG::Enter, system.frequency() / 15.0); stream = Emulator::audio.createStream(1, frequency() / 16.0); - stream->addFilter(Emulator::Filter::Order::First, Emulator::Filter::Type::HighPass, 20.0); - stream->addFilter(Emulator::Filter::Order::First, Emulator::Filter::Type::LowPass, 2840.0); + stream->addHighPassFilter( 20.0, Emulator::Filter::Order::First); + stream->addLowPassFilter (2840.0, Emulator::Filter::Order::First); + stream->addDCRemovalFilter(); select = 0; for(auto n : range(15)) { diff --git a/higan/md/ym2612/ym2612.cpp b/higan/md/ym2612/ym2612.cpp index c3bf5fde..888cb22c 100644 --- a/higan/md/ym2612/ym2612.cpp +++ b/higan/md/ym2612/ym2612.cpp @@ -157,8 +157,9 @@ auto YM2612::step(uint clocks) -> void { auto YM2612::power(bool reset) -> void { create(YM2612::Enter, system.frequency() / 7.0); stream = Emulator::audio.createStream(2, frequency() / 144.0); - stream->addFilter(Emulator::Filter::Order::First, Emulator::Filter::Type::HighPass, 20.0); - stream->addFilter(Emulator::Filter::Order::First, Emulator::Filter::Type::LowPass, 2840.0); + stream->addHighPassFilter( 20.0, Emulator::Filter::Order::First); + stream->addLowPassFilter (2840.0, Emulator::Filter::Order::First); + stream->addDCRemovalFilter(); io = {}; lfo = {}; diff --git a/higan/ms/cartridge/cartridge.cpp b/higan/ms/cartridge/cartridge.cpp index 81611903..620fb395 100644 --- a/higan/ms/cartridge/cartridge.cpp +++ b/higan/ms/cartridge/cartridge.cpp @@ -9,6 +9,20 @@ Cartridge cartridge; auto Cartridge::load() -> bool { information = {}; + if(Model::SG1000()) { + if(auto loaded = platform->load(ID::SG1000, "SG-1000", "sg1000", {"NTSC", "PAL"})) { + information.pathID = loaded.pathID; + information.region = loaded.option; + } else return false; + } + + if(Model::SC3000()) { + if(auto loaded = platform->load(ID::SC3000, "SC-3000", "sc3000", {"NTSC", "PAL"})) { + information.pathID = loaded.pathID; + information.region = loaded.option; + } else return false; + } + if(Model::MasterSystem()) { if(auto loaded = platform->load(ID::MasterSystem, "Master System", "ms", {"NTSC", "PAL"})) { information.pathID = loaded.pathID; diff --git a/higan/ms/controller/controller.cpp b/higan/ms/controller/controller.cpp index 91bcfa38..618b96fb 100644 --- a/higan/ms/controller/controller.cpp +++ b/higan/ms/controller/controller.cpp @@ -32,7 +32,7 @@ auto Controller::main() -> void { auto ControllerPort::connect(uint deviceID) -> void { delete device; if(!system.loaded()) return; - if(!Model::MasterSystem()) return; + if(Model::GameGear()) return; switch(deviceID) { default: case ID::Device::None: device = new Controller(port); break; diff --git a/higan/ms/cpu/bus.cpp b/higan/ms/cpu/bus.cpp index 48c1a6dd..45dbf6d9 100644 --- a/higan/ms/cpu/bus.cpp +++ b/higan/ms/cpu/bus.cpp @@ -42,6 +42,16 @@ auto CPU::in(uint8 addr) -> uint8 { } case 3: { + if(Model::SG1000() || Model::SC3000()) { + auto port1 = controllerPort1.device->readData(); + auto port2 = controllerPort2.device->readData(); + if(addr.bit(0) == 0) { + return port1.bits(0,5) << 0 | port2.bits(0,1) << 6; + } else { + return port2.bits(2,5) << 0 | 1 << 4 | 1 << 5 | port1.bit(6) << 6 | port2.bit(6) << 7; + } + } + if(Model::MasterSystem()) { bool reset = !platform->inputPoll(ID::Port::Hardware, ID::Device::MasterSystemControls, 0); auto port1 = controllerPort1.device->readData(); diff --git a/higan/ms/cpu/cpu.cpp b/higan/ms/cpu/cpu.cpp index a60aef25..6e023c03 100644 --- a/higan/ms/cpu/cpu.cpp +++ b/higan/ms/cpu/cpu.cpp @@ -37,6 +37,13 @@ auto CPU::synchronizing() const -> bool { //called once per frame auto CPU::pollPause() -> void { + if(Model::SG1000() || Model::SC3000()) { + static bool pause = 0; + bool state = platform->inputPoll(ID::Port::Hardware, ID::Device::SG1000Controls, 0); + if(!pause && state) setNMI(1); + pause = state; + } + if(Model::MasterSystem()) { static bool pause = 0; bool state = platform->inputPoll(ID::Port::Hardware, ID::Device::MasterSystemControls, 1); diff --git a/higan/ms/interface/interface.cpp b/higan/ms/interface/interface.cpp index e7d55251..291ac599 100644 --- a/higan/ms/interface/interface.cpp +++ b/higan/ms/interface/interface.cpp @@ -3,6 +3,8 @@ namespace MasterSystem { Settings settings; +#include "sg-1000.cpp" +#include "sc-3000.cpp" #include "master-system.cpp" #include "game-gear.cpp" diff --git a/higan/ms/interface/interface.hpp b/higan/ms/interface/interface.hpp index c2d2e537..c1a42546 100644 --- a/higan/ms/interface/interface.hpp +++ b/higan/ms/interface/interface.hpp @@ -5,6 +5,8 @@ namespace MasterSystem { struct ID { enum : uint { System, + SG1000, + SC3000, MasterSystem, GameGear, }; @@ -17,6 +19,8 @@ struct ID { struct Device { enum : uint { None, + SG1000Controls, + SC3000Controls, MasterSystemControls, GameGearControls, Gamepad, @@ -44,6 +48,38 @@ struct Interface : Emulator::Interface { auto set(const string& name, const any& value) -> bool override; }; +struct SG1000Interface : Interface { + auto information() -> Information override; + + auto displays() -> vector override; + auto color(uint32 color) -> uint64 override; + + auto ports() -> vector override; + auto devices(uint port) -> vector override; + auto inputs(uint device) -> vector override; + + auto load() -> bool override; + + auto connected(uint port) -> uint override; + auto connect(uint port, uint device) -> void override; +}; + +struct SC3000Interface : Interface { + auto information() -> Information override; + + auto displays() -> vector override; + auto color(uint32 color) -> uint64 override; + + auto ports() -> vector override; + auto devices(uint port) -> vector override; + auto inputs(uint device) -> vector override; + + auto load() -> bool override; + + auto connected(uint port) -> uint override; + auto connect(uint port, uint device) -> void override; +}; + struct MasterSystemInterface : Interface { auto information() -> Information override; diff --git a/higan/ms/interface/sc-3000.cpp b/higan/ms/interface/sc-3000.cpp new file mode 100644 index 00000000..429ecc16 --- /dev/null +++ b/higan/ms/interface/sc-3000.cpp @@ -0,0 +1,105 @@ +auto SC3000Interface::information() -> Information { + Information information; + information.manufacturer = "Sega"; + information.name = "SC-3000"; + information.extension = "sc3000"; + return information; +} + +auto SC3000Interface::displays() -> vector { + Display display; + display.type = Display::Type::CRT; + display.colors = 1 << 4; + display.width = 256; + display.height = 192; + display.internalWidth = 256; + display.internalHeight = 192; + display.aspectCorrection = 1.0; + if(Region::NTSC()) display.refreshRate = (system.colorburst() * 15.0 / 5.0) / (262.0 * 684.0); + if(Region::PAL()) display.refreshRate = (system.colorburst() * 15.0 / 5.0) / (312.0 * 684.0); + return {display}; +} + +auto SC3000Interface::color(uint32 color) -> uint64 { + switch(color.bits(0,3)) { + case 0: return 0x0000'0000'0000ull; //transparent + case 1: return 0x0000'0000'0000ull; //black + case 2: return 0x2121'c8c8'4242ull; //medium green + case 3: return 0x5e5e'dcdc'7878ull; //light green + case 4: return 0x5454'5555'ededull; //dark blue + case 5: return 0x7d7d'7676'fcfcull; //light blue + case 6: return 0xd4d4'5252'4d4dull; //dark red + case 7: return 0x4242'ebeb'f5f5ull; //cyan + case 8: return 0xfcfc'5555'5454ull; //medium red + case 9: return 0xffff'7979'7878ull; //light red + case 10: return 0xd4d4'c1c1'5454ull; //dark yellow + case 11: return 0xe6e6'cece'8080ull; //light yellow + case 12: return 0x2121'b0b0'3b3bull; //dark green + case 13: return 0xc9c9'5b5b'babaull; //magenta + case 14: return 0xcccc'cccc'ccccull; //gray + case 15: return 0xffff'ffff'ffffull; //white + } + unreachable; +} + +auto SC3000Interface::ports() -> vector { return { + {ID::Port::Controller1, "Controller Port 1"}, + {ID::Port::Controller2, "Controller Port 2"}, + {ID::Port::Hardware, "Hardware" }}; +} + +auto SC3000Interface::devices(uint port) -> vector { + if(port == ID::Port::Controller1) return { + {ID::Device::None, "None" }, + {ID::Device::Gamepad, "Gamepad"} + }; + + if(port == ID::Port::Controller2) return { + {ID::Device::None, "None" }, + {ID::Device::Gamepad, "Gamepad"} + }; + + if(port == ID::Port::Hardware) return { + {ID::Device::SC3000Controls, "Controls"} + }; + + return {}; +} + +auto SC3000Interface::inputs(uint device) -> vector { + using Type = Input::Type; + + if(device == ID::Device::None) return { + }; + + if(device == ID::Device::Gamepad) return { + {Type::Hat, "Up" }, + {Type::Hat, "Down" }, + {Type::Hat, "Left" }, + {Type::Hat, "Right"}, + {Type::Button, "1" }, + {Type::Button, "2" } + }; + + if(device == ID::Device::SC3000Controls) return { + {Type::Control, "Pause"} + }; + + return {}; +} + +auto SC3000Interface::load() -> bool { + return system.load(this, System::Model::SC3000); +} + +auto SC3000Interface::connected(uint port) -> uint { + if(port == ID::Port::Controller1) return settings.controllerPort1; + if(port == ID::Port::Controller2) return settings.controllerPort2; + if(port == ID::Port::Hardware) return ID::Device::SC3000Controls; + return 0; +} + +auto SC3000Interface::connect(uint port, uint device) -> void { + if(port == ID::Port::Controller1) controllerPort1.connect(settings.controllerPort1 = device); + if(port == ID::Port::Controller2) controllerPort2.connect(settings.controllerPort2 = device); +} diff --git a/higan/ms/interface/sg-1000.cpp b/higan/ms/interface/sg-1000.cpp new file mode 100644 index 00000000..97c338cf --- /dev/null +++ b/higan/ms/interface/sg-1000.cpp @@ -0,0 +1,105 @@ +auto SG1000Interface::information() -> Information { + Information information; + information.manufacturer = "Sega"; + information.name = "SG-1000"; + information.extension = "sg1000"; + return information; +} + +auto SG1000Interface::displays() -> vector { + Display display; + display.type = Display::Type::CRT; + display.colors = 1 << 4; + display.width = 256; + display.height = 192; + display.internalWidth = 256; + display.internalHeight = 192; + display.aspectCorrection = 1.0; + if(Region::NTSC()) display.refreshRate = (system.colorburst() * 15.0 / 5.0) / (262.0 * 684.0); + if(Region::PAL()) display.refreshRate = (system.colorburst() * 15.0 / 5.0) / (312.0 * 684.0); + return {display}; +} + +auto SG1000Interface::color(uint32 color) -> uint64 { + switch(color.bits(0,3)) { + case 0: return 0x0000'0000'0000ull; //transparent + case 1: return 0x0000'0000'0000ull; //black + case 2: return 0x2121'c8c8'4242ull; //medium green + case 3: return 0x5e5e'dcdc'7878ull; //light green + case 4: return 0x5454'5555'ededull; //dark blue + case 5: return 0x7d7d'7676'fcfcull; //light blue + case 6: return 0xd4d4'5252'4d4dull; //dark red + case 7: return 0x4242'ebeb'f5f5ull; //cyan + case 8: return 0xfcfc'5555'5454ull; //medium red + case 9: return 0xffff'7979'7878ull; //light red + case 10: return 0xd4d4'c1c1'5454ull; //dark yellow + case 11: return 0xe6e6'cece'8080ull; //light yellow + case 12: return 0x2121'b0b0'3b3bull; //dark green + case 13: return 0xc9c9'5b5b'babaull; //magenta + case 14: return 0xcccc'cccc'ccccull; //gray + case 15: return 0xffff'ffff'ffffull; //white + } + unreachable; +} + +auto SG1000Interface::ports() -> vector { return { + {ID::Port::Controller1, "Controller Port 1"}, + {ID::Port::Controller2, "Controller Port 2"}, + {ID::Port::Hardware, "Hardware" }}; +} + +auto SG1000Interface::devices(uint port) -> vector { + if(port == ID::Port::Controller1) return { + {ID::Device::None, "None" }, + {ID::Device::Gamepad, "Gamepad"} + }; + + if(port == ID::Port::Controller2) return { + {ID::Device::None, "None" }, + {ID::Device::Gamepad, "Gamepad"} + }; + + if(port == ID::Port::Hardware) return { + {ID::Device::SG1000Controls, "Controls"} + }; + + return {}; +} + +auto SG1000Interface::inputs(uint device) -> vector { + using Type = Input::Type; + + if(device == ID::Device::None) return { + }; + + if(device == ID::Device::Gamepad) return { + {Type::Hat, "Up" }, + {Type::Hat, "Down" }, + {Type::Hat, "Left" }, + {Type::Hat, "Right"}, + {Type::Button, "1" }, + {Type::Button, "2" } + }; + + if(device == ID::Device::SG1000Controls) return { + {Type::Control, "Pause"} + }; + + return {}; +} + +auto SG1000Interface::load() -> bool { + return system.load(this, System::Model::SG1000); +} + +auto SG1000Interface::connected(uint port) -> uint { + if(port == ID::Port::Controller1) return settings.controllerPort1; + if(port == ID::Port::Controller2) return settings.controllerPort2; + if(port == ID::Port::Hardware) return ID::Device::SG1000Controls; + return 0; +} + +auto SG1000Interface::connect(uint port, uint device) -> void { + if(port == ID::Port::Controller1) controllerPort1.connect(settings.controllerPort1 = device); + if(port == ID::Port::Controller2) controllerPort2.connect(settings.controllerPort2 = device); +} diff --git a/higan/ms/ms.hpp b/higan/ms/ms.hpp index c1c02dc8..2fdced1a 100644 --- a/higan/ms/ms.hpp +++ b/higan/ms/ms.hpp @@ -30,6 +30,8 @@ namespace MasterSystem { }; struct Model { + inline static auto SG1000() -> bool; + inline static auto SC3000() -> bool; inline static auto MasterSystem() -> bool; inline static auto GameGear() -> bool; }; diff --git a/higan/ms/psg/psg.cpp b/higan/ms/psg/psg.cpp index 770d76c4..e86a2ec6 100644 --- a/higan/ms/psg/psg.cpp +++ b/higan/ms/psg/psg.cpp @@ -44,7 +44,8 @@ auto PSG::power() -> void { //use stereo mode for both; output same sample to both channels for Master System create(PSG::Enter, system.colorburst() / 16.0); stream = Emulator::audio.createStream(2, frequency()); - stream->addFilter(Emulator::Filter::Order::First, Emulator::Filter::Type::HighPass, 20.0); + stream->addHighPassFilter(20.0, Emulator::Filter::Order::First); + stream->addDCRemovalFilter(); select = 0; for(auto n : range(15)) { diff --git a/higan/ms/system/system.cpp b/higan/ms/system/system.cpp index ad46185b..c4a1d46c 100644 --- a/higan/ms/system/system.cpp +++ b/higan/ms/system/system.cpp @@ -50,7 +50,7 @@ auto System::save() -> void { } auto System::unload() -> void { - if(MasterSystem::Model::MasterSystem()) { + if(!MasterSystem::Model::GameGear()) { cpu.peripherals.reset(); controllerPort1.unload(); controllerPort2.unload(); @@ -71,7 +71,7 @@ auto System::power() -> void { psg.power(); scheduler.primary(cpu); - if(MasterSystem::Model::MasterSystem()) { + if(!MasterSystem::Model::GameGear()) { controllerPort1.power(ID::Port::Controller1); controllerPort2.power(ID::Port::Controller2); diff --git a/higan/ms/system/system.hpp b/higan/ms/system/system.hpp index 222a7141..57b4d28f 100644 --- a/higan/ms/system/system.hpp +++ b/higan/ms/system/system.hpp @@ -1,5 +1,5 @@ struct System { - enum class Model : uint { MasterSystem, GameGear }; + enum class Model : uint { SG1000, SC3000, MasterSystem, GameGear }; enum class Region : uint { NTSC, PAL }; auto loaded() const -> bool { return information.loaded; } @@ -38,6 +38,8 @@ private: extern System system; +auto Model::SG1000() -> bool { return system.model() == System::Model::SG1000; } +auto Model::SC3000() -> bool { return system.model() == System::Model::SC3000; } auto Model::MasterSystem() -> bool { return system.model() == System::Model::MasterSystem; } auto Model::GameGear() -> bool { return system.model() == System::Model::GameGear; } diff --git a/higan/ms/vdp/background.cpp b/higan/ms/vdp/background.cpp index eb3132dd..f78897c9 100644 --- a/higan/ms/vdp/background.cpp +++ b/higan/ms/vdp/background.cpp @@ -1,58 +1,122 @@ -auto VDP::Background::scanline() -> void { - state.x = 0; - state.y = vdp.io.vcounter; +auto VDP::Background::run(uint8 hoffset, uint9 voffset) -> void { + output = {}; + switch(vdp.io.mode) { + case 0b0000: return graphics1(hoffset, voffset); + case 0b0001: return; + case 0b0010: return graphics2(hoffset, voffset); + case 0b0011: return; + case 0b0100: return; + case 0b0101: return; + case 0b0110: return; + case 0b0111: return; + case 0b1000: return graphics3(hoffset, voffset, 192); + case 0b1001: return; + case 0b1010: return graphics3(hoffset, voffset, 192); + case 0b1011: return graphics3(hoffset, voffset, 224); + case 0b1100: return graphics3(hoffset, voffset, 192); + case 0b1101: return; + case 0b1110: return graphics3(hoffset, voffset, 240); + case 0b1111: return graphics3(hoffset, voffset, 192); + } } -auto VDP::Background::run() -> void { - uint8 hoffset = state.x++; - uint9 voffset = state.y; +auto VDP::Background::graphics1(uint8 hoffset, uint9 voffset) -> void { + uint14 nameTableAddress; + nameTableAddress.bits( 0, 4) = hoffset.bits(3,7); + nameTableAddress.bits( 5, 9) = voffset.bits(3,7); + nameTableAddress.bits(10,13) = vdp.io.nameTableAddress; + uint8 pattern = vdp.vram[nameTableAddress]; - if(voffset >= vdp.vlines() - || hoffset < (vdp.io.hscroll & 7) - ) { - output.color = 0; - output.palette = 0; - output.priority = 0; - return; + uint14 patternAddress; + patternAddress.bits( 0, 2) = voffset.bits(0,2); + patternAddress.bits( 3,10) = pattern; + patternAddress.bits(11,13) = vdp.io.patternTableAddress; + + uint14 colorAddress; //d5 = 0 + colorAddress.bits(0, 4) = pattern.bits(3,7); + colorAddress.bits(6,13) = vdp.io.colorTableAddress; + + uint8 color = vdp.vram[colorAddress]; + uint3 index = hoffset ^ 7; + if(vdp.vram[patternAddress].bit(index)) { + output.color = color.bits(4,7); + } else { + output.color = color.bits(0,3); } +} - bool hscroll = !vdp.io.horizontalScrollLock || voffset >= 16; - bool vscroll = !vdp.io.verticalScrollLock || hoffset <= 191; +auto VDP::Background::graphics2(uint8 hoffset, uint9 voffset) -> void { + uint14 nameTableAddress; + nameTableAddress.bits( 0, 4) = hoffset.bits(3,7); + nameTableAddress.bits( 5, 9) = voffset.bits(3,7); + nameTableAddress.bits(10,13) = vdp.io.nameTableAddress; + uint8 pattern = vdp.vram[nameTableAddress]; - if(hscroll) hoffset -= vdp.io.hscroll; - if(vscroll) voffset += vdp.io.vscroll; + uint14 patternAddress; + patternAddress.bits(0, 2) = voffset.bits(0,2); + patternAddress.bits(3,10) = pattern; + if(voffset >= 64 && voffset <= 127) patternAddress.bit(11) = vdp.io.patternTableAddress.bit(0); + if(voffset >= 128 && voffset <= 191) patternAddress.bit(12) = vdp.io.patternTableAddress.bit(1); + patternAddress.bit(13) = vdp.io.patternTableAddress.bit(2); + + uint14 colorAddress; + colorAddress.bits(0, 2) = voffset.bits(0,2); + colorAddress.bits(3,10) = pattern; + if(voffset >= 64 && voffset <= 127) colorAddress.bit(11) = vdp.io.patternTableAddress.bit(0); + if(voffset >= 128 && voffset <= 191) colorAddress.bit(12) = vdp.io.patternTableAddress.bit(1); + colorAddress.bit(13) = vdp.io.colorTableAddress.bit(7); + + uint8 colorMask = vdp.io.colorTableAddress.bits(0,6) << 1 | 1; + uint8 color = vdp.vram[colorAddress] & colorMask; + uint3 index = hoffset ^ 7; + if(vdp.vram[patternAddress].bit(index)) { + output.color = color.bits(4,7); + } else { + output.color = color.bits(0,3); + } +} + +auto VDP::Background::graphics3(uint8 hoffset, uint9 voffset, uint vlines) -> void { + if(hoffset < vdp.io.hscroll.bits(0,2)) return; + + if(!vdp.io.horizontalScrollLock || voffset >= 16) hoffset -= vdp.io.hscroll; + if(!vdp.io.verticalScrollLock || hoffset <= 191) voffset += vdp.io.vscroll; uint14 nameTableAddress; - if(vdp.vlines() == 192) { + if(vlines == 192) { if(voffset >= 224) voffset -= 224; - nameTableAddress = vdp.io.nameTableAddress << 11; + nameTableAddress.bits( 1, 5) = hoffset.bits(3,7); + nameTableAddress.bits( 6,10) = voffset.bits(3,7); + nameTableAddress.bits(11,13) = vdp.io.nameTableAddress.bits(1,3); } else { - voffset &= 255; - nameTableAddress = (vdp.io.nameTableAddress & ~1) << 11 | 0x700; + voffset += 224; + nameTableAddress.bits( 1, 5) = hoffset.bits(3,7); + nameTableAddress.bits( 6,11) = voffset.bits(3,8); + nameTableAddress.bits(12,13) = vdp.io.nameTableAddress.bits(2,3); } - nameTableAddress += ((voffset >> 3) << 6) + ((hoffset >> 3) << 1); - uint16 tiledata; - tiledata = vdp.vram[nameTableAddress + 0] << 0; - tiledata |= vdp.vram[nameTableAddress + 1] << 8; + uint16 pattern; + pattern.byte(0) = vdp.vram[nameTableAddress | 0]; + pattern.byte(1) = vdp.vram[nameTableAddress | 1]; - uint14 patternAddress = tiledata.bits(0,8) << 5; - if(tiledata.bit(9)) hoffset ^= 7; - if(tiledata.bit(10)) voffset ^= 7; - output.palette = tiledata.bit(11); - output.priority = tiledata.bit(12); + if(pattern.bit( 9)) hoffset ^= 7; //hflip + if(pattern.bit(10)) voffset ^= 7; //vflip + output.palette = pattern.bit(11); + output.priority = pattern.bit(12); - auto index = 7 - (hoffset & 7); - patternAddress += (voffset & 7) << 2; - output.color.bit(0) = vdp.vram[patternAddress + 0].bit(index); - output.color.bit(1) = vdp.vram[patternAddress + 1].bit(index); - output.color.bit(2) = vdp.vram[patternAddress + 2].bit(index); - output.color.bit(3) = vdp.vram[patternAddress + 3].bit(index); + uint14 patternAddress; + patternAddress.bits(2, 4) = voffset.bits(0,2); + patternAddress.bits(5,13) = pattern.bits(0,8); + + uint3 index = hoffset ^ 7; + output.color.bit(0) = vdp.vram[patternAddress | 0].bit(index); + output.color.bit(1) = vdp.vram[patternAddress | 1].bit(index); + output.color.bit(2) = vdp.vram[patternAddress | 2].bit(index); + output.color.bit(3) = vdp.vram[patternAddress | 3].bit(index); if(output.color == 0) output.priority = 0; } auto VDP::Background::power() -> void { - state = {}; output = {}; } diff --git a/higan/ms/vdp/io.cpp b/higan/ms/vdp/io.cpp index d8a35eb1..36755065 100644 --- a/higan/ms/vdp/io.cpp +++ b/higan/ms/vdp/io.cpp @@ -1,16 +1,9 @@ auto VDP::vcounter() -> uint8 { - if(io.lines240) { - //NTSC 256x240 - return io.vcounter; - } else if(io.lines224) { - //NTSC 256x224 - return io.vcounter <= 234 ? io.vcounter : io.vcounter - 6; - } else { - //NTSC 256x192 - return io.vcounter <= 218 ? io.vcounter : io.vcounter - 6; + switch(io.mode) { + default: return io.vcounter <= 218 ? io.vcounter : io.vcounter - 6; //256x192 + case 0b1011: return io.vcounter <= 234 ? io.vcounter : io.vcounter - 6; //256x224 + case 0b1110: return io.vcounter; //256x240 } - - unreachable; } auto VDP::hcounter() -> uint8 { @@ -51,9 +44,8 @@ auto VDP::data(uint8 data) -> void { vram[io.address++] = data; } else { uint mask = 0; - if(Model::MasterSystem()) mask = 0x1f; - if(Model::GameGear()) mask = 0x3f; - cram[io.address++ & mask] = data; + if(Model::MasterSystem()) cram[io.address++ & 0x1f] = data; + if(Model::GameGear()) cram[io.address++ & 0x3f] = data; } } @@ -83,8 +75,8 @@ auto VDP::registerWrite(uint4 addr, uint8 data) -> void { //mode control 1 case 0x0: { io.externalSync = data.bit(0); - io.extendedHeight = data.bit(1); - io.mode4 = data.bit(2); + io.mode.bit(1) = data.bit(1); + io.mode.bit(3) = data.bit(2) & !Model::SG1000() & !Model::SC3000(); io.spriteShift = data.bit(3); io.lineInterrupts = data.bit(4); io.leftClip = data.bit(5); @@ -97,8 +89,8 @@ auto VDP::registerWrite(uint4 addr, uint8 data) -> void { case 0x1: { io.spriteDouble = data.bit(0); io.spriteTile = data.bit(1); - io.lines240 = data.bit(3); - io.lines224 = data.bit(4); + io.mode.bit(2) = data.bit(3); + io.mode.bit(0) = data.bit(4); io.frameInterrupts = data.bit(5); io.displayEnable = data.bit(6); return; @@ -106,8 +98,7 @@ auto VDP::registerWrite(uint4 addr, uint8 data) -> void { //name table base address case 0x2: { - io.nameTableMask = data.bit(0); - io.nameTableAddress = data.bits(1,3); + io.nameTableAddress = data.bits(0,3); return; } @@ -119,21 +110,19 @@ auto VDP::registerWrite(uint4 addr, uint8 data) -> void { //pattern table base address case 0x4: { - io.patternTableAddress = data.bits(0,7); + io.patternTableAddress = data.bits(0,2); return; } //sprite attribute table base address case 0x5: { - io.spriteAttributeTableMask = data.bit(0); - io.spriteAttributeTableAddress = data.bits(1,6); + io.spriteAttributeTableAddress = data.bits(0,6); return; } //sprite pattern table base address case 0x6: { - io.spritePatternTableMask = data.bits(0,1); - io.spritePatternTableAddress = data.bit(2); + io.spritePatternTableAddress = data.bits(0,2); return; } diff --git a/higan/ms/vdp/serialization.cpp b/higan/ms/vdp/serialization.cpp index 0caccf01..1b13c658 100644 --- a/higan/ms/vdp/serialization.cpp +++ b/higan/ms/vdp/serialization.cpp @@ -21,8 +21,6 @@ auto VDP::serialize(serializer& s) -> void { s.integer(io.address); s.integer(io.vramLatch); s.integer(io.externalSync); - s.integer(io.extendedHeight); - s.integer(io.mode4); s.integer(io.spriteShift); s.integer(io.lineInterrupts); s.integer(io.leftClip); @@ -30,17 +28,13 @@ auto VDP::serialize(serializer& s) -> void { s.integer(io.verticalScrollLock); s.integer(io.spriteDouble); s.integer(io.spriteTile); - s.integer(io.lines240); - s.integer(io.lines224); s.integer(io.frameInterrupts); s.integer(io.displayEnable); - s.integer(io.nameTableMask); + s.integer(io.mode); s.integer(io.nameTableAddress); s.integer(io.colorTableAddress); s.integer(io.patternTableAddress); - s.integer(io.spriteAttributeTableMask); s.integer(io.spriteAttributeTableAddress); - s.integer(io.spritePatternTableMask); s.integer(io.spritePatternTableAddress); s.integer(io.backdropColor); s.integer(io.hscroll); @@ -49,16 +43,18 @@ auto VDP::serialize(serializer& s) -> void { } auto VDP::Background::serialize(serializer& s) -> void { - s.integer(state.x); - s.integer(state.y); s.integer(output.color); s.integer(output.palette); s.integer(output.priority); } auto VDP::Sprite::serialize(serializer& s) -> void { - s.integer(state.x); - s.integer(state.y); s.integer(output.color); - //todo: array is not serializable + for(auto& object : objects) { + s.integer(object.x); + s.integer(object.y); + s.integer(object.pattern); + s.integer(object.color); + } + s.integer(objectsValid); } diff --git a/higan/ms/vdp/sprite.cpp b/higan/ms/vdp/sprite.cpp index 7fa574bf..494f534c 100644 --- a/higan/ms/vdp/sprite.cpp +++ b/higan/ms/vdp/sprite.cpp @@ -1,68 +1,133 @@ -auto VDP::Sprite::scanline() -> void { - state.x = 0; - state.y = vdp.io.vcounter; - objects.reset(); - +auto VDP::Sprite::setup(uint9 voffset) -> void { + objectsValid = 0; uint limit = vdp.io.spriteTile ? 15 : 7; - uint14 attributeAddress = vdp.io.spriteAttributeTableAddress << 8; - for(uint index : range(64)) { - uint8 y = vdp.vram[attributeAddress + index]; - uint8 x = vdp.vram[attributeAddress + 0x80 + (index << 1)]; - uint8 pattern = vdp.vram[attributeAddress + 0x81 + (index << 1)]; - if(vdp.vlines() == 192 && y == 0xd0) break; - if(vdp.io.spriteShift) x -= 8; - y += 1; - if(state.y < y) continue; - if(state.y > y + limit) continue; + if(!vdp.io.mode.bit(3)) { + uint14 attributeAddress; + attributeAddress.bits(7,13) = vdp.io.spriteAttributeTableAddress; + for(uint index : range(32)) { + uint8 y = vdp.vram[attributeAddress++]; + if(y == 0xd0) break; - if(limit == 15) pattern.bit(0) = 0; + uint8 x = vdp.vram[attributeAddress++]; + uint8 pattern = vdp.vram[attributeAddress++]; + uint8 extra = vdp.vram[attributeAddress++]; - objects.append({x, y, pattern}); - if(objects.size() == 8) { - vdp.io.spriteOverflow = 1; - break; + if(extra.bit(7)) x -= 32; + y += 1; + if(voffset < y) continue; + if(voffset > y + limit) continue; + + if(limit == 15) pattern.bits(0,1) = 0; + + objects[objectsValid] = {x, y, pattern, extra.bits(0,3)}; + if(++objectsValid == 4) { + vdp.io.spriteOverflow = 1; + break; + } + } + } else { + uint14 attributeAddress; + attributeAddress.bits(8,13) = vdp.io.spriteAttributeTableAddress.bits(1,6); + for(uint index : range(64)) { + uint8 y = vdp.vram[attributeAddress + index]; + uint8 x = vdp.vram[attributeAddress + 0x80 + (index << 1)]; + uint8 pattern = vdp.vram[attributeAddress + 0x81 + (index << 1)]; + if(vdp.vlines() == 192 && y == 0xd0) break; + + if(vdp.io.spriteShift) x -= 8; + y += 1; + if(voffset < y) continue; + if(voffset > y + limit) continue; + + if(limit == 15) pattern.bit(0) = 0; + + objects[objectsValid] = {x, y, pattern}; + if(++objectsValid == 8) { + vdp.io.spriteOverflow = 1; + break; + } } } } -auto VDP::Sprite::run() -> void { - output.color = 0; +auto VDP::Sprite::run(uint8 hoffset, uint9 voffset) -> void { + output = {}; + switch(vdp.io.mode) { + case 0b0000: return graphics1(hoffset, voffset); + case 0b0001: return; + case 0b0010: return graphics2(hoffset, voffset); + case 0b0011: return; + case 0b0100: return; + case 0b0101: return; + case 0b0110: return; + case 0b0111: return; + case 0b1000: return graphics3(hoffset, voffset, 192); + case 0b1001: return; + case 0b1010: return graphics3(hoffset, voffset, 192); + case 0b1011: return graphics3(hoffset, voffset, 224); + case 0b1100: return graphics3(hoffset, voffset, 192); + case 0b1101: return; + case 0b1110: return graphics3(hoffset, voffset, 240); + case 0b1111: return graphics3(hoffset, voffset, 192); + } +} - if(state.y >= vdp.vlines()) return; +auto VDP::Sprite::graphics1(uint8 hoffset, uint9 voffset) -> void { + //todo: are sprites different in graphics mode 1? + return graphics2(hoffset, voffset); +} +auto VDP::Sprite::graphics2(uint8 hoffset, uint9 voffset) -> void { uint limit = vdp.io.spriteTile ? 15 : 7; - for(auto& o : objects) { - if(state.x < o.x) continue; - if(state.x > o.x + 7) continue; + for(uint objectIndex : range(objectsValid)) { + auto& o = objects[objectIndex]; + if(hoffset < o.x) continue; + if(hoffset > o.x + limit) continue; - uint x = state.x - o.x; - uint y = state.y - o.y; + uint x = hoffset - o.x; + uint y = voffset - o.y; - uint14 address = vdp.io.spritePatternTableAddress << 13; - address += o.pattern << 5; - address += (y & limit) << 2; + uint14 address; + address.bits( 0,10) = (o.pattern << 3) + (x >> 3 << 4) + (y & limit); + address.bits(11,13) = vdp.io.spritePatternTableAddress; - auto index = 7 - (x & 7); + uint3 index = x ^ 7; + if(vdp.vram[address].bit(index)) { + if(output.color) { vdp.io.spriteCollision = true; break; } + output.color = o.color; + } + } +} + +auto VDP::Sprite::graphics3(uint8 hoffset, uint9 voffset, uint vlines) -> void { + uint limit = vdp.io.spriteTile ? 15 : 7; + for(uint objectIndex : range(objectsValid)) { + auto& o = objects[objectIndex]; + if(hoffset < o.x) continue; + if(hoffset > o.x + 7) continue; + + uint x = hoffset - o.x; + uint y = voffset - o.y; + + uint14 address; + address.bits(2,12) = (o.pattern << 3) + (y & limit); + address.bit (13) = vdp.io.spritePatternTableAddress.bit(2); + + uint3 index = x ^ 7; uint4 color; - color.bit(0) = vdp.vram[address + 0].bit(index); - color.bit(1) = vdp.vram[address + 1].bit(index); - color.bit(2) = vdp.vram[address + 2].bit(index); - color.bit(3) = vdp.vram[address + 3].bit(index); + color.bit(0) = vdp.vram[address | 0].bit(index); + color.bit(1) = vdp.vram[address | 1].bit(index); + color.bit(2) = vdp.vram[address | 2].bit(index); + color.bit(3) = vdp.vram[address | 3].bit(index); if(color == 0) continue; - if(output.color) { - vdp.io.spriteCollision = true; - break; - } - + if(output.color) { vdp.io.spriteCollision = true; break; } output.color = color; } - - state.x++; } auto VDP::Sprite::power() -> void { - state = {}; output = {}; + objectsValid = 0; } diff --git a/higan/ms/vdp/vdp.cpp b/higan/ms/vdp/vdp.cpp index df948dbf..a7077d6a 100644 --- a/higan/ms/vdp/vdp.cpp +++ b/higan/ms/vdp/vdp.cpp @@ -26,16 +26,14 @@ auto VDP::main() -> void { io.intFrame = 1; } - background.scanline(); - sprite.scanline(); - //684 clocks/scanline uint y = io.vcounter; + sprite.setup(y); if(y < vlines()) { uint32* screen = buffer + (24 + y) * 256; for(uint x : range(256)) { - background.run(); - sprite.run(); + background.run(x, y); + sprite.run(x, y); step(2); uint12 color = palette(16 | io.backdropColor); @@ -74,6 +72,12 @@ auto VDP::step(uint clocks) -> void { } auto VDP::refresh() -> void { + if(Model::SG1000() || Model::SC3000()) { + uint32* screen = buffer; + screen += 24 * 256; + Emulator::video.refresh(screen, 256 * sizeof(uint32), 256, 192); + } + if(Model::MasterSystem()) { //center the video output vertically in the viewport uint32* screen = buffer; @@ -89,9 +93,11 @@ auto VDP::refresh() -> void { } auto VDP::vlines() -> uint { - if(io.lines240) return 240; - if(io.lines224) return 224; - return 192; + switch(io.mode) { + default: return 192; + case 0b1011: return 224; + case 0b1110: return 240; + } } auto VDP::vblank() -> bool { @@ -109,14 +115,15 @@ auto VDP::power() -> void { } auto VDP::palette(uint5 index) -> uint12 { - if(Model::MasterSystem()) { - return cram[index]; - } - - if(Model::GameGear()) { - return cram[index * 2 + 0] << 0 | cram[index * 2 + 1] << 8; - } - + if(Model::SG1000() || Model::SC3000()) return index.bits(0,3); + //Master System and Game Gear approximate TMS9918A colors by converting to RGB6 palette colors + static uint6 palette[16] = { + 0x00, 0x00, 0x08, 0x0c, 0x10, 0x30, 0x01, 0x3c, + 0x02, 0x03, 0x05, 0x0f, 0x04, 0x33, 0x15, 0x3f, + }; + if(!io.mode.bit(3)) return palette[index.bits(0,3)]; + if(Model::MasterSystem()) return cram[index]; + if(Model::GameGear()) return cram[index * 2 + 0] << 0 | cram[index * 2 + 1] << 8; return 0; } diff --git a/higan/ms/vdp/vdp.hpp b/higan/ms/vdp/vdp.hpp index 90928407..e0475ce9 100644 --- a/higan/ms/vdp/vdp.hpp +++ b/higan/ms/vdp/vdp.hpp @@ -1,4 +1,4 @@ -//TI TMS9918A (derivative) +//Texas Instruments TMS9918A (derivative) struct VDP : Thread { static auto Enter() -> void; @@ -23,19 +23,16 @@ struct VDP : Thread { //background.cpp struct Background { - auto scanline() -> void; - auto run() -> void; + auto run(uint8 hoffset, uint9 voffset) -> void; + auto graphics1(uint8 hoffset, uint9 voffset) -> void; + auto graphics2(uint8 hoffset, uint9 voffset) -> void; + auto graphics3(uint8 hoffset, uint9 voffset, uint vlines) -> void; auto power() -> void; //serialization.cpp auto serialize(serializer&) -> void; - struct State { - uint x = 0; - uint y = 0; - } state; - struct Output { uint4 color; uint1 palette; @@ -45,8 +42,11 @@ struct VDP : Thread { //sprite.cpp struct Sprite { - auto scanline() -> void; - auto run() -> void; + auto setup(uint9 voffset) -> void; + auto run(uint8 hoffset, uint9 voffset) -> void; + auto graphics1(uint8 hoffset, uint9 voffset) -> void; + auto graphics2(uint8 hoffset, uint9 voffset) -> void; + auto graphics3(uint8 hoffset, uint9 voffset, uint vlines) -> void; auto power() -> void; @@ -57,18 +57,15 @@ struct VDP : Thread { uint8 x; uint8 y; uint8 pattern; + uint4 color; }; - struct State { - uint x = 0; - uint y = 0; - } state; - struct Output { uint4 color; } output; - adaptive_array objects; + array objects; + uint objectsValid; } sprite; //serialization.cpp @@ -79,7 +76,7 @@ private: uint32 buffer[256 * 264]; uint8 vram[0x4000]; - uint8 cram[0x40]; //MS = 0x20, GG = 0x40 + uint8 cram[0x40]; //SG + MS = 0x20, GG = 0x40 struct IO { uint vcounter = 0; //vertical counter @@ -105,8 +102,6 @@ private: //$00 mode control 1 bool externalSync = 0; - bool extendedHeight = 0; - bool mode4 = 0; bool spriteShift = 0; bool lineInterrupts = 0; bool leftClip = 0; @@ -116,28 +111,26 @@ private: //$01 mode control 2 bool spriteDouble = 0; bool spriteTile = 0; - bool lines240 = 0; - bool lines224 = 0; bool frameInterrupts = 0; bool displayEnable = 0; + //$00 + $01 + uint4 mode; + //$02 name table base address - uint1 nameTableMask; - uint3 nameTableAddress; + uint4 nameTableAddress; //$03 color table base address uint8 colorTableAddress; //$04 pattern table base address - uint8 patternTableAddress; + uint3 patternTableAddress; //$05 sprite attribute table base address - uint1 spriteAttributeTableMask; - uint6 spriteAttributeTableAddress; + uint7 spriteAttributeTableAddress; //$06 sprite pattern table base address - uint2 spritePatternTableMask; - uint1 spritePatternTableAddress; + uint3 spritePatternTableAddress; //$07 backdrop color uint4 backdropColor; diff --git a/higan/pce/psg/psg.cpp b/higan/pce/psg/psg.cpp index 934aab24..c0f18129 100644 --- a/higan/pce/psg/psg.cpp +++ b/higan/pce/psg/psg.cpp @@ -52,7 +52,8 @@ auto PSG::step(uint clocks) -> void { auto PSG::power() -> void { create(PSG::Enter, system.colorburst()); stream = Emulator::audio.createStream(2, frequency()); - stream->addFilter(Emulator::Filter::Order::First, Emulator::Filter::Type::HighPass, 20.0); + stream->addHighPassFilter(20.0, Emulator::Filter::Order::First); + stream->addDCRemovalFilter(); io = {}; for(auto C : range(6)) channel[C].power(C); diff --git a/higan/sfc/coprocessor/icd/icd.cpp b/higan/sfc/coprocessor/icd/icd.cpp index e1a0bae9..5ac180a0 100644 --- a/higan/sfc/coprocessor/icd/icd.cpp +++ b/higan/sfc/coprocessor/icd/icd.cpp @@ -46,7 +46,8 @@ auto ICD::power() -> void { //SGB1 uses CPU oscillator; SGB2 uses dedicated oscillator create(ICD::Enter, (Frequency ? Frequency : system.cpuFrequency()) / 5.0); stream = Emulator::audio.createStream(2, frequency() / 2.0); - stream->addFilter(Emulator::Filter::Order::First, Emulator::Filter::Type::HighPass, 20.0); + stream->addHighPassFilter(20.0, Emulator::Filter::Order::First); + stream->addDCRemovalFilter(); r6003 = 0x00; r6004 = 0xff; diff --git a/higan/systems/SC-3000.sys/manifest.bml b/higan/systems/SC-3000.sys/manifest.bml new file mode 100644 index 00000000..fa6204a1 --- /dev/null +++ b/higan/systems/SC-3000.sys/manifest.bml @@ -0,0 +1 @@ +system name:SC-3000 diff --git a/higan/systems/SG-1000.sys/manifest.bml b/higan/systems/SG-1000.sys/manifest.bml new file mode 100644 index 00000000..4b1d86f1 --- /dev/null +++ b/higan/systems/SG-1000.sys/manifest.bml @@ -0,0 +1 @@ +system name:SG-1000 diff --git a/higan/target-bsnes/GNUmakefile b/higan/target-bsnes/GNUmakefile index a00a0d29..ea2aec4e 100644 --- a/higan/target-bsnes/GNUmakefile +++ b/higan/target-bsnes/GNUmakefile @@ -33,7 +33,7 @@ endif verbose: hiro.verbose ruby.verbose nall.verbose all; -install: +install: all ifeq ($(shell id -un),root) $(error "make install should not be run as root") else ifeq ($(platform),windows) diff --git a/higan/target-higan/GNUmakefile b/higan/target-higan/GNUmakefile index 851cdd62..71e5d9d4 100644 --- a/higan/target-higan/GNUmakefile +++ b/higan/target-higan/GNUmakefile @@ -33,7 +33,7 @@ endif verbose: hiro.verbose ruby.verbose nall.verbose all; -install: +install: all ifeq ($(shell id -un),root) $(error "make install should not be run as root") else ifeq ($(platform),windows) diff --git a/higan/target-higan/program/platform.cpp b/higan/target-higan/program/platform.cpp index 20207a34..dd58a24a 100644 --- a/higan/target-higan/program/platform.cpp +++ b/higan/target-higan/program/platform.cpp @@ -93,7 +93,12 @@ auto Program::videoRefresh(uint displayID, const uint32* data, uint pitch, uint } auto Program::audioSample(const double* samples, uint channels) -> void { - audio->output(samples); + if(channels == 1) { + double stereo[] = {samples[0], samples[0]}; + audio->output(stereo); + } else { + audio->output(samples); + } } auto Program::inputPoll(uint port, uint device, uint input) -> int16 { diff --git a/higan/target-higan/program/program.cpp b/higan/target-higan/program/program.cpp index e0848a4e..cb5caed0 100644 --- a/higan/target-higan/program/program.cpp +++ b/higan/target-higan/program/program.cpp @@ -24,6 +24,12 @@ Program::Program(Arguments arguments) { #ifdef CORE_SFC emulators.append(new SuperFamicom::Interface); #endif +#ifdef CORE_MS + emulators.append(new MasterSystem::SG1000Interface); +#endif +#ifdef CORE_MS + emulators.append(new MasterSystem::SC3000Interface); +#endif #ifdef CORE_MS emulators.append(new MasterSystem::MasterSystemInterface); #endif diff --git a/higan/ws/apu/apu.cpp b/higan/ws/apu/apu.cpp index f7285061..245b4c2e 100644 --- a/higan/ws/apu/apu.cpp +++ b/higan/ws/apu/apu.cpp @@ -68,7 +68,8 @@ auto APU::step(uint clocks) -> void { auto APU::power() -> void { create(APU::Enter, 3'072'000); stream = Emulator::audio.createStream(2, frequency()); - stream->addFilter(Emulator::Filter::Order::First, Emulator::Filter::Type::HighPass, 20.0); + stream->addHighPassFilter(20.0, Emulator::Filter::Order::First); + stream->addDCRemovalFilter(); bus.map(this, 0x004a, 0x004c); bus.map(this, 0x004e, 0x0050); diff --git a/icarus/GNUmakefile b/icarus/GNUmakefile index ea5ee7cc..1b134009 100644 --- a/icarus/GNUmakefile +++ b/icarus/GNUmakefile @@ -34,7 +34,7 @@ endif $(call delete,obj/*) $(call delete,out/*) -install: +install: all ifeq ($(platform),macos) cp -R out/$(name).app /Applications/$(name).app else ifneq ($(filter $(platform),linux bsd),) diff --git a/icarus/core/core.cpp b/icarus/core/core.cpp index 0dae2b59..895e673e 100644 --- a/icarus/core/core.cpp +++ b/icarus/core/core.cpp @@ -1,10 +1,13 @@ Icarus::Icarus() { Database::Famicom = BML::unserialize(string::read(locate("Database/Famicom.bml"))); Database::SuperFamicom = BML::unserialize(string::read(locate("Database/Super Famicom.bml"))); + Database::SG1000 = BML::unserialize(string::read(locate("Database/SG-1000.bml"))); + Database::SC3000 = BML::unserialize(string::read(locate("Database/SC-3000.bml"))); Database::MasterSystem = BML::unserialize(string::read(locate("Database/Master System.bml"))); Database::MegaDrive = BML::unserialize(string::read(locate("Database/Mega Drive.bml"))); Database::PCEngine = BML::unserialize(string::read(locate("Database/PC Engine.bml"))); Database::SuperGrafx = BML::unserialize(string::read(locate("Database/SuperGrafx.bml"))); + Database::MSX = BML::unserialize(string::read(locate("Database/MSX.bml"))); Database::GameBoy = BML::unserialize(string::read(locate("Database/Game Boy.bml"))); Database::GameBoyColor = BML::unserialize(string::read(locate("Database/Game Boy Color.bml"))); Database::GameBoyAdvance = BML::unserialize(string::read(locate("Database/Game Boy Advance.bml"))); @@ -41,10 +44,13 @@ auto Icarus::manifest(string location) -> string { auto type = Location::suffix(location).downcase(); if(type == ".fc") return famicomManifest(location); if(type == ".sfc") return superFamicomManifest(location); + if(type == ".sg1000") return sg1000Manifest(location); + if(type == ".sc3000") return sc3000Manifest(location); if(type == ".ms") return masterSystemManifest(location); if(type == ".md") return megaDriveManifest(location); if(type == ".pce") return pcEngineManifest(location); - if(type == ".sg") return superGrafxManifest(location); + if(type == ".sgx") return superGrafxManifest(location); + if(type == ".msx") return msxManifest(location); if(type == ".gb") return gameBoyManifest(location); if(type == ".gbc") return gameBoyColorManifest(location); if(type == ".gba") return gameBoyAdvanceManifest(location); @@ -85,10 +91,13 @@ auto Icarus::import(string location) -> string { if(type == ".fc" || type == ".nes") return famicomImport(buffer, location); if(type == ".sfc" || type == ".smc") return superFamicomImport(buffer, location); + if(type == ".sg1000" || type == ".sg") return sg1000Import(buffer, location); + if(type == ".sc3000" || type == ".sc") return sc3000Import(buffer, location); if(type == ".ms" || type == ".sms") return masterSystemImport(buffer, location); if(type == ".md" || type == ".smd" || type == ".gen") return megaDriveImport(buffer, location); if(type == ".pce") return pcEngineImport(buffer, location); - if(type == ".sg" || type == ".sgx") return superGrafxImport(buffer, location); + if(type == ".sgx") return superGrafxImport(buffer, location); + if(type == ".msx") return msxImport(buffer, location); if(type == ".gb") return gameBoyImport(buffer, location); if(type == ".gbc") return gameBoyColorImport(buffer, location); if(type == ".gba") return gameBoyAdvanceImport(buffer, location); diff --git a/icarus/core/core.hpp b/icarus/core/core.hpp index f9dd53c5..e2ca8f19 100644 --- a/icarus/core/core.hpp +++ b/icarus/core/core.hpp @@ -46,6 +46,16 @@ struct Icarus { auto superFamicomManifest(vector& buffer, string location) -> string; auto superFamicomImport(vector& buffer, string location) -> string; + //sg-1000.cpp + auto sg1000Manifest(string location) -> string; + auto sg1000Manifest(vector& buffer, string location) -> string; + auto sg1000Import(vector& buffer, string location) -> string; + + //sc-3000.cpp + auto sc3000Manifest(string location) -> string; + auto sc3000Manifest(vector& buffer, string location) -> string; + auto sc3000Import(vector& buffer, string location) -> string; + //master-system.cpp auto masterSystemManifest(string location) -> string; auto masterSystemManifest(vector& buffer, string location) -> string; @@ -66,6 +76,11 @@ struct Icarus { auto superGrafxManifest(vector& buffer, string location) -> string; auto superGrafxImport(vector& buffer, string location) -> string; + //msx.cpp + auto msxManifest(string location) -> string; + auto msxManifest(vector& buffer, string location) -> string; + auto msxImport(vector& buffer, string location) -> string; + //game-boy.cpp auto gameBoyManifest(string location) -> string; auto gameBoyManifest(vector& buffer, string location) -> string; @@ -119,10 +134,13 @@ private: namespace Database { Markup::Node Famicom; Markup::Node SuperFamicom; + Markup::Node SG1000; + Markup::Node SC3000; Markup::Node MasterSystem; Markup::Node MegaDrive; Markup::Node PCEngine; Markup::Node SuperGrafx; + Markup::Node MSX; Markup::Node GameBoy; Markup::Node GameBoyColor; Markup::Node GameBoyAdvance; diff --git a/icarus/core/msx.cpp b/icarus/core/msx.cpp new file mode 100644 index 00000000..b9d00a55 --- /dev/null +++ b/icarus/core/msx.cpp @@ -0,0 +1,39 @@ +auto Icarus::msxManifest(string location) -> string { + vector buffer; + concatenate(buffer, {location, "program.rom"}); + return msxManifest(buffer, location); +} + +auto Icarus::msxManifest(vector& buffer, string location) -> string { + if(settings["icarus/UseDatabase"].boolean()) { + auto digest = Hash::SHA256(buffer).digest(); + for(auto game : Database::MSX.find("game")) { + if(game["sha256"].text() == digest) return BML::serialize(game); + } + } + + if(settings["icarus/UseHeuristics"].boolean()) { + Heuristics::MSX game{buffer, location}; + if(auto manifest = game.manifest()) return manifest; + } + + return {}; +} + +auto Icarus::msxImport(vector& buffer, string location) -> string { + auto name = Location::prefix(location); + auto source = Location::path(location); + string target{settings["Library/Location"].text(), "MSX/", name, ".msx/"}; + + auto manifest = msxManifest(buffer, location); + if(!manifest) return failure("failed to parse ROM image"); + + if(!create(target)) return failure("library path unwritable"); + if(exists({source, name, ".sav"}) && !exists({target, "save.ram"})) { + copy({source, name, ".sav"}, {target, "save.ram"}); + } + + if(settings["icarus/CreateManifests"].boolean()) write({target, "manifest.bml"}, manifest); + write({target, "program.rom"}, buffer); + return success(target); +} diff --git a/icarus/core/sc-3000.cpp b/icarus/core/sc-3000.cpp new file mode 100644 index 00000000..eb9d8906 --- /dev/null +++ b/icarus/core/sc-3000.cpp @@ -0,0 +1,39 @@ +auto Icarus::sc3000Manifest(string location) -> string { + vector buffer; + concatenate(buffer, {location, "program.rom"}); + return sc3000Manifest(buffer, location); +} + +auto Icarus::sc3000Manifest(vector& buffer, string location) -> string { + if(settings["icarus/UseDatabase"].boolean()) { + auto digest = Hash::SHA256(buffer).digest(); + for(auto game : Database::SC3000.find("game")) { + if(game["sha256"].text() == digest) return BML::serialize(game); + } + } + + if(settings["icarus/UseHeuristics"].boolean()) { + Heuristics::SC3000 game{buffer, location}; + if(auto manifest = game.manifest()) return manifest; + } + + return {}; +} + +auto Icarus::sc3000Import(vector& buffer, string location) -> string { + auto name = Location::prefix(location); + auto source = Location::path(location); + string target{settings["Library/Location"].text(), "SC-3000/", name, ".sc3000/"}; + + auto manifest = sc3000Manifest(buffer, location); + if(!manifest) return failure("failed to parse ROM image"); + + if(!create(target)) return failure("library path unwritable"); + if(exists({source, name, ".sav"}) && !exists({target, "save.ram"})) { + copy({source, name, ".sav"}, {target, "save.ram"}); + } + + if(settings["icarus/CreateManifests"].boolean()) write({target, "manifest.bml"}, manifest); + write({target, "program.rom"}, buffer); + return success(target); +} diff --git a/icarus/core/sg-1000.cpp b/icarus/core/sg-1000.cpp new file mode 100644 index 00000000..73b58291 --- /dev/null +++ b/icarus/core/sg-1000.cpp @@ -0,0 +1,39 @@ +auto Icarus::sg1000Manifest(string location) -> string { + vector buffer; + concatenate(buffer, {location, "program.rom"}); + return sg1000Manifest(buffer, location); +} + +auto Icarus::sg1000Manifest(vector& buffer, string location) -> string { + if(settings["icarus/UseDatabase"].boolean()) { + auto digest = Hash::SHA256(buffer).digest(); + for(auto game : Database::SG1000.find("game")) { + if(game["sha256"].text() == digest) return BML::serialize(game); + } + } + + if(settings["icarus/UseHeuristics"].boolean()) { + Heuristics::SG1000 game{buffer, location}; + if(auto manifest = game.manifest()) return manifest; + } + + return {}; +} + +auto Icarus::sg1000Import(vector& buffer, string location) -> string { + auto name = Location::prefix(location); + auto source = Location::path(location); + string target{settings["Library/Location"].text(), "SG-1000/", name, ".sg1000/"}; + + auto manifest = sg1000Manifest(buffer, location); + if(!manifest) return failure("failed to parse ROM image"); + + if(!create(target)) return failure("library path unwritable"); + if(exists({source, name, ".sav"}) && !exists({target, "save.ram"})) { + copy({source, name, ".sav"}, {target, "save.ram"}); + } + + if(settings["icarus/CreateManifests"].boolean()) write({target, "manifest.bml"}, manifest); + write({target, "program.rom"}, buffer); + return success(target); +} diff --git a/icarus/heuristics/msx.cpp b/icarus/heuristics/msx.cpp new file mode 100644 index 00000000..6470cef3 --- /dev/null +++ b/icarus/heuristics/msx.cpp @@ -0,0 +1,31 @@ +namespace Heuristics { + +struct MSX { + MSX(vector& data, string location); + explicit operator bool() const; + auto manifest() const -> string; + +private: + vector& data; + string location; +}; + +MSX::MSX(vector& data, string location) : data(data), location(location) { +} + +MSX::operator bool() const { + return (bool)data; +} + +auto MSX::manifest() const -> string { + string output; + output.append("game\n"); + output.append(" sha256: ", Hash::SHA256(data).digest(), "\n"); + output.append(" label: ", Location::prefix(location), "\n"); + output.append(" name: ", Location::prefix(location), "\n"); + output.append(" board\n"); + output.append(Memory{}.type("ROM").size(data.size()).content("Program").text()); + return output; +} + +} diff --git a/icarus/heuristics/sc-3000.cpp b/icarus/heuristics/sc-3000.cpp new file mode 100644 index 00000000..6f24f26c --- /dev/null +++ b/icarus/heuristics/sc-3000.cpp @@ -0,0 +1,32 @@ +namespace Heuristics { + +struct SC3000 { + SC3000(vector& data, string location); + explicit operator bool() const; + auto manifest() const -> string; + +private: + vector& data; + string location; +}; + +SC3000::SC3000(vector& data, string location) : data(data), location(location) { +} + +SC3000::operator bool() const { + return (bool)data; +} + +auto SC3000::manifest() const -> string { + string output; + output.append("game\n"); + output.append(" sha256: ", Hash::SHA256(data).digest(), "\n"); + output.append(" label: ", Location::prefix(location), "\n"); + output.append(" name: ", Location::prefix(location), "\n"); + output.append(" board\n"); + output.append(Memory{}.type("ROM").size(data.size()).content("Program").text()); + output.append(Memory{}.type("RAM").size(0x8000).content("Save").text()); + return output; +} + +} diff --git a/icarus/heuristics/sg-1000.cpp b/icarus/heuristics/sg-1000.cpp new file mode 100644 index 00000000..1a5f24ed --- /dev/null +++ b/icarus/heuristics/sg-1000.cpp @@ -0,0 +1,32 @@ +namespace Heuristics { + +struct SG1000 { + SG1000(vector& data, string location); + explicit operator bool() const; + auto manifest() const -> string; + +private: + vector& data; + string location; +}; + +SG1000::SG1000(vector& data, string location) : data(data), location(location) { +} + +SG1000::operator bool() const { + return (bool)data; +} + +auto SG1000::manifest() const -> string { + string output; + output.append("game\n"); + output.append(" sha256: ", Hash::SHA256(data).digest(), "\n"); + output.append(" label: ", Location::prefix(location), "\n"); + output.append(" name: ", Location::prefix(location), "\n"); + output.append(" board\n"); + output.append(Memory{}.type("ROM").size(data.size()).content("Program").text()); + output.append(Memory{}.type("RAM").size(0x8000).content("Save").text()); + return output; +} + +} diff --git a/icarus/icarus.cpp b/icarus/icarus.cpp index efd02356..4614445b 100644 --- a/icarus/icarus.cpp +++ b/icarus/icarus.cpp @@ -19,10 +19,13 @@ Settings settings; #include "heuristics/heuristics.cpp" #include "heuristics/famicom.cpp" #include "heuristics/super-famicom.cpp" +#include "heuristics/sg-1000.cpp" +#include "heuristics/sc-3000.cpp" #include "heuristics/master-system.cpp" #include "heuristics/mega-drive.cpp" #include "heuristics/pc-engine.cpp" #include "heuristics/supergrafx.cpp" +#include "heuristics/msx.cpp" #include "heuristics/game-boy.cpp" #include "heuristics/game-boy-advance.cpp" #include "heuristics/game-gear.cpp" @@ -34,10 +37,13 @@ Settings settings; #include "core/core.cpp" #include "core/famicom.cpp" #include "core/super-famicom.cpp" +#include "core/sg-1000.cpp" +#include "core/sc-3000.cpp" #include "core/master-system.cpp" #include "core/mega-drive.cpp" #include "core/pc-engine.cpp" #include "core/supergrafx.cpp" +#include "core/msx.cpp" #include "core/game-boy.cpp" #include "core/game-boy-color.cpp" #include "core/game-boy-advance.cpp" @@ -85,10 +91,13 @@ auto nall::main(Arguments arguments) -> void { .setFilters("ROM Files|" "*.fc:*.nes:" "*.sfc:*.smc:" + "*.sg1000:*.sg:" + "*.sc3000:*.sc:" "*.ms:*.sms:" "*.md:*.smd:*.gen:" "*.pce:" - "*.sg:*.sgx:" + "*.sgx:" + "*.msx:" "*.gb:" "*.gbc:" "*.gba:" diff --git a/icarus/ui/scan-dialog.cpp b/icarus/ui/scan-dialog.cpp index 5c60212e..e57df071 100644 --- a/icarus/ui/scan-dialog.cpp +++ b/icarus/ui/scan-dialog.cpp @@ -102,10 +102,13 @@ auto ScanDialog::gamePakType(const string& type) -> bool { return type == ".sys" || type == ".fc" || type == ".sfc" + || type == ".sg1000" + || type == ".sc3000" || type == ".ms" || type == ".md" || type == ".pce" - || type == ".sg" + || type == ".sgx" + || type == ".msx" || type == ".gb" || type == ".gbc" || type == ".gba" @@ -121,10 +124,13 @@ auto ScanDialog::gameRomType(const string& type) -> bool { return type == ".zip" || type == ".fc" || type == ".nes" || type == ".sfc" || type == ".smc" + || type == ".sg1000" || type == ".sg" + || type == ".sc3000" || type == ".sc" || type == ".ms" || type == ".sms" || type == ".md" || type == ".smd" || type == ".gen" || type == ".pce" - || type == ".sg" || type == ".sgx" + || type == ".sgx" + || type == ".msx" || type == ".gb" || type == ".gbc" || type == ".gba" diff --git a/nall/dsp/iir/dc-removal.hpp b/nall/dsp/iir/dc-removal.hpp new file mode 100644 index 00000000..1fc17e65 --- /dev/null +++ b/nall/dsp/iir/dc-removal.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +//DC offset removal IIR filter + +namespace nall { namespace DSP { namespace IIR { + +struct DCRemoval { + inline auto reset() -> void; + inline auto process(double in) -> double; //normalized sample (-1.0 to +1.0) + +private: + double x; + double y; +}; + +auto DCRemoval::reset() -> void { + x = 0.0; + y = 0.0; +} + +auto DCRemoval::process(double in) -> double { + x = 0.999 * x + in - y; + y = in; + return x; +} + +}}}