From 34b99816f53139ca4e484a78740a1beec1292664 Mon Sep 17 00:00:00 2001 From: Luke Usher Date: Mon, 5 Oct 2020 08:58:19 +0100 Subject: [PATCH] Preliminary ZX Spectrum support for higan --- README.md | 2 +- higan-ui/GNUmakefile | 2 +- higan-ui/higan-ui.cpp | 6 + higan/GNUmakefile | 4 + .../ZX Spectrum 128/Expansion/.gitignore | 0 .../Expansion/Kempston/.gitignore | 0 .../ZX Spectrum 128/Keyboard/.gitignore | 0 .../Keyboard/Original/layout.bml | 43 +++++ higan/System/ZX Spectrum 128/bios.rom | Bin 0 -> 16384 bytes higan/System/ZX Spectrum 128/sub.rom | Bin 0 -> 16384 bytes .../ZX Spectrum 48k/Expansion/.gitignore | 0 .../Expansion/Kempston/.gitignore | 0 .../ZX Spectrum 48k/Keyboard/.gitignore | 0 .../Keyboard/Original/layout.bml | 43 +++++ higan/System/ZX Spectrum 48k/bios.rom | Bin 0 -> 16384 bytes higan/spec/GNUmakefile | 14 ++ higan/spec/cpu/cpu.cpp | 55 ++++++ higan/spec/cpu/cpu.hpp | 49 +++++ higan/spec/cpu/debugger.cpp | 27 +++ higan/spec/cpu/memory.cpp | 114 ++++++++++++ higan/spec/cpu/serialization.cpp | 7 + higan/spec/expansion/expansion.cpp | 8 + higan/spec/expansion/expansion.hpp | 13 ++ higan/spec/expansion/kempston/kempston.cpp | 27 +++ higan/spec/expansion/kempston/kempston.hpp | 18 ++ higan/spec/expansion/port.cpp | 26 +++ higan/spec/expansion/port.hpp | 23 +++ higan/spec/interface/interface.cpp | 44 +++++ higan/spec/interface/interface.hpp | 31 ++++ higan/spec/keyboard/keyboard.cpp | 76 ++++++++ higan/spec/keyboard/keyboard.hpp | 23 +++ higan/spec/psg/psg.cpp | 53 ++++++ higan/spec/psg/psg.hpp | 23 +++ higan/spec/psg/serialization.cpp | 4 + higan/spec/spec.hpp | 29 +++ higan/spec/system/serialization.cpp | 71 +++++++ higan/spec/system/system.cpp | 110 +++++++++++ higan/spec/system/system.hpp | 55 ++++++ higan/spec/tape/deck.cpp | 20 ++ higan/spec/tape/deck.hpp | 23 +++ higan/spec/tape/tape.cpp | 92 +++++++++ higan/spec/tape/tape.hpp | 33 ++++ higan/spec/tape/tray.cpp | 14 ++ higan/spec/tape/tray.hpp | 9 + higan/spec/ula/color.cpp | 22 +++ higan/spec/ula/serialization.cpp | 4 + higan/spec/ula/ula.cpp | 174 ++++++++++++++++++ higan/spec/ula/ula.hpp | 49 +++++ icarus/icarus.cpp | 2 + icarus/icarus.hpp | 1 + icarus/tape/tape.cpp | 52 ++++++ icarus/tape/tape.hpp | 14 ++ icarus/tape/zx-spectrum-tape.cpp | 14 ++ icarus/tape/zx-spectrum-tape.hpp | 6 + 54 files changed, 1527 insertions(+), 2 deletions(-) create mode 100644 higan/System/ZX Spectrum 128/Expansion/.gitignore create mode 100644 higan/System/ZX Spectrum 128/Expansion/Kempston/.gitignore create mode 100644 higan/System/ZX Spectrum 128/Keyboard/.gitignore create mode 100644 higan/System/ZX Spectrum 128/Keyboard/Original/layout.bml create mode 100644 higan/System/ZX Spectrum 128/bios.rom create mode 100644 higan/System/ZX Spectrum 128/sub.rom create mode 100644 higan/System/ZX Spectrum 48k/Expansion/.gitignore create mode 100644 higan/System/ZX Spectrum 48k/Expansion/Kempston/.gitignore create mode 100644 higan/System/ZX Spectrum 48k/Keyboard/.gitignore create mode 100644 higan/System/ZX Spectrum 48k/Keyboard/Original/layout.bml create mode 100644 higan/System/ZX Spectrum 48k/bios.rom create mode 100644 higan/spec/GNUmakefile create mode 100644 higan/spec/cpu/cpu.cpp create mode 100644 higan/spec/cpu/cpu.hpp create mode 100644 higan/spec/cpu/debugger.cpp create mode 100644 higan/spec/cpu/memory.cpp create mode 100644 higan/spec/cpu/serialization.cpp create mode 100644 higan/spec/expansion/expansion.cpp create mode 100644 higan/spec/expansion/expansion.hpp create mode 100644 higan/spec/expansion/kempston/kempston.cpp create mode 100644 higan/spec/expansion/kempston/kempston.hpp create mode 100644 higan/spec/expansion/port.cpp create mode 100644 higan/spec/expansion/port.hpp create mode 100644 higan/spec/interface/interface.cpp create mode 100644 higan/spec/interface/interface.hpp create mode 100644 higan/spec/keyboard/keyboard.cpp create mode 100644 higan/spec/keyboard/keyboard.hpp create mode 100644 higan/spec/psg/psg.cpp create mode 100644 higan/spec/psg/psg.hpp create mode 100644 higan/spec/psg/serialization.cpp create mode 100644 higan/spec/spec.hpp create mode 100644 higan/spec/system/serialization.cpp create mode 100644 higan/spec/system/system.cpp create mode 100644 higan/spec/system/system.hpp create mode 100644 higan/spec/tape/deck.cpp create mode 100644 higan/spec/tape/deck.hpp create mode 100644 higan/spec/tape/tape.cpp create mode 100644 higan/spec/tape/tape.hpp create mode 100644 higan/spec/tape/tray.cpp create mode 100644 higan/spec/tape/tray.hpp create mode 100644 higan/spec/ula/color.cpp create mode 100644 higan/spec/ula/serialization.cpp create mode 100644 higan/spec/ula/ula.cpp create mode 100644 higan/spec/ula/ula.hpp create mode 100644 icarus/tape/tape.cpp create mode 100644 icarus/tape/tape.hpp create mode 100644 icarus/tape/zx-spectrum-tape.cpp create mode 100644 icarus/tape/zx-spectrum-tape.hpp diff --git a/README.md b/README.md index 0758915b4..055c65551 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Super Famicom, Super Game Boy, Game Boy, Game Boy Color, Game Boy Advance, Game Boy Player, SG-1000, SC-3000, Master System, Game Gear, Mega Drive, Mega CD, PC Engine, SuperGrafx, MSX, MSX2, ColecoVision, Neo Geo Pocket, Neo Geo Pocket Color, WonderSwan, WonderSwan Color, SwanCrystal, -Pocket Challenge V2. +Pocket Challenge V2, ZX Spectrum 48k, ZX Spectrum 128. Links ----- diff --git a/higan-ui/GNUmakefile b/higan-ui/GNUmakefile index e06561e04..5aeb85981 100644 --- a/higan-ui/GNUmakefile +++ b/higan-ui/GNUmakefile @@ -22,7 +22,7 @@ hiro.resource := resource/higan-ui.rc include $(hiro.path)/GNUmakefile profile := accuracy -cores := fc sfc n64 sg ms md ps1 pce msx cv gb gba ws ngp +cores := fc sfc n64 sg ms md ps1 pce msx cv gb gba ws ngp spec higan.path := ../higan include $(higan.path)/GNUmakefile diff --git a/higan-ui/higan-ui.cpp b/higan-ui/higan-ui.cpp index ef3006f37..cb67279d4 100644 --- a/higan-ui/higan-ui.cpp +++ b/higan-ui/higan-ui.cpp @@ -27,6 +27,7 @@ Input inputInstance; #include #include #include +#include #include auto nall::main(Arguments arguments) -> void { @@ -159,6 +160,11 @@ auto nall::main(Arguments arguments) -> void { interfaces.append(new higan::WonderSwan::WonderSwanColorInterface); #endif + #ifdef CORE_SPEC + interfaces.append(new higan::Spectrum::Spectrum48kInterface); + interfaces.append(new higan::Spectrum::Spectrum128Interface); + #endif + higan::platform = &emulator; Instances::program.construct(); diff --git a/higan/GNUmakefile b/higan/GNUmakefile index 288006aa1..a70d0a75a 100644 --- a/higan/GNUmakefile +++ b/higan/GNUmakefile @@ -78,6 +78,10 @@ ifneq ($(filter $(cores),ngp),) include $(higan.path)/ngp/GNUmakefile endif +ifneq ($(filter $(cores),spec),) + include $(higan.path)/spec/GNUmakefile +endif + include $(higan.path)/component/GNUmakefile flags += $(foreach c,$(call strupper,$(cores)),-DCORE_$c) diff --git a/higan/System/ZX Spectrum 128/Expansion/.gitignore b/higan/System/ZX Spectrum 128/Expansion/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/higan/System/ZX Spectrum 128/Expansion/Kempston/.gitignore b/higan/System/ZX Spectrum 128/Expansion/Kempston/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/higan/System/ZX Spectrum 128/Keyboard/.gitignore b/higan/System/ZX Spectrum 128/Keyboard/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/higan/System/ZX Spectrum 128/Keyboard/Original/layout.bml b/higan/System/ZX Spectrum 128/Keyboard/Original/layout.bml new file mode 100644 index 000000000..3e6957b48 --- /dev/null +++ b/higan/System/ZX Spectrum 128/Keyboard/Original/layout.bml @@ -0,0 +1,43 @@ +layout + name: Original + + key: 1 + key: 2 + key: 3 + key: 4 + key: 5 + key: 6 + key: 7 + key: 8 + key: 9 + key: 0 + key: Q + key: W + key: E + key: R + key: T + key: Y + key: U + key: I + key: O + key: P + key: A + key: S + key: D + key: F + key: G + key: H + key: J + key: K + key: L + key: Z + key: X + key: C + key: V + key: B + key: N + key: M + key: ENTER + key: SPACE BREAK + key: CAPS SHIFT + key: SYMBOL SHIFT diff --git a/higan/System/ZX Spectrum 128/bios.rom b/higan/System/ZX Spectrum 128/bios.rom new file mode 100644 index 0000000000000000000000000000000000000000..c4a04e86ac94d8eb3fa760e7241a77ef58a422e7 GIT binary patch literal 16384 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
ipD&GM&n~T#SW1#>(92sPt_@C${X0r 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 literal 0 HcmV?d00001 diff --git a/higan/System/ZX Spectrum 128/sub.rom b/higan/System/ZX Spectrum 128/sub.rom new file mode 100644 index 0000000000000000000000000000000000000000..64c3e73769cb459680f335a2db0abc557c243d8b GIT binary patch literal 16384 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 Qs19QP8;1_PQIz+80l1Bavj6}9 literal 0 HcmV?d00001 diff --git a/higan/System/ZX Spectrum 48k/Expansion/.gitignore b/higan/System/ZX Spectrum 48k/Expansion/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/higan/System/ZX Spectrum 48k/Expansion/Kempston/.gitignore b/higan/System/ZX Spectrum 48k/Expansion/Kempston/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/higan/System/ZX Spectrum 48k/Keyboard/.gitignore b/higan/System/ZX Spectrum 48k/Keyboard/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/higan/System/ZX Spectrum 48k/Keyboard/Original/layout.bml b/higan/System/ZX Spectrum 48k/Keyboard/Original/layout.bml new file mode 100644 index 000000000..3e6957b48 --- /dev/null +++ b/higan/System/ZX Spectrum 48k/Keyboard/Original/layout.bml @@ -0,0 +1,43 @@ +layout + name: Original + + key: 1 + key: 2 + key: 3 + key: 4 + key: 5 + key: 6 + key: 7 + key: 8 + key: 9 + key: 0 + key: Q + key: W + key: E + key: R + key: T + key: Y + key: U + key: I + key: O + key: P + key: A + key: S + key: D + key: F + key: G + key: H + key: J + key: K + key: L + key: Z + key: X + key: C + key: V + key: B + key: N + key: M + key: ENTER + key: SPACE BREAK + key: CAPS SHIFT + key: SYMBOL SHIFT diff --git a/higan/System/ZX Spectrum 48k/bios.rom b/higan/System/ZX Spectrum 48k/bios.rom new file mode 100644 index 0000000000000000000000000000000000000000..4d6895e0bbf3fa543f192a66a297192b3c969ac9 GIT binary patch literal 16384 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

