Update to v102r09 release.

byuu says:

Changelog:

  - MD: restructured DMA to a subclass of VDP
  - MD: implemented VRAM copy mode (fixes Langrisser II ... mostly)
  - MS: implemened PSG support [Cydrak]
  - GG: implemented PSG stereo sound support
  - MS: use the new struct Model {} design that other cores use

The MS/GG PSG should be feature complete, but I don't have good tests
for Game Gear stereo mode, nor for the noise channel. There's also a
really weird behavior with when to reload the channel counters on volume
register writes. I can confirm what Cydrak observed in that following
the docs and reloading always creates serious audio distortion problems.
So, more research is needed there.

To get the correct sound out of the PSG, I have to run it at 3.58MHz /
16, which seems really weird to me. The docs make it sound like it's
supposed to run at the full 3.58MHz. If we can really run it at
223.7KHz, then that's help reduce the overhead of PSG emulation, which
will definitely come in handy for Mega Drive, and possibly later Mega
CD, emulation.

I have not implemented the PSG into the Mega Drive just yet. Nor have I
implemented save states or cheat code support into the MS/GG cores yet.
The latter is next on my list.
This commit is contained in:
Tim Allen 2017-02-21 22:07:33 +11:00
parent d76c0c7e82
commit 8071da4c6a
20 changed files with 319 additions and 77 deletions

View File