5RYmg65FY9s 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 literal 0 HcmV?d00001 diff --git a/higan/spec/GNUmakefile b/higan/spec/GNUmakefile new file mode 100644 index 000000000..a39e20a8e --- /dev/null +++ b/higan/spec/GNUmakefile @@ -0,0 +1,14 @@ +higan.components += z80 + +higan.objects += higan-spec-interface higan-spec-keyboard higan-spec-expansion higan-spec-system +higan.objects += higan-spec-cpu higan-spec-ula higan-spec-psg +higan.objects += higan-spec-tape + +$(object.path)/higan-spec-interface.o: $(higan.path)/spec/interface/interface.cpp +$(object.path)/higan-spec-keyboard.o: $(higan.path)/spec/keyboard/keyboard.cpp +$(object.path)/higan-spec-expansion.o: $(higan.path)/spec/expansion/expansion.cpp +$(object.path)/higan-spec-system.o: $(higan.path)/spec/system/system.cpp +$(object.path)/higan-spec-cpu.o: $(higan.path)/spec/cpu/cpu.cpp +$(object.path)/higan-spec-ula.o: $(higan.path)/spec/ula/ula.cpp +$(object.path)/higan-spec-psg.o: $(higan.path)/spec/psg/psg.cpp +$(object.path)/higan-spec-tape.o: $(higan.path)/spec/tape/tape.cpp diff --git a/higan/spec/cpu/cpu.cpp b/higan/spec/cpu/cpu.cpp new file mode 100644 index 000000000..bc8ba63f2 --- /dev/null +++ b/higan/spec/cpu/cpu.cpp @@ -0,0 +1,55 @@ +#include + +namespace higan::Spectrum { + +CPU cpu; +#include "memory.cpp" +#include "debugger.cpp" +#include "serialization.cpp" + +auto CPU::load(Node::Object parent) -> void { + if(Model::Spectrum48k()) ram.allocate(48_KiB); + if(Model::Spectrum128()) ram.allocate(128_KiB); + + ram.fill(0x00); + node = parent->append("CPU"); + + debugger.load(node); +} + +auto CPU::unload() -> void { + ram.reset(); + node = {}; + debugger = {}; +} + +auto CPU::main() -> void { + if(irqLine) { + debugger.interrupt("IRQ"); + irq(1, 0x0038, 0xff); + } + + debugger.instruction(); + instruction(); +} + +auto CPU::step(uint clocks) -> void { + Thread::step(clocks); + Thread::synchronize(); +} + +auto CPU::power() -> void { + Z80::bus = this; + Z80::power(); + Thread::create(system.frequency(), {&CPU::main, this}); + + r.pc = 0x0000; //reset vector address + + irqLine = false; +} + +auto CPU::setIrq(bool line) -> void { + irqLine = line; +} + +} diff --git a/higan/spec/cpu/cpu.hpp b/higan/spec/cpu/cpu.hpp new file mode 100644 index 000000000..d401c1680 --- /dev/null +++ b/higan/spec/cpu/cpu.hpp @@ -0,0 +1,49 @@ +struct CPU : Z80, Z80::Bus, Thread { + Node::Component node; + Memory::Writable ram; + + struct Debugger { + //debugger.cpp + auto load(Node::Object) -> void; + auto instruction() -> void; + auto interrupt(string_view) -> void; + + struct Memory { + Node::Memory ram; + } memory; + + struct Tracer { + Node::Instruction instruction; + Node::Notification interrupt; + } tracer; + } debugger; + + auto synchronizing() const -> bool override { return scheduler.synchronizing(); } + + //cpu.cpp + auto load(Node::Object) -> void; + auto unload() -> void; + + auto main() -> void; + auto step(uint clocks) -> void override; + + auto power() -> void; + auto setIrq(bool line) -> void; + + //memory.cpp + auto read(uint16 address) -> uint8 override; + auto write(uint16 address, uint8 data) -> void override; + auto readBanked(uint3 bank, uint16 address) -> uint8; + auto writeBanked(uint3 bank, uint16 address, uint8 data) -> void; + + auto in(uint16 address) -> uint8 override; + auto out(uint16 address, uint8 data) -> void override; + + //serialization.cpp + auto serialize(serializer&) -> void; + +private: + uint1 irqLine; +}; + +extern CPU cpu; diff --git a/higan/spec/cpu/debugger.cpp b/higan/spec/cpu/debugger.cpp new file mode 100644 index 000000000..158b65d6c --- /dev/null +++ b/higan/spec/cpu/debugger.cpp @@ -0,0 +1,27 @@ +auto CPU::Debugger::load(Node::Object parent) -> void { + memory.ram = parent->append("CPU RAM"); + memory.ram->setSize(cpu.ram.size()); + memory.ram->setRead([&](uint32 address) -> uint8 { + return cpu.ram[address]; + }); + memory.ram->setWrite([&](uint32 address, uint8 data) -> void { + cpu.ram[address] = data; + }); + + tracer.instruction = parent->append("Instruction", "CPU"); + tracer.instruction->setAddressBits(16); + + tracer.interrupt = parent->append("Interrupt", "CPU"); +} + +auto CPU::Debugger::instruction() -> void { + if(tracer.instruction->enabled() && tracer.instruction->address(cpu.r.pc)) { + tracer.instruction->notify(cpu.disassembleInstruction(), cpu.disassembleContext()); + } +} + +auto CPU::Debugger::interrupt(string_view type) -> void { + if(tracer.interrupt->enabled()) { + tracer.interrupt->notify(type); + } +} diff --git a/higan/spec/cpu/memory.cpp b/higan/spec/cpu/memory.cpp new file mode 100644 index 000000000..10b80a507 --- /dev/null +++ b/higan/spec/cpu/memory.cpp @@ -0,0 +1,114 @@ +auto CPU::read(uint16 address) -> uint8 { + if (address < 0x4000) { + if (expansionPort.connected() && expansionPort.romcs()) { + return expansionPort.read(address); + } + + if (system.romBank) { + return rom.sub.read(address); + } + + return rom.bios.read(address); + } + + if (Model::Spectrum48k()) { + return ram.read(address - 0x4000); + } + + if (address < 0x8000) { + return readBanked(5, address - 0x4000); + } + + if (address < 0xC000) { + return readBanked(2, address - 0x8000); + } + + return readBanked(system.ramBank, address - 0xC000); +} + +auto CPU::write(uint16 address, uint8 data) -> void { + if (expansionPort.connected() && expansionPort.mapped(address, false)) { + expansionPort.write(address, data); + } + + if (address < 0x4000) { + return; + } + + if (Model::Spectrum48k()) { + return ram.write(address - 0x4000, data); + } + + if (address < 0x8000) { + return writeBanked(5, address - 0x4000, data); + } + + if (address < 0xC000) { + return writeBanked(2, address - 0x8000, data); + } + + return writeBanked(system.ramBank, address - 0xC000, data); +} + +auto CPU::readBanked(uint3 bank, uint16 address) -> uint8 { + return ram.read((0x4000 * (uint32)bank) + address); +} + +auto CPU::writeBanked(uint3 bank, uint16 address, uint8 data) -> void { + return ram.write((0x4000 * (uint32)bank) + address, data); +} + +auto CPU::in(uint16 address) -> uint8 { + if (expansionPort.connected() && expansionPort.mapped(address, true)) { + if (address.bit(0) == 0) { + // Some expansion port devices override keyboard (bits 0-4) of the ULA + uint8 value = ula.in(address); + uint8 expValue = expansionPort.in(address); + value.bit(0, 4) &= expValue.bit(0,4); + return value; + } + + return expansionPort.in(address); + } + + if (address.bit(0) == 0) { + return ula.in(address); + } + + if (Model::Spectrum128()) { + if (address.bit(1) == 0 && address.bit(15) == 1) { + return psg.read(); + } + } + + return ula.floatingBus(); +} + +auto CPU::out(uint16 address, uint8 data) -> void { + if (expansionPort.connected() && expansionPort.mapped(address, true)) { + expansionPort.out(address, data); + } + + if (address.bit(0) == 0) { + ula.out(data); + } + + if (Model::Spectrum128()) { + if (address.bit(1) == 0 && address.bit(15) == 0 && !system.pagingDisabled) { + system.ramBank = data.bit(0, 2); + system.screenBank = data.bit(3); + system.romBank = data.bit(4); + system.pagingDisabled = data.bit(5); + } + + if (address.bit(1) == 0 && address.bit(15) == 1) { + if (address.bit(14) == 1) { + psg.select(data); + } + + if (address.bit(14) == 0) { + psg.write(data); + } + } + } +} diff --git a/higan/spec/cpu/serialization.cpp b/higan/spec/cpu/serialization.cpp new file mode 100644 index 000000000..7fcd84fdc --- /dev/null +++ b/higan/spec/cpu/serialization.cpp @@ -0,0 +1,7 @@ +auto CPU::serialize(serializer& s) -> void { + Z80::serialize(s); + Z80::Bus::serialize(s); + Thread::serialize(s); + + ram.serialize(s); +} diff --git a/higan/spec/expansion/expansion.cpp b/higan/spec/expansion/expansion.cpp new file mode 100644 index 000000000..347978c61 --- /dev/null +++ b/higan/spec/expansion/expansion.cpp @@ -0,0 +1,8 @@ +#include + +namespace higan::Spectrum { + +#include "port.cpp" +#include "kempston/kempston.cpp" + +} diff --git a/higan/spec/expansion/expansion.hpp b/higan/spec/expansion/expansion.hpp new file mode 100644 index 000000000..6224f17eb --- /dev/null +++ b/higan/spec/expansion/expansion.hpp @@ -0,0 +1,13 @@ +struct Expansion { + Node::Peripheral node; + + virtual auto romcs() -> bool = 0; + virtual auto mapped(uint16 address, bool io) -> bool = 0; + virtual auto read(uint16 address) -> uint8 = 0; + virtual auto write(uint16 address, uint8 data) -> void = 0; + virtual auto in(uint16 address) -> uint8 = 0; + virtual auto out(uint16 address, uint8 data) -> void = 0; +}; + +#include "kempston/kempston.hpp" +#include "port.hpp" diff --git a/higan/spec/expansion/kempston/kempston.cpp b/higan/spec/expansion/kempston/kempston.cpp new file mode 100644 index 000000000..513135ad3 --- /dev/null +++ b/higan/spec/expansion/kempston/kempston.cpp @@ -0,0 +1,27 @@ +Kempston::Kempston(Node::Port parent) { + node = parent->append("Kempston"); + + up = node->append("Up"); + down = node->append("Down"); + left = node->append("Left"); + right = node->append("Right"); + fire = node->append("Fire"); +} + +auto Kempston::in(uint16 address) -> uint8 +{ + platform->input(up); + platform->input(down); + platform->input(left); + platform->input(right); + platform->input(fire); + + uint8 value = 0; + value.bit(0) = right->value(); + value.bit(1) = left->value(); + value.bit(2) = down->value(); + value.bit(3) = up->value(); + value.bit(4) = fire->value(); + + return value; +} diff --git a/higan/spec/expansion/kempston/kempston.hpp b/higan/spec/expansion/kempston/kempston.hpp new file mode 100644 index 000000000..6db289cdd --- /dev/null +++ b/higan/spec/expansion/kempston/kempston.hpp @@ -0,0 +1,18 @@ +struct Kempston : Expansion { + Node::Button up; + Node::Button down; + Node::Button left; + Node::Button right; + Node::Button fire; + + Kempston(Node::Port parent); + + auto romcs() -> bool { return false; } + auto mapped(uint16 address, bool io) -> bool { return io && (uint8)address == 0x1f; } + + auto read(uint16 address) -> uint8 { return 0xff; } + auto write(uint16 address, uint8 data) -> void {} + + auto in(uint16 address) -> uint8; + auto out(uint16 address, uint8 data) -> void {} +}; diff --git a/higan/spec/expansion/port.cpp b/higan/spec/expansion/port.cpp new file mode 100644 index 000000000..1bd7ce94f --- /dev/null +++ b/higan/spec/expansion/port.cpp @@ -0,0 +1,26 @@ +ExpansionPort expansionPort{"Expansion Port"}; + +ExpansionPort::ExpansionPort(string name) : name(name) { +} + +auto ExpansionPort::load(Node::Object parent) -> void { + port = parent->append(name); + port->setFamily("ZX Spectrum"); + port->setType("Expansion"); + port->setHotSwappable(false); + port->setAllocate([&](auto name) { return allocate(name); }); +} + +auto ExpansionPort::unload() -> void { + device = {}; + port = {}; +} + +auto ExpansionPort::allocate(string name) -> Node::Peripheral { + if(name == "Kempston") device = new Kempston(port); + if(device) return device->node; + return {}; +} + +auto ExpansionPort::serialize(serializer& s) -> void { +} diff --git a/higan/spec/expansion/port.hpp b/higan/spec/expansion/port.hpp new file mode 100644 index 000000000..84a3258a0 --- /dev/null +++ b/higan/spec/expansion/port.hpp @@ -0,0 +1,23 @@ +struct ExpansionPort { + Node::Port port; + unique_pointer device; + + ExpansionPort(string name); + auto load(Node::Object parent) -> void; + auto unload() -> void; + auto allocate(string name) -> Node::Peripheral; + + auto connected() -> bool { return device ? true : false; } + auto romcs() -> bool { return device ? device->romcs() : false; } + auto mapped(uint16 address, bool io) -> bool { return device ? device->mapped(address, io) : false; } + auto read(uint16 address) -> uint8 { return device ? device->read(address) : (uint8)0xff; } + auto write(uint16 address, uint8 data) -> void { if (device) device->write(address, data); } + auto in(uint16 address) -> uint8 { return device ? device->in(address) : (uint8)0xff;} + auto out(uint16 address, uint8 data) -> void { if (device) device->out(address, data); } + + auto serialize(serializer&) -> void; + + const string name; +}; + +extern ExpansionPort expansionPort; diff --git a/higan/spec/interface/interface.cpp b/higan/spec/interface/interface.cpp new file mode 100644 index 000000000..0ee2d529d --- /dev/null +++ b/higan/spec/interface/interface.cpp @@ -0,0 +1,44 @@ +#include + +namespace higan::Spectrum { + +Interface* interface = nullptr; + +auto AbstractInterface::game() -> string { + return ""; +} + +auto AbstractInterface::root() -> Node::Object { + return system.node; +} + +auto AbstractInterface::load(Node::Object& root) -> void { + interface = this; + system.load(root); +} + +auto AbstractInterface::unload() -> void { + system.unload(); +} + +auto AbstractInterface::save() -> void { + system.save(); +} + +auto AbstractInterface::power() -> void { + system.power(); +} + +auto AbstractInterface::run() -> void { + system.run(); +} + +auto AbstractInterface::serialize(bool synchronize) -> serializer { + return system.serialize(synchronize); +} + +auto AbstractInterface::unserialize(serializer& s) -> bool { + return system.unserialize(s); +} + +} diff --git a/higan/spec/interface/interface.hpp b/higan/spec/interface/interface.hpp new file mode 100644 index 000000000..8c846a058 --- /dev/null +++ b/higan/spec/interface/interface.hpp @@ -0,0 +1,31 @@ +#if defined(CORE_SPEC) + +namespace higan::Spectrum { + +extern Interface* interface; + +struct AbstractInterface : Interface { + auto game() -> string override; + + auto root() -> Node::Object override; + auto load(Node::Object&) -> void override; + auto unload() -> void override; + auto save() -> void override; + auto power() -> void override; + auto run() -> void override; + + auto serialize(bool synchronize) -> serializer override; + auto unserialize(serializer&) -> bool override; +}; + +struct Spectrum48kInterface : AbstractInterface { + auto name() -> string override { return "ZX Spectrum 48k"; } +}; + +struct Spectrum128Interface : AbstractInterface { + auto name() -> string override { return "ZX Spectrum 128"; } +}; + +} + +#endif diff --git a/higan/spec/keyboard/keyboard.cpp b/higan/spec/keyboard/keyboard.cpp new file mode 100644 index 000000000..75aeb969c --- /dev/null +++ b/higan/spec/keyboard/keyboard.cpp @@ -0,0 +1,76 @@ +#include + +namespace higan::Spectrum { + +Keyboard keyboard; + +auto Keyboard::load(Node::Object parent) -> void { + port = parent->append("Keyboard"); + port->setFamily("ZX Spectrum"); + port->setType("Keyboard"); + port->setHotSwappable(true); + port->setAllocate([&](auto name) { return allocate(port, name); }); + port->setConnect([&] { connect(); }); + port->setDisconnect([&] { disconnect(); }); + port->setSupported({"Original"}); +} + +auto Keyboard::unload() -> void { + disconnect(); + port = {}; +} + +auto Keyboard::allocate(Node::Port parent, string name) -> Node::Peripheral { + return layout = parent->append(name); +} + +auto Keyboard::connect() -> void { + Markup::Node document; + if(auto fp = platform->open(layout, "layout.bml", File::Read)) { + document = BML::unserialize(fp->reads()); + } + + for(uint _key : range(40)) { + string label{_key}; + if(auto key = document[{"layout/key[", _key, "]"}]) { + label = key.text(); + } + keys[_key] = layout->append(label); + } +} + +auto Keyboard::disconnect() -> void { + layout = {}; + for(uint _key : range(40)) { + keys[_key] = {}; + } +} + +auto Keyboard::power() -> void { + +} + +auto Keyboard::read(uint8 row) -> uint5 { + constexpr uint rows[8][5] = { + {38, 29, 30, 31, 32}, + {20, 21, 22, 23, 24}, + {10, 11, 12, 13, 14}, + { 0, 1, 2, 3, 4}, + { 9, 8, 7, 6, 5}, + {19, 18, 17, 16, 15}, + {36, 28, 27, 26, 25}, + {37, 39, 35, 34, 33}, + }; + + uint5 data = 0x1f; + + for(uint _key : range(5)) { + if(auto node = keys[rows[row][_key]]) { + platform->input(node); + data.bit(_key) = !node->value(); + } + } + return data; +} + +} diff --git a/higan/spec/keyboard/keyboard.hpp b/higan/spec/keyboard/keyboard.hpp new file mode 100644 index 000000000..b899a0bac --- /dev/null +++ b/higan/spec/keyboard/keyboard.hpp @@ -0,0 +1,23 @@ +struct Keyboard { + Node::Port port; + Node::Peripheral layout; + + //keyboard.cpp + auto load(Node::Object) -> void; + auto unload() -> void; + + auto allocate(Node::Port, string) -> Node::Peripheral; + auto connect() -> void; + auto disconnect() -> void; + + auto power() -> void; + auto read(uint8 row) -> uint5 ; + + //serialization.cpp + auto serialize(serializer&) -> void; + +private: + Node::Button keys[40]; +}; + +extern Keyboard keyboard; diff --git a/higan/spec/psg/psg.cpp b/higan/spec/psg/psg.cpp new file mode 100644 index 000000000..3ec6bae69 --- /dev/null +++ b/higan/spec/psg/psg.cpp @@ -0,0 +1,53 @@ +#include + +namespace higan::Spectrum { + +PSG psg; +#include "serialization.cpp" + +auto PSG::load(Node::Object parent) -> void { + node = parent->append("PSG"); + + stream = node->append("PSG"); + stream->setChannels(1); + stream->setFrequency(system.frequency() / 16); +} + +auto PSG::unload() -> void { + node = {}; + stream = {}; +} + +auto PSG::main() -> void { + auto channels = AY38910::clock(); + double output = 0.0; + output += volume[channels[0]]; + output += volume[channels[1]]; + output += volume[channels[2]]; + stream->sample(output / 3.0); + step(1); +} + +auto PSG::step(uint clocks) -> void { + Thread::step(clocks); + Thread::synchronize(); +} + +auto PSG::power() -> void { + AY38910::power(); + Thread::create(system.frequency() / 16, {&PSG::main, this}); + + for(uint level : range(16)) { + volume[level] = 1.0 / pow(2, 1.0 / 2 * (15 - level)); + } +} + +auto PSG::readIO(uint1 port) -> uint8 { + return 0xff; +} + +auto PSG::writeIO(uint1 port, uint8 data) -> void { + +} + +} diff --git a/higan/spec/psg/psg.hpp b/higan/spec/psg/psg.hpp new file mode 100644 index 000000000..76ac45192 --- /dev/null +++ b/higan/spec/psg/psg.hpp @@ -0,0 +1,23 @@ +struct PSG : AY38910, Thread { + Node::Component node; + Node::Stream stream; + + //psg.cpp + auto load(Node::Object) -> void; + auto unload() -> void; + + auto main() -> void; + auto step(uint clocks) -> void; + auto power() -> void; + + auto readIO(uint1 port) -> uint8 override; + auto writeIO(uint1 port, uint8 data) -> void override; + + //serialization.cpp + auto serialize(serializer&) -> void; + +private: + double volume[16]; +}; + +extern PSG psg; diff --git a/higan/spec/psg/serialization.cpp b/higan/spec/psg/serialization.cpp new file mode 100644 index 000000000..913c76647 --- /dev/null +++ b/higan/spec/psg/serialization.cpp @@ -0,0 +1,4 @@ +auto PSG::serialize(serializer& s) -> void { + AY38910::serialize(s); + Thread::serialize(s); +} diff --git a/higan/spec/spec.hpp b/higan/spec/spec.hpp new file mode 100644 index 000000000..8d61c5038 --- /dev/null +++ b/higan/spec/spec.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include +#include + +namespace higan::Spectrum { + #include + + struct Model { + inline static auto Spectrum48k() -> bool; + inline static auto Spectrum128() -> bool; + }; + + struct Region { + inline static auto PAL() -> bool; + }; + + #include + #include + #include + #include + #include + #include + #include +} + +#include diff --git a/higan/spec/system/serialization.cpp b/higan/spec/system/serialization.cpp new file mode 100644 index 000000000..7174e2b0f --- /dev/null +++ b/higan/spec/system/serialization.cpp @@ -0,0 +1,71 @@ +auto System::serialize(bool synchronize) -> serializer { + if(synchronize) scheduler.enter(Scheduler::Mode::Synchronize); + serializer s{information.serializeSize[synchronize]}; + + uint signature = 0x31545342; + uint size = s.capacity(); + char version[16] = {}; + char description[512] = {}; + memory::copy(&version, (const char*)SerializerVersion, SerializerVersion.size()); + + s.integer(signature); + s.integer(size); + s.integer(synchronize); + s.array(version); + s.array(description); + serializeAll(s, synchronize); + return s; +} + +auto System::unserialize(serializer& s) -> bool { + uint signature = 0; + uint size = 0; + bool synchronize = true; + char version[16] = {}; + char description[512] = {}; + + s.integer(signature); + s.integer(size); + s.integer(synchronize); + s.array(version); + s.array(description); + + if(signature != 0x31545342) return false; + if(size != information.serializeSize[synchronize]) return false; + if(string{version} != SerializerVersion) return false; + + if(synchronize) power(); + serializeAll(s, synchronize); + return true; +} + +//internal + +auto System::serialize(serializer& s) -> void { +} + +auto System::serializeAll(serializer& s, bool synchronize) -> void { + scheduler.setSynchronize(synchronize); + system.serialize(s); + //keyboard.serialize(s); + cpu.serialize(s); + //vdp.serialize(s); + //psg.serialize(s); +} + +auto System::serializeInit(bool synchronize) -> uint { + serializer s; + + uint signature = 0; + uint size = 0; + char version[16] = {}; + char description[512] = {}; + + s.integer(signature); + s.integer(size); + s.integer(synchronize); + s.array(version); + s.array(description); + serializeAll(s, synchronize); + return s.size(); +} diff --git a/higan/spec/system/system.cpp b/higan/spec/system/system.cpp new file mode 100644 index 000000000..f1e8f54ba --- /dev/null +++ b/higan/spec/system/system.cpp @@ -0,0 +1,110 @@ +#include + +namespace higan::Spectrum { + +Scheduler scheduler; +ROM rom; +System system; +#include "serialization.cpp" + +auto System::run() -> void { + if(scheduler.enter() == Event::Frame) ula.refresh(); +} + +auto System::load(Node::Object& root) -> void { + if(node) unload(); + + information = {}; + if(interface->name() == "ZX Spectrum 48k" ) { + information.model = Model::Spectrum48k; + information.frequency = 3'500'000; + } + + if(interface->name() == "ZX Spectrum 128") { + information.model = Model::Spectrum128; + information.frequency = 3'546'900; + } + + node = Node::System::create(interface->name()); + root = node; + + regionNode = node->append("Region", "PAL"); + regionNode->setAllowedValues({ + "PAL", + }); + + scheduler.reset(); + cpu.load(node); + tapeDeck.load(node); + keyboard.load(node); + expansionPort.load(node); + ula.load(node); + + if (model() == Model::Spectrum128) { + psg.load(node); + } +} + +auto System::save() -> void { + if(!node) return; +} + +auto System::unload() -> void { + if(!node) return; + save(); + cpu.unload(); + ula.unload(); + keyboard.unload(); + tapeDeck.unload(); + expansionPort.unload(); + node = {}; + rom.bios.reset(); + + if (model() == Model::Spectrum128) { + psg.unload(); + rom.sub.reset(); + } +} + +auto System::power() -> void { + for(auto& setting : node->find()) setting->setLatch(); + + auto setRegion = [&](string region) { + if(region == "PAL") { + information.region = Region::PAL; + } + }; + auto regions = regionNode->latch().split("→").strip(); + setRegion(regions.first()); + + rom.bios.allocate(16_KiB); + if(auto fp = platform->open(node, "bios.rom", File::Read, File::Required)) { + rom.bios.load(fp); + } + + if (model() == Model::Spectrum128) { + rom.sub.allocate(16_KiB); + if(auto fp = platform->open(node, "sub.rom", File::Read, File::Required)) { + rom.sub.load(fp); + } + } + + romBank = 0; + ramBank = 0; + screenBank = 0; + pagingDisabled = 0; + + cpu.power(); + keyboard.power(); + ula.power(); + if (model() == Model::Spectrum128) { + psg.power(); + } + tapeDeck.power(); + scheduler.power(cpu); + + information.serializeSize[0] = serializeInit(0); + information.serializeSize[1] = serializeInit(1); +} + +} diff --git a/higan/spec/system/system.hpp b/higan/spec/system/system.hpp new file mode 100644 index 000000000..f2554dfa3 --- /dev/null +++ b/higan/spec/system/system.hpp @@ -0,0 +1,55 @@ +struct ROM { + Memory::Readable bios; + Memory::Readable sub; +}; + +struct System { + Node::Object node; + Node::String regionNode; + + enum class Model : uint { Spectrum48k, Spectrum128 }; + enum class Region : uint { PAL }; + + auto model() const -> Model { return information.model; } + auto region() const -> Region { return information.region; } + auto frequency() const -> double { return information.frequency; } + + //system.cpp + auto run() -> void; + + auto load(Node::Object&) -> void; + auto save() -> void; + auto unload() -> void; + + auto power() -> void; + + //serialization.cpp + auto serialize(bool synchronize) -> serializer; + auto unserialize(serializer&) -> bool; + + // 128k specific state + bool romBank; + bool screenBank; + uint3 ramBank; + bool pagingDisabled; +private: + struct Information { + Model model = Model::Spectrum48k; + Region region = Region::PAL; + double frequency = 3'500'000; + uint32 serializeSize[2]; + } information; + + //serialization.cpp + auto serialize(serializer&) -> void; + auto serializeAll(serializer&, bool synchronize) -> void; + auto serializeInit(bool synchronize) -> uint; +}; + +extern ROM rom; +extern System system; + +auto Model::Spectrum48k() -> bool { return system.model() == System::Model::Spectrum48k; } +auto Model::Spectrum128() -> bool { return system.model() == System::Model::Spectrum128; } + +auto Region::PAL() -> bool { return system.region() == System::Region::PAL; } diff --git a/higan/spec/tape/deck.cpp b/higan/spec/tape/deck.cpp new file mode 100644 index 000000000..54b19b33b --- /dev/null +++ b/higan/spec/tape/deck.cpp @@ -0,0 +1,20 @@ +TapeDeck tapeDeck{"Tape Deck"}; + +TapeDeck::TapeDeck(string name) : name(name) { +} + +auto TapeDeck::load(Node::Object parent) -> void { + node = parent->append(name); + + playStop = node->append("Play/Stop"); + tray.load(node); +} + +auto TapeDeck::power() -> void { + state = {}; +} + +auto TapeDeck::unload() -> void { + tray.unload(); + node = {}; +} diff --git a/higan/spec/tape/deck.hpp b/higan/spec/tape/deck.hpp new file mode 100644 index 000000000..0aea5ac35 --- /dev/null +++ b/higan/spec/tape/deck.hpp @@ -0,0 +1,23 @@ +struct TapeDeck { + Node::Peripheral node; + Node::Button playStop; + TapeDeckTray tray; + + TapeDeck(string name); + + auto playing() -> bool { return state.playing; } + auto read() -> uint1 { return state.output; } + + auto load(Node::Object) -> void; + auto unload() -> void; + auto power() -> void; + + const string name; + + struct { + bool playing; + uint1 output; + } state; +}; + +extern TapeDeck tapeDeck; diff --git a/higan/spec/tape/tape.cpp b/higan/spec/tape/tape.cpp new file mode 100644 index 000000000..14f147a78 --- /dev/null +++ b/higan/spec/tape/tape.cpp @@ -0,0 +1,92 @@ +#include + +namespace higan::Spectrum { + +#include "deck.cpp" +#include "tray.cpp" + +auto Tape::allocate(Node::Port parent) -> Node::Peripheral { + node = parent->append("ZX Spectrum (Tapes)"); + position = 0; + length = 0; + range = 0; + + return node; +} + +auto Tape::connect() -> void { + node->setManifest([&] { return information.manifest; }); + + information = {}; + + if(auto fp = platform->open(node, "manifest.bml", File::Read, File::Required)) { + information.manifest = fp->reads(); + } + + auto document = BML::unserialize(information.manifest); + information.name = document["game/label"].text(); + + auto location = node->attribute("location"); + if (file::exists({location, "program.tape"})) { + Decode::WAV wav; + if (wav.open({location, "program.tape"})) { + range = (1 << wav.bitrate) -1; + data.allocate(wav.size()); + position = 0; + frequency = wav.frequency; + length = wav.size(); + for(int i = 0; i < length; i++) { + data.write(i, wav.read()); + } + wav.close(); + + + stream = node->append("Audio"); + stream->setChannels(1); + stream->setFrequency(frequency); + Thread::create(frequency, [&] { Tape::main(); }); + } + } +} + +auto Tape::main() -> void { + platform->input(tapeDeck.playStop); + if (tapeDeck.playStop->value()) { + tapeDeck.state.playing ^= 1; + } + + if (!tapeDeck.state.playing) { + stream->sample(0.0f); + step(1); + return; + } + + if (position > length) { + tapeDeck.state.playing = 0; + step(1); + return; + } + + uint64 sample = data.read(position); + position++; + + stream->sample((float)sample / (float)range); + tapeDeck.state.output = sample > (range / 2); + step(1); +} + +auto Tape::step(uint clocks) -> void { + Thread::step(clocks); + Thread::synchronize(); +} + +auto Tape::disconnect() -> void { + if(!node) return; + Thread::destroy(); + data.reset(); + node = {}; + position = 0; + length = 0; +} + +} diff --git a/higan/spec/tape/tape.hpp b/higan/spec/tape/tape.hpp new file mode 100644 index 000000000..c0c75b9be --- /dev/null +++ b/higan/spec/tape/tape.hpp @@ -0,0 +1,33 @@ +struct Tape : Thread { + Node::Peripheral node; + Node::Stream stream; + + auto manifest() const -> string { return information.manifest; } + auto name() const -> string { return information.name; } + + //tape.cpp + auto allocate(Node::Port) -> Node::Peripheral; + auto connect() -> void; + auto disconnect() -> void; + + auto main() -> void; + auto step(uint clocks) -> void; + + auto save() -> void; + +private: + struct Information { + string manifest; + string name; + } information; + + uint64 position; + uint64 length; + uint64 range; + uint64 frequency; + Memory::Writable data; + +}; + +#include "tray.hpp" +#include "deck.hpp" diff --git a/higan/spec/tape/tray.cpp b/higan/spec/tape/tray.cpp new file mode 100644 index 000000000..700284e80 --- /dev/null +++ b/higan/spec/tape/tray.cpp @@ -0,0 +1,14 @@ +auto TapeDeckTray::load(Node::Object parent) -> void { + port = parent->append("Tray"); + port->setFamily(interface->name()); + port->setType("Tape"); + port->setHotSwappable(true); + port->setAllocate([&](auto name) { return tape.allocate(port); }); + port->setConnect([&] { return tape.connect(); }); + port->setDisconnect([&] { return tape.disconnect(); }); +} + +auto TapeDeckTray::unload() -> void { + tape.disconnect(); + port = {}; +} diff --git a/higan/spec/tape/tray.hpp b/higan/spec/tape/tray.hpp new file mode 100644 index 000000000..238a47ec5 --- /dev/null +++ b/higan/spec/tape/tray.hpp @@ -0,0 +1,9 @@ +struct TapeDeckTray { + Node::Port port; + Tape tape; + + auto load(Node::Object) -> void; + auto unload() -> void; +}; + +extern TapeDeckTray tapeDeckTray; diff --git a/higan/spec/ula/color.cpp b/higan/spec/ula/color.cpp new file mode 100644 index 000000000..94c4c422a --- /dev/null +++ b/higan/spec/ula/color.cpp @@ -0,0 +1,22 @@ +auto ULA::color(uint32 color) -> uint64 { + switch(color) { + case 0: return 0x0000'0000'0000ull; //black + case 1: return 0x0000'0000'd7d7ull; //blue + case 2: return 0xd7d7'0000'0000ull; //red + case 3: return 0xd7d7'0000'd7d7ull; //magenta + case 4: return 0x0000'd7d8'0000ull; //green + case 5: return 0x0000'd7d7'd7d7ull; //cyan + case 6: return 0xd7d7'd7d7'0000ull; //yellow + case 7: return 0xd7d7'd7d7'd7d7ull; //white + case 8: return 0x0000'0000'0000ull; //black + case 9: return 0x0000'0000'ffffull; //blue + case 10: return 0xffff'0000'0000ull; //red + case 11: return 0xffff'0000'ffffull; //magenta + case 12: return 0x0000'ffff'0000ull; //green + case 13: return 0x0000'ffff'ffffull; //cyan + case 14: return 0xffff'ffff'0000ull; //yellow + case 15: return 0xffff'ffff'ffffull; //white + } + + unreachable; +} diff --git a/higan/spec/ula/serialization.cpp b/higan/spec/ula/serialization.cpp new file mode 100644 index 000000000..19e3ecfd5 --- /dev/null +++ b/higan/spec/ula/serialization.cpp @@ -0,0 +1,4 @@ +auto ULA::serialize(serializer& s) -> void { + + Thread::serialize(s); +} diff --git a/higan/spec/ula/ula.cpp b/higan/spec/ula/ula.cpp new file mode 100644 index 000000000..efe13cbce --- /dev/null +++ b/higan/spec/ula/ula.cpp @@ -0,0 +1,174 @@ +#include + +namespace higan::Spectrum { + +ULA ula; +#include "color.cpp" +#include "serialization.cpp" + +auto ULA::load(Node::Object parent) -> void { + node = parent->append("ULA"); + + screen_ = node->append("Screen"); + screen_->colors(16, {&ULA::color, this}); + screen_->setSize(352, 296); + screen_->setScale(1.0, 1.0); + screen_->setAspect(1.0, 1.0); + + stream = node->append("Audio"); + stream->setChannels(1); + stream->setFrequency(system.frequency()); +} + +auto ULA::unload() -> void { + node = {}; + screen_ = {}; + stream = {}; +} + +auto ULA::main() -> void { + // Audio is sampled at system frequency, not pixel frequency + // So we sample on every even pixel + if ((hcounter & 1) == 0) { + stream->sample((0.75 * io.ear) + (0.25 * io.mic)); + } + + if (vcounter >= border_top_start && hcounter >= border_left_start) { + const auto pixel = ((vcounter - border_top_start) * 352) + hcounter - border_left_start; + + if (vcounter < screen_top_start || vcounter >= border_bottom_start) { + buffer[pixel] = io.borderColor; + } else if (hcounter < screen_left_start || hcounter >= border_right_start) { + buffer[pixel] = io.borderColor; + } else { + const uint16 y = vcounter - screen_top_start; + const uint16 x = hcounter - screen_left_start; + + const uint8 x_tile = x / 8; + + uint16 pixel_addr; + pixel_addr.bit( 0, 4) = x_tile; + pixel_addr.bit( 5, 7) = y.bit(3, 5); + pixel_addr.bit( 8, 10) = y.bit(0, 2); + pixel_addr.bit(11, 12) = y.bit(6, 7); + pixel_addr.bit(13, 15) = 0x02; + const uint8 pixels = fetch(pixel_addr); + + uint16 attr_addr; + attr_addr.bit( 0, 4) = x_tile; + attr_addr.bit( 5, 7) = y.bit(3, 5); + attr_addr.bit( 8, 9) = y.bit(6, 7); + attr_addr.bit(10, 15) = 0x16; + const uint8 attr = fetch(attr_addr); + + const auto ink = attr.bit(0, 2); + const auto paper = attr.bit(3, 5); + const auto bright = attr.bit(6); + const auto flash = attr.bit(7); + + const uint1 use_ink = (pixels.bit(7 - (x & 7))) ^ (flash & flashState); + buffer[pixel] = (use_ink ? ink : paper) + (bright * 8); + } + } + step(1); + + // Increment pixel counters + hcounter++; + if (hcounter == border_right_end) { + hcounter = 0; + vcounter++; + if (vcounter == border_bottom_end) { + vcounter = 0; + } + } + + // IRQ happens on the cycle as active display starts - 64 lines + if (hcounter == screen_left_start && vcounter == 0) { + if (++flashFrameCounter == 31) { + flashState ^= 1; + } + + cpu.setIrq(true); + frame(); + } + + // IRQ ends 64 pixel cycles (32 cpu cycles) from IRQ start + if (hcounter == screen_left_start + 64 && vcounter == 0) { + cpu.setIrq(false); + } +} + +auto ULA::step(uint clocks) -> void { + Thread::step(clocks); + Thread::synchronize(); +} + +auto ULA::frame() -> void { + scheduler.exit(Event::Frame); +} + +auto ULA::refresh() -> void { + screen_->refresh(buffer, 352 * sizeof(uint32), 352, 296); +} + +auto ULA::power() -> void { + Thread::create(system.frequency() * 2, [&] { ULA::main(); }); + hcounter = 0; + vcounter = 0; + flashFrameCounter = 0; + flashState = 0; + + if (Model::Spectrum128()) { + // Spectrum 128 has 311 lines per frame, rather than 312 + // Spectrum 128 has 456 pixel cycles per scanline, rather than 448 + border_top_start = 16; + screen_top_start = border_top_start + 48; + border_bottom_start = screen_top_start + 192; + border_bottom_end = border_bottom_start + 55; + border_left_start = 104; + screen_left_start = border_left_start + 48; + border_right_start = screen_left_start + 256; + border_right_end = border_right_start + 48; + } +} + +auto ULA::fetch(uint16 address) -> uint8 { + if (Model::Spectrum128()) { + busValue = cpu.readBanked(system.screenBank ? 7 : 5, address - 0x4000); + return busValue; + } + + busValue = cpu.ram.read(address - 0x4000); + return busValue; +} + +auto ULA::in(uint16 port) -> uint8 { + uint5 keys = 0x1f; + + for (uint n : range(8)) { + if (!port.bit(n + 8)) { + keys &= keyboard.read(n); + } + } + + uint8 value; + value.bit(0, 4) = keys; + value.bit(5) = 1; + + if (tapeDeck.playing()) { + value.bit(6) = tapeDeck.read(); + } else { + value.bit(6) = io.mic | io.ear; + } + + value.bit(7) = 1; + return value; +} + +auto ULA::out(uint8 data) -> void { + io.borderColor = data.bit(0, 2); + io.mic = data.bit(3); + io.ear = data.bit(4); +} + +} diff --git a/higan/spec/ula/ula.hpp b/higan/spec/ula/ula.hpp new file mode 100644 index 000000000..06856d170 --- /dev/null +++ b/higan/spec/ula/ula.hpp @@ -0,0 +1,49 @@ +struct ULA : Thread { + Node::Component node; + Node::Screen screen_; + Node::Stream stream; + + auto load(Node::Object) -> void; + auto unload() -> void; + + auto main() -> void; + auto step(uint clocks) -> void; + auto frame() -> void; + auto refresh() -> void; + auto power() -> void; + + auto fetch(uint16 address) -> uint8; + auto in(uint16 port) -> uint8; + auto out(uint8 data) -> void; + + auto activeDisplay() -> uint1 { return vcounter >= screen_top_start && vcounter < border_bottom_start && hcounter >= screen_left_start && hcounter < border_right_start; } + auto floatingBus() -> uint8 { return activeDisplay() ? busValue : (uint8)0xff; } + + auto serialize(serializer&) -> void; + + auto color(uint32 color) -> uint64; + + struct IO { + uint3 borderColor; + uint1 mic; + uint1 ear; + } io; + + uint32 buffer[352 * 296]; + uint16 hcounter; + uint16 vcounter; + uint5 flashFrameCounter; + uint1 flashState; + uint8 busValue; + + uint border_top_start = 16; + uint screen_top_start = border_top_start + 48; + uint border_bottom_start = screen_top_start + 192; + uint border_bottom_end = border_bottom_start + 56; + uint border_left_start = 96; + uint screen_left_start = border_left_start + 48; + uint border_right_start = screen_left_start + 256; + uint border_right_end = border_right_start + 48; +}; + +extern ULA ula; diff --git a/icarus/icarus.cpp b/icarus/icarus.cpp index e778242a6..d7f156951 100644 --- a/icarus/icarus.cpp +++ b/icarus/icarus.cpp @@ -22,6 +22,7 @@ auto operator+=(string& lhs, const string& rhs) -> string& { #include "cartridge/cartridge.cpp" #include "compact-disc/compact-disc.cpp" #include "floppy-disk/floppy-disk.cpp" +#include "tape/tape.cpp" #include "program/program.cpp" auto construct() -> void { @@ -48,6 +49,7 @@ auto construct() -> void { media.append(new PocketChallengeV2); media.append(new SC3000); media.append(new SG1000); + media.append(new ZXSpectrumTape); media.append(new SufamiTurbo); media.append(new SuperFamicom); media.append(new SuperGrafx); diff --git a/icarus/icarus.hpp b/icarus/icarus.hpp index 57e8e5a48..68a97373a 100644 --- a/icarus/icarus.hpp +++ b/icarus/icarus.hpp @@ -20,6 +20,7 @@ namespace icarus { #include "compact-disc/compact-disc.hpp" #include "compact-disc/playstation.hpp" #include "floppy-disk/floppy-disk.hpp" + #include "tape/tape.hpp" #include "program/program.hpp" extern vector> media; diff --git a/icarus/tape/tape.cpp b/icarus/tape/tape.cpp new file mode 100644 index 000000000..d2861d6fb --- /dev/null +++ b/icarus/tape/tape.cpp @@ -0,0 +1,52 @@ +#include "zx-spectrum-tape.cpp" + +auto Tape::construct() -> void { + Media::construct(); +} + +auto Tape::append(vector& output, string filename) -> bool { + if(!file::exists(filename)) return false; + auto input = file::read(filename); + auto size = output.size(); + output.resize(size + input.size()); + memory::copy(output.data() + size, input.data(), input.size()); + return true; +} + +auto Tape::manifest(string location) -> string { + vector data; + if(directory::exists(location)) { + data = export(location); + } else if(file::exists(location)) { + data = file::read(location); + } + return manifest(data, location); +} + +auto Tape::manifest(vector& data, string location) -> string { + string digest = Hash::SHA256(data).digest(); + for(auto game : database.find("game")) { + if(game["sha256"].text() == digest) return BML::serialize(game); + } + return heuristics(data, location); +} + +auto Tape::import(string location) -> string { + auto data = Media::read(location); + auto manifest = this->manifest(data, location); + if(!manifest) return "failed to parse tape"; + + auto document = BML::unserialize(manifest); + location = {pathname, Location::prefix(location), "/"}; + if(!directory::create(location)) return "output directory not writable"; + + if(settings.createManifests) { + file::write({location, "manifest.bml"}, manifest); + } + + location = {pathname, Location::prefix(location), "/"}; + if(!directory::create(location)) return "output directory not writable"; + + file::write({location, "program.tape"}, data); + return {}; +} diff --git a/icarus/tape/tape.hpp b/icarus/tape/tape.hpp new file mode 100644 index 000000000..4856d3c5b --- /dev/null +++ b/icarus/tape/tape.hpp @@ -0,0 +1,14 @@ +struct Tape : Media { + auto type() -> string override { return "Tape"; } + auto construct() -> void override; + auto manifest(string location) -> string override; + auto import(string filename) -> string override; + + auto append(vector& data, string filename) -> bool; + auto manifest(vector& data, string location) -> string; + + virtual auto export(string location) -> vector = 0; + virtual auto heuristics(vector& data, string location) -> string = 0; +}; + +#include "zx-spectrum-tape.hpp" diff --git a/icarus/tape/zx-spectrum-tape.cpp b/icarus/tape/zx-spectrum-tape.cpp new file mode 100644 index 000000000..f82e16bbb --- /dev/null +++ b/icarus/tape/zx-spectrum-tape.cpp @@ -0,0 +1,14 @@ +auto ZXSpectrumTape::export(string location) -> vector { + vector data; + append(data, {location, "program.tape"}); + return data; +} + +auto ZXSpectrumTape::heuristics(vector& data, string location) -> string { + string s; + s += "game\n"; + s +={" name: ", Media::name(location), "\n"}; + s +={" label: ", Media::name(location), "\n"}; + return s; +} + diff --git a/icarus/tape/zx-spectrum-tape.hpp b/icarus/tape/zx-spectrum-tape.hpp new file mode 100644 index 000000000..93b006fbe --- /dev/null +++ b/icarus/tape/zx-spectrum-tape.hpp @@ -0,0 +1,6 @@ +struct ZXSpectrumTape : Tape { + auto name() -> string override { return "ZX Spectrum (Tapes)"; } + auto extensions() -> vector override { return {"wav"}; } + auto export(string location) -> vector override; + auto heuristics(vector& data, string location) -> string override; +};