@ -12,7 +12,7 @@ using namespace nall;
namespace Emulator {
static const string Name = "higan";
static const string Version = "102.08";
static const string Version = "102.09";
static const string Author = "byuu";
static const string License = "GPLv3";
static const string Website = "http://byuu.org/";

View File

@ -1,36 +1,45 @@
auto VDP::dmaRun() -> void {
if(!io.dmaEnable) return;
if(!io.command.bit(5)) return;
auto VDP::DMA::run() -> void {
if(!io.enable || io.wait) return;
if(!vdp.io.command.bit(5)) return;
if(io.dmaMode <= 1) return dmaLoad();
if(io.dmaMode == 2) return dmaFill();
if(io.dmaMode == 3) return dmaCopy();
if(io.mode <= 1) return load();
if(io.mode == 2) return fill();
if(io.mode == 3) return copy();
}
auto VDP::dmaLoad() -> void {
auto VDP::DMA::load() -> void {
cpu.wait |= Wait::VDP_DMA;
auto data = busCPU.readWord(io.dmaMode.bit(0) << 23 | io.dmaSource << 1);
writeDataPort(data);
auto data = busCPU.readWord(io.mode.bit(0) << 23 | io.source << 1);
vdp.writeDataPort(data);
io.dmaSource.bits(0,15)++;
if(--io.dmaLength == 0) {
io.command.bit(5) = 0;
io.source.bits(0,15)++;
if(--io.length == 0) {
vdp.io.command.bit(5) = 0;
cpu.wait &=~ Wait::VDP_DMA;
}
}
auto VDP::dmaFill() -> void {
if(io.dmaFillWait) return;
auto VDP::DMA::fill() -> void {
auto data = io.fill;
vdp.writeDataPort(data << 8 | data << 0);
auto data = io.dmaFillByte;
writeDataPort(data << 8 | data << 0);
io.dmaSource.bits(0,15)++;
if(--io.dmaLength == 0) {
io.command.bit(5) = 0;
io.source.bits(0,15)++;
if(--io.length == 0) {
vdp.io.command.bit(5) = 0;
}
}
auto VDP::dmaCopy() -> void {
auto VDP::DMA::copy() -> void {
auto data = vdp.vram[io.source.bits(0,14)];
vdp.writeDataPort(data);
io.source.bits(0,15)++;
if(--io.length == 0) {
vdp.io.command.bit(5) = 0;
}
}
auto VDP::DMA::power() -> void {
memory::fill(&io, sizeof(IO));
}

View File

@ -74,8 +74,9 @@ auto VDP::writeDataPort(uint16 data) -> void {
io.commandPending = false;
//DMA VRAM fill
if(io.dmaFillWait.lower()) {
io.dmaFillByte = data >> 8;
if(dma.io.wait.lower()) {
dma.io.fill = data >> 8;
return;
}
//VRAM write
@ -132,7 +133,7 @@ auto VDP::writeControlPort(uint16 data) -> void {
io.command.bits(2,5) = data.bits(4,7);
io.address.bits(14,15) = data.bits(0,1);
io.dmaFillWait = io.dmaMode == 2 && io.command.bits(4,5) == 2;
if(dma.io.mode == 3) dma.io.wait = false;
return;
}
@ -162,12 +163,12 @@ auto VDP::writeControlPort(uint16 data) -> void {
case 0x01: {
io.videoMode = data.bit(2);
io.overscan = data.bit(3);
io.dmaEnable = data.bit(4);
dma.io.enable = data.bit(4);
io.verticalBlankInterruptEnable = data.bit(5);
io.displayEnable = data.bit(6);
io.externalVRAM = data.bit(7);
if(!io.dmaEnable) io.command.bit(5) = 0;
if(!dma.io.enable) io.command.bit(5) = 0;
return;
}
@ -280,32 +281,33 @@ auto VDP::writeControlPort(uint16 data) -> void {
//DMA length
case 0x13: {
io.dmaLength.bits(0,7) = data.bits(0,7);
dma.io.length.bits(0,7) = data.bits(0,7);
return;
}
//DMA length
case 0x14: {
io.dmaLength.bits(8,15) = data.bits(0,7);
dma.io.length.bits(8,15) = data.bits(0,7);
return;
}
//DMA source
case 0x15: {
io.dmaSource.bits(0,7) = data.bits(0,7);
dma.io.source.bits(0,7) = data.bits(0,7);
return;
}
//DMA source
case 0x16: {
io.dmaSource.bits(8,15) = data.bits(0,7);
dma.io.source.bits(8,15) = data.bits(0,7);
return;
}
//DMA source
case 0x17: {
io.dmaSource.bits(16,21) = data.bits(0,5);
io.dmaMode = data.bits(6,7);
dma.io.source.bits(16,21) = data.bits(0,5);
dma.io.mode = data.bits(6,7);
dma.io.wait = dma.io.mode.bit(1);
return;
}

View File

@ -42,7 +42,7 @@ auto VDP::main() -> void {
auto VDP::step(uint clocks) -> void {
while(clocks--) {
dmaRun();
dma.run();
Thread::step(1);
synchronize(cpu);
synchronize(apu);
@ -63,6 +63,7 @@ auto VDP::power() -> void {
window.power();
planeB.power();
sprite.power();
dma.power();
}
}

View File

@ -19,10 +19,23 @@ struct VDP : Thread {
auto writeControlPort(uint16 data) -> void;
//dma.cpp
auto dmaRun() -> void;
auto dmaLoad() -> void;
auto dmaFill() -> void;
auto dmaCopy() -> void;
struct DMA {
auto run() -> void;
auto load() -> void;
auto fill() -> void;
auto copy() -> void;
auto power() -> void;
struct IO {
uint2 mode;
uint22 source;
uint16 length;
uint8 fill;
boolean enable;
boolean wait;
} io;
} dma;
//render.cpp
auto scanline() -> void;
@ -119,15 +132,10 @@ private:
auto screenHeight() const -> uint { return io.overscan ? 240 : 224; }
uint16 vram[32768];
uint16 vramExpansion[32768]; //not present in stock Mega Drive hardware
uint9 cram[64];
uint10 vsram[40];
struct IO {
//internal state
boolean dmaFillWait;
uint8 dmaFillByte;
//command
uint6 command;
uint16 address;
@ -142,7 +150,6 @@ private:
//$01 mode register 2
uint1 videoMode; //0 = Master System; 1 = Mega Drive
uint1 overscan; //0 = 224 lines; 1 = 240 lines
uint1 dmaEnable;
uint1 verticalBlankInterruptEnable;
uint1 displayEnable;
uint1 externalVRAM;
@ -170,13 +177,6 @@ private:
//$0f data port auto-increment value
uint8 dataIncrement;
//$13-$14 DMA length
uint16 dmaLength;
//$15-$17 DMA source
uint22 dmaSource;
uint2 dmaMode;
} io;
struct State {

View File

@ -19,7 +19,7 @@ auto Bus::in(uint8 addr) -> uint8 {
switch(addr >> 6) {
case 0: {
if(system.model() == Model::GameGear) {
if(Model::GameGear()) {
bool start = !platform->inputPoll(ID::Port::Hardware, ID::Device::GameGearControls, 6);
return start << 7 | 0x7f;
}
@ -36,7 +36,7 @@ auto Bus::in(uint8 addr) -> uint8 {
}
case 3: {
if(system.model() == Model::MasterSystem) {
if(Model::MasterSystem()) {
bool reset = !platform->inputPoll(ID::Port::Hardware, ID::Device::MasterSystemControls, 0);
auto port1 = peripherals.controllerPort1->readData();
auto port2 = peripherals.controllerPort2->readData();
@ -46,7 +46,8 @@ auto Bus::in(uint8 addr) -> uint8 {
return port2.bits(2,5) << 0 | reset << 4 | 1 << 5 | port1.bit(6) << 6 | port2.bit(6) << 7;
}
}
if(system.model() == Model::GameGear) {
if(Model::GameGear()) {
bool up = !platform->inputPoll(ID::Port::Hardware, ID::Device::GameGearControls, 0);
bool down = !platform->inputPoll(ID::Port::Hardware, ID::Device::GameGearControls, 1);
bool left = !platform->inputPoll(ID::Port::Hardware, ID::Device::GameGearControls, 2);
@ -61,6 +62,7 @@ auto Bus::in(uint8 addr) -> uint8 {
return 0xff;
}
}
return 0xff;
}
@ -70,8 +72,16 @@ auto Bus::in(uint8 addr) -> uint8 {
}
auto Bus::out(uint8 addr, uint8 data) -> void {
if(addr == 0x06) {
if(Model::GameGear()) return psg.balance(data);
}
switch(addr >> 6) {
case 1: {
return psg.write(data);
}
case 2: {
return !addr.bit(0) ? vdp.data(data) : vdp.control(data);
}

View File

@ -8,17 +8,16 @@ Cartridge cartridge;
auto Cartridge::load() -> bool {
information = {};
switch(system.model()) {
case Model::MasterSystem:
if(Model::MasterSystem()) {
if(auto pathID = platform->load(ID::MasterSystem, "Master System", "ms")) {
information.pathID = pathID();
} else return false;
break;
case Model::GameGear:
}
if(Model::GameGear()) {
if(auto pathID = platform->load(ID::GameGear, "Game Gear", "gg")) {
information.pathID = pathID();
} else return false;
break;
}
if(auto fp = platform->open(pathID(), "manifest.bml", File::Read, File::Required)) {

View File

@ -31,7 +31,7 @@ auto CPU::step(uint clocks) -> void {
//called once per frame
auto CPU::pollPause() -> void {
if(system.model() == Model::MasterSystem) {
if(Model::MasterSystem()) {
static bool pause = 0;
bool state = platform->inputPoll(ID::Port::Hardware, ID::Device::MasterSystemControls, 1);
if(!pause && state) setNMI(1);

View File

@ -53,6 +53,6 @@ auto GameGearInterface::videoColor(uint32 color) -> uint64 {
}
auto GameGearInterface::load(uint id) -> bool {
if(id == ID::GameGear) return system.load(this, Model::GameGear);
if(id == ID::GameGear) return system.load(this, System::Model::GameGear);
return false;
}

View File

@ -69,6 +69,6 @@ auto MasterSystemInterface::videoColor(uint32 color) -> uint64 {
}
auto MasterSystemInterface::load(uint id) -> bool {
if(id == ID::MasterSystem) return system.load(this, Model::MasterSystem);
if(id == ID::MasterSystem) return system.load(this, System::Model::MasterSystem);
return false;
}

View File

@ -16,11 +16,6 @@ namespace MasterSystem {
extern Scheduler scheduler;
struct Interface;
enum class Model : uint {
MasterSystem,
GameGear,
};
struct Thread : Emulator::Thread {
auto create(auto (*entrypoint)() -> void, double frequency) -> void {
Emulator::Thread::create(entrypoint, frequency);
@ -32,6 +27,11 @@ namespace MasterSystem {
}
};
struct Model {
inline static auto MasterSystem() -> bool;
inline static auto GameGear() -> bool;
};
#include <ms/controller/controller.hpp>
#include <ms/cpu/cpu.hpp>

82
higan/ms/psg/io.cpp Normal file
View File

@ -0,0 +1,82 @@
//note: tone is supposed to reload counters on volume writes
//however, if this is always done, the output is very grainy
//as such, this behavior is suppressed when pitch >= 2 (which is a hack)
auto PSG::write(uint8 data) -> void {
bool l = data.bit(7);
if(l) select = data.bits(4,6);
switch(select) {
case 0: {
if(l) tone0.pitch.bits(0,3) = data.bits(0,3);
else tone0.pitch.bits(4,9) = data.bits(0,5);
break;
}
case 1: {
tone0.volume = data.bits(0,3);
if(tone0.pitch < 2) {
tone0.output = 1;
tone0.counter = tone0.pitch;
}
break;
}
case 2: {
if(l) tone1.pitch.bits(0,3) = data.bits(0,3);
else tone1.pitch.bits(4,9) = data.bits(0,5);
break;
}
case 3: {
tone1.volume = data.bits(0,3);
if(tone1.pitch < 2) {
tone1.output = 1;
tone1.counter = tone1.pitch;
}
break;
}
case 4: {
if(l) tone2.pitch.bits(0,3) = data.bits(0,3);
else tone2.pitch.bits(4,9) = data.bits(0,5);
break;
}
case 5: {
tone2.volume = data.bits(0,3);
if(tone2.pitch < 2) {
tone2.output = 1;
tone2.counter = tone2.pitch;
}
break;
}
case 6: {
noise.rate = data.bits(0,1);
noise.enable = data.bit(2);
noise.lfsr = 0x8000;
break;
}
case 7: {
noise.volume = data.bits(0,3);
noise.output = 1;
break;
}
}
}
//Game Gear only
auto PSG::balance(uint8 data) -> void {
tone0.right = data.bit(0);
tone1.right = data.bit(1);
tone2.right = data.bit(2);
noise.right = data.bit(3);
tone0.left = data.bit(4);
tone1.left = data.bit(5);
tone2.left = data.bit(6);
noise.left = data.bit(7);
}

30
higan/ms/psg/noise.cpp Normal file
View File

@ -0,0 +1,30 @@
auto PSG::Noise::run() -> void {
auto latch = clock;
counter++;
if(rate < 3) {
clock ^= counter & ((16 << rate) - 1) == 0;
} else {
clock ^= psg.tone2.clock;
}
if(!latch && clock) {
auto eor = enable ? ~lfsr >> 3 : 0;
lfsr = (lfsr ^ eor) << 15 | lfsr >> 1;
}
output = lfsr.bit(0);
}
auto PSG::Noise::power() -> void {
volume = ~0;
counter = 0;
enable = 0;
rate = 0;
lfsr = 0x8000;
clock = 0;
output = 0;
left = 1;
right = 1;
}

View File

@ -3,14 +3,42 @@
namespace MasterSystem {
PSG psg;
#include "io.cpp"
#include "tone.cpp"
#include "noise.cpp"
auto PSG::Enter() -> void {
while(true) scheduler.synchronize(), psg.main();
}
auto PSG::main() -> void {
tone0.run();
tone1.run();
tone2.run();
noise.run();
int left = 0;
if(tone0.output && tone0.left) left += levels[tone0.volume];
if(tone1.output && tone1.left) left += levels[tone1.volume];
if(tone2.output && tone2.left) left += levels[tone2.volume];
if(noise.output && noise.left) left += levels[noise.volume];
lowpassLeft += (left - lowpassLeft) * 20.0 / 256.0;
left = left * 2.0 / 6.0 + lowpassLeft * 3.0 / 4.0;
left = sclamp<16>(left);
int right = 0;
if(tone0.output && tone0.right) right += levels[tone0.volume];
if(tone1.output && tone1.right) right += levels[tone1.volume];
if(tone2.output && tone2.right) right += levels[tone2.volume];
if(noise.output && noise.right) right += levels[noise.volume];
lowpassRight += (right - lowpassRight) * 20.0 / 256.0;
right = right * 2.0 / 6.0 + lowpassRight * 3.0 / 4.0;
right = sclamp<16>(right);
step(1);
stream->sample(0.0, 0.0);
stream->sample(left / 32768.0, right / 32768.0);
}
auto PSG::step(uint clocks) -> void {
@ -19,8 +47,23 @@ auto PSG::step(uint clocks) -> void {
}
auto PSG::power() -> void {
create(PSG::Enter, system.colorburst());
stream = Emulator::audio.createStream(2, system.colorburst());
//Master System is monaural; Game Gear is stereo
//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());
select = 0;
lowpassLeft = 0;
lowpassRight = 0;
for(auto n : range(15)) {
levels[n] = 0x3fff * pow(2, n * -2.0 / 6.0) + 0.5;
}
levels[15] = 0;
tone0.power();
tone1.power();
tone2.power();
noise.power();
}
}

View File

@ -8,6 +8,48 @@ struct PSG : Thread {
auto step(uint clocks) -> void;
auto power() -> void;
//io.cpp
auto write(uint8 data) -> void;
auto balance(uint8 data) -> void;
private:
struct Tone {
//tone.cpp
auto run() -> void;
auto power() -> void;
uint4 volume;
uint10 counter;
uint10 pitch;
uint1 clock;
uint1 output;
uint1 left;
uint1 right;
} tone0, tone1, tone2;
struct Noise {
//noise.cpp
auto run() -> void;
auto power() -> void;
uint4 volume;
uint6 counter;
uint1 enable;
uint2 rate;
uint16 lfsr;
uint1 clock;
uint1 output;
uint1 left;
uint1 right;
} noise;
uint3 select;
int lowpassLeft;
int lowpassRight;
uint16 levels[16];
};
extern PSG psg;

19
higan/ms/psg/tone.cpp Normal file
View File

@ -0,0 +1,19 @@
auto PSG::Tone::run() -> void {
clock = 0;
if(--counter) return;
clock = 1;
counter = pitch;
output ^= 1;
}
auto PSG::Tone::power() -> void {
volume = ~0;
counter = 0;
pitch = 0;
clock = 0;
output = 0;
left = 1;
right = 1;
}

View File

@ -15,7 +15,7 @@ auto Peripherals::reset() -> void {
auto Peripherals::connect(uint port, uint device) -> void {
cpu.peripherals.reset();
if(system.model() == Model::MasterSystem) {
if(Model::MasterSystem()) {
if(port == ID::Port::Controller1) {
settings.controllerPort1 = device;
if(!system.loaded()) return;

View File

@ -1,4 +1,6 @@
struct System {
enum class Model : uint { MasterSystem, GameGear };
auto loaded() const -> bool { return information.loaded; }
auto model() const -> Model { return information.model; }
auto colorburst() const -> double { return information.colorburst; }
@ -33,3 +35,6 @@ struct Peripherals {
extern System system;
extern Peripherals peripherals;
auto Model::MasterSystem() -> bool { return system.model() == System::Model::MasterSystem; }
auto Model::GameGear() -> bool { return system.model() == System::Model::GameGear; }

View File

@ -51,8 +51,8 @@ auto VDP::data(uint8 data) -> void {
vram[io.address++] = data;
} else {
uint mask = 0;
if(system.model() == Model::MasterSystem) mask = 0x1f;
if(system.model() == Model::GameGear) mask = 0x3f;
if(Model::MasterSystem()) mask = 0x1f;
if(Model::GameGear()) mask = 0x3f;
cram[io.address++ & mask] = data;
}
}

View File

@ -72,7 +72,7 @@ auto VDP::step(uint clocks) -> void {
}
auto VDP::refresh() -> void {
if(system.model() == Model::MasterSystem) {
if(Model::MasterSystem()) {
//center the video output vertically in the viewport
uint32* screen = buffer;
if(vlines() == 224) screen += 16 * 256;
@ -81,7 +81,7 @@ auto VDP::refresh() -> void {
Emulator::video.refresh(screen, 256 * sizeof(uint32), 256, 240);
}
if(system.model() == Model::GameGear) {
if(Model::GameGear()) {
Emulator::video.refresh(buffer + 48 * 256 + 48, 256 * sizeof(uint32), 160, 144);
}
}
@ -107,11 +107,11 @@ auto VDP::power() -> void {
}
auto VDP::palette(uint5 index) -> uint12 {
if(system.model() == Model::MasterSystem) {
if(Model::MasterSystem()) {
return cram[index];
}
if(system.model() == Model::GameGear) {
if(Model::GameGear()) {
return cram[index * 2 + 0] << 0 | cram[index * 2 + 1] << 8;
